Interactive Price Filter: use `context` instead of `state` (#42980)

* feat: use context instead of state

* fix: temporary move the context to inner element for diffing to work

* fix: update context before navigation for optimistic UI
This commit is contained in:
Tung Du 2023-12-22 22:05:31 +07:00 committed by GitHub
parent d14be998f5
commit 0c5d01a6ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 140 additions and 130 deletions

View File

@ -52,7 +52,7 @@ export const PriceSlider = ( { attributes }: EditProps ) => {
);
return (
<>
<div>
<div className="range">
<div className="range-bar"></div>
<input
@ -76,6 +76,6 @@ export const PriceSlider = ( { attributes }: EditProps ) => {
{ priceMin }
{ priceMax }
</div>
</>
</div>
);
};

View File

@ -1,21 +1,21 @@
/**
* External dependencies
*/
import { store, navigate } from '@woocommerce/interactivity';
import { store, navigate, getContext } from '@woocommerce/interactivity';
import { formatPrice, getCurrency } from '@woocommerce/price-format';
import { HTMLElementEvent } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { PriceFilterState } from './types';
import type { PriceFilterContext, PriceFilterStore } from './types';
const getHrefWithFilters = ( state: PriceFilterState ) => {
const { minPrice = 0, maxPrice = 0, maxRange = 0 } = state;
const getUrl = ( context: PriceFilterContext ) => {
const { minPrice, maxPrice, minRange, maxRange } = context;
const url = new URL( window.location.href );
const { searchParams } = url;
if ( minPrice > 0 ) {
if ( minPrice > minRange ) {
searchParams.set( 'min_price', minPrice.toString() );
} else {
searchParams.delete( 'min_price' );
@ -34,80 +34,73 @@ const getHrefWithFilters = ( state: PriceFilterState ) => {
return url.href;
};
interface PriceFilterStore {
state: PriceFilterState;
actions: {
setMinPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => void;
setMaxPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => void;
updateProducts: () => void;
reset: () => void;
};
}
store< PriceFilterStore >( 'woocommerce/collection-price-filter', {
state: {
rangeStyle: () => {
const { minPrice, maxPrice, minRange, maxRange } =
getContext< PriceFilterContext >();
const { state } = store< PriceFilterStore >(
'woocommerce/collection-price-filter',
{
state: {
get rangeStyle(): string {
const {
minPrice = 0,
maxPrice = 0,
minRange = 0,
maxRange = 0,
} = state;
return [
`--low: ${
( 100 * ( minPrice - minRange ) ) /
( maxRange - minRange )
}%`,
`--high: ${
( 100 * ( maxPrice - minRange ) ) /
( maxRange - minRange )
}%`,
].join( ';' );
},
get formattedMinPrice(): string {
const { minPrice = 0 } = state;
return formatPrice( minPrice, getCurrency( { minorUnit: 0 } ) );
},
get formattedMaxPrice(): string {
const { maxPrice = 0 } = state;
return formatPrice( maxPrice, getCurrency( { minorUnit: 0 } ) );
},
return [
`--low: ${
( 100 * ( minPrice - minRange ) ) / ( maxRange - minRange )
}%`,
`--high: ${
( 100 * ( maxPrice - minRange ) ) / ( maxRange - minRange )
}%`,
].join( ';' );
},
actions: {
setMinPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => {
const { minRange = 0, maxPrice = 0, maxRange = 0 } = state;
const value = parseFloat( event.target.value );
state.minPrice = Math.min(
Number.isNaN( value ) ? minRange : value,
maxRange - 1
);
state.maxPrice = Math.max( maxPrice, state.minPrice + 1 );
},
setMaxPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => {
const {
minRange = 0,
minPrice = 0,
maxPrice = 0,
maxRange = 0,
} = state;
const value = parseFloat( event.target.value );
state.maxPrice = Math.max(
Number.isNaN( value ) ? maxRange : value,
minRange + 1
);
state.minPrice = Math.min( minPrice, maxPrice - 1 );
},
updateProducts: () => {
navigate( getHrefWithFilters( state ) );
},
reset: () => {
const { maxRange = 0 } = state;
state.minPrice = 0;
state.maxPrice = maxRange;
navigate( getHrefWithFilters( state ) );
},
formattedMinPrice: () => {
const { minPrice } = getContext< PriceFilterContext >();
return formatPrice( minPrice, getCurrency( { minorUnit: 0 } ) );
},
}
);
formattedMaxPrice: () => {
const { maxPrice } = getContext< PriceFilterContext >();
return formatPrice( maxPrice, getCurrency( { minorUnit: 0 } ) );
},
},
actions: {
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => {
const context = getContext< PriceFilterContext >();
const { minRange, minPrice, maxPrice, maxRange } = context;
const type = event.target.name;
const value = parseFloat( event.target.value );
const currentMinPrice =
type === 'min'
? Math.min(
Number.isNaN( value ) ? minRange : value,
maxRange - 1
)
: minPrice;
const currentMaxPrice =
type === 'max'
? Math.max(
Number.isNaN( value ) ? maxRange : value,
minRange + 1
)
: maxPrice;
context.minPrice = currentMinPrice;
context.maxPrice = currentMaxPrice;
navigate(
getUrl( {
minRange,
maxRange,
minPrice: currentMinPrice,
maxPrice: currentMaxPrice,
} )
);
},
reset: () => {
navigate(
getUrl( {
minRange: 0,
maxRange: 0,
minPrice: 0,
maxPrice: 0,
} )
);
},
},
} );

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { BlockEditProps } from '@wordpress/blocks';
import { HTMLElementEvent } from '@woocommerce/types';
export type BlockAttributes = {
showInputFields: boolean;
@ -11,11 +12,26 @@ export type BlockAttributes = {
export type EditProps = BlockEditProps< BlockAttributes >;
export type PriceFilterState = {
minPrice?: number;
maxPrice?: number;
minRange?: number;
maxRange?: number;
rangeStyle: string;
formattedMinPrice: string;
formattedMaxPrice: string;
rangeStyle: () => string;
formattedMinPrice: () => string;
formattedMaxPrice: () => string;
};
export type PriceFilterContext = {
minPrice: number;
maxPrice: number;
minRange: number;
maxRange: number;
};
export type FilterComponentProps = BlockEditProps< BlockAttributes > & {
collectionData: Partial< PriceFilterState >;
};
export type PriceFilterStore = {
state: PriceFilterState;
actions: {
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => void;
reset: () => void;
};
};

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Convert the Price Filter `state` to `context` to enhance its reactivity.

View File

@ -121,7 +121,6 @@ final class CollectionPriceFilter extends AbstractBlock {
$price_range = $block->context['collectionData']['price_range'];
$wrapper_attributes = get_block_wrapper_attributes();
$min_range = $price_range['min_price'] / 10 ** $price_range['currency_minor_unit'];
$max_range = $price_range['max_price'] / 10 ** $price_range['currency_minor_unit'];
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
@ -156,12 +155,10 @@ final class CollectionPriceFilter extends AbstractBlock {
$__high = 100 * ( $max_price - $min_range ) / ( $max_range - $min_range );
$range_style = "--low: $__low%; --high: $__high%";
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-price-filter' ) );
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
'data-wc-interactive' => $data_directive,
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-price-filter' ) ),
)
);
@ -171,8 +168,7 @@ final class CollectionPriceFilter extends AbstractBlock {
class="min"
type="text"
value="%d"
data-wc-bind--value="state.minPrice"
data-wc-on--input="actions.setMinPrice"
data-wc-bind--value="context.minPrice"
data-wc-on--change="actions.updateProducts"
/>',
esc_attr( $min_price )
@ -188,8 +184,7 @@ final class CollectionPriceFilter extends AbstractBlock {
class="max"
type="text"
value="%d"
data-wc-bind--value="state.maxPrice"
data-wc-on--input="actions.setMaxPrice"
data-wc-bind--value="context.maxPrice"
data-wc-on--change="actions.updateProducts"
/>',
esc_attr( $max_price )
@ -202,41 +197,43 @@ final class CollectionPriceFilter extends AbstractBlock {
ob_start();
?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div
class="range"
style="<?php echo esc_attr( $range_style ); ?>"
data-wc-bind--style="state.rangeStyle"
>
<div class="range-bar"></div>
<input
type="range"
class="min"
min="<?php echo esc_attr( $min_range ); ?>"
max="<?php echo esc_attr( $max_range ); ?>"
value="<?php echo esc_attr( $min_price ); ?>"
data-wc-bind--max="state.maxRange"
data-wc-bind--value="state.minPrice"
data-wc-class--active="state.isMinActive"
data-wc-on--input="actions.setMinPrice"
data-wc-on--change="actions.updateProducts"
<div data-wc-context="<?php echo esc_attr( wp_json_encode( $data ) ); ?>" >
<div
class="range"
style="<?php echo esc_attr( $range_style ); ?>"
data-wc-bind--style="state.rangeStyle"
>
<input
type="range"
class="max"
min="<?php echo esc_attr( $min_range ); ?>"
max="<?php echo esc_attr( $max_range ); ?>"
value="<?php echo esc_attr( $max_price ); ?>"
data-wc-bind--max="state.maxRange"
data-wc-bind--value="state.maxPrice"
data-wc-class--active="state.isMaxActive"
data-wc-on--input="actions.setMaxPrice"
data-wc-on--change="actions.updateProducts"
>
</div>
<div class="text">
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<div class="range-bar"></div>
<input
type="range"
class="min"
name="min"
min="<?php echo esc_attr( $min_range ); ?>"
max="<?php echo esc_attr( $max_range ); ?>"
value="<?php echo esc_attr( $min_price ); ?>"
data-wc-bind--min="context.minRange"
data-wc-bind--max="context.maxRange"
data-wc-bind--value="context.minPrice"
data-wc-on--change="actions.updateProducts"
>
<input
type="range"
class="max"
name="max"
min="<?php echo esc_attr( $min_range ); ?>"
max="<?php echo esc_attr( $max_range ); ?>"
value="<?php echo esc_attr( $max_price ); ?>"
data-wc-bind--min="context.minRange"
data-wc-bind--max="context.maxRange"
data-wc-bind--value="context.maxPrice"
data-wc-on--change="actions.updateProducts"
>
</div>
<div class="text">
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
</div>
</div>
<?php