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

View File

@ -1,21 +1,21 @@
/** /**
* External dependencies * External dependencies
*/ */
import { store, navigate } from '@woocommerce/interactivity'; import { store, navigate, getContext } from '@woocommerce/interactivity';
import { formatPrice, getCurrency } from '@woocommerce/price-format'; import { formatPrice, getCurrency } from '@woocommerce/price-format';
import { HTMLElementEvent } from '@woocommerce/types'; import { HTMLElementEvent } from '@woocommerce/types';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { PriceFilterState } from './types'; import type { PriceFilterContext, PriceFilterStore } from './types';
const getHrefWithFilters = ( state: PriceFilterState ) => { const getUrl = ( context: PriceFilterContext ) => {
const { minPrice = 0, maxPrice = 0, maxRange = 0 } = state; const { minPrice, maxPrice, minRange, maxRange } = context;
const url = new URL( window.location.href ); const url = new URL( window.location.href );
const { searchParams } = url; const { searchParams } = url;
if ( minPrice > 0 ) { if ( minPrice > minRange ) {
searchParams.set( 'min_price', minPrice.toString() ); searchParams.set( 'min_price', minPrice.toString() );
} else { } else {
searchParams.delete( 'min_price' ); searchParams.delete( 'min_price' );
@ -34,80 +34,73 @@ const getHrefWithFilters = ( state: PriceFilterState ) => {
return url.href; return url.href;
}; };
interface PriceFilterStore { store< PriceFilterStore >( 'woocommerce/collection-price-filter', {
state: PriceFilterState; state: {
actions: { rangeStyle: () => {
setMinPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => void; const { minPrice, maxPrice, minRange, maxRange } =
setMaxPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => void; getContext< PriceFilterContext >();
updateProducts: () => void;
reset: () => void;
};
}
const { state } = store< PriceFilterStore >( return [
'woocommerce/collection-price-filter', `--low: ${
{ ( 100 * ( minPrice - minRange ) ) / ( maxRange - minRange )
state: { }%`,
get rangeStyle(): string { `--high: ${
const { ( 100 * ( maxPrice - minRange ) ) / ( maxRange - minRange )
minPrice = 0, }%`,
maxPrice = 0, ].join( ';' );
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 } ) );
},
}, },
actions: { formattedMinPrice: () => {
setMinPrice: ( event: HTMLElementEvent< HTMLInputElement > ) => { const { minPrice } = getContext< PriceFilterContext >();
const { minRange = 0, maxPrice = 0, maxRange = 0 } = state; return formatPrice( minPrice, getCurrency( { minorUnit: 0 } ) );
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 ) );
},
}, },
} 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 * External dependencies
*/ */
import { BlockEditProps } from '@wordpress/blocks'; import { BlockEditProps } from '@wordpress/blocks';
import { HTMLElementEvent } from '@woocommerce/types';
export type BlockAttributes = { export type BlockAttributes = {
showInputFields: boolean; showInputFields: boolean;
@ -11,11 +12,26 @@ export type BlockAttributes = {
export type EditProps = BlockEditProps< BlockAttributes >; export type EditProps = BlockEditProps< BlockAttributes >;
export type PriceFilterState = { export type PriceFilterState = {
minPrice?: number; rangeStyle: () => string;
maxPrice?: number; formattedMinPrice: () => string;
minRange?: number; formattedMaxPrice: () => string;
maxRange?: number; };
rangeStyle: string;
formattedMinPrice: string; export type PriceFilterContext = {
formattedMaxPrice: string; 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']; $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']; $min_range = $price_range['min_price'] / 10 ** $price_range['currency_minor_unit'];
$max_range = $price_range['max_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 ) ); $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 ); $__high = 100 * ( $max_price - $min_range ) / ( $max_range - $min_range );
$range_style = "--low: $__low%; --high: $__high%"; $range_style = "--low: $__low%; --high: $__high%";
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-price-filter' ) );
$wrapper_attributes = get_block_wrapper_attributes( $wrapper_attributes = get_block_wrapper_attributes(
array( array(
'class' => $show_input_fields && $inline_input ? 'inline-input' : '', '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" class="min"
type="text" type="text"
value="%d" value="%d"
data-wc-bind--value="state.minPrice" data-wc-bind--value="context.minPrice"
data-wc-on--input="actions.setMinPrice"
data-wc-on--change="actions.updateProducts" data-wc-on--change="actions.updateProducts"
/>', />',
esc_attr( $min_price ) esc_attr( $min_price )
@ -188,8 +184,7 @@ final class CollectionPriceFilter extends AbstractBlock {
class="max" class="max"
type="text" type="text"
value="%d" value="%d"
data-wc-bind--value="state.maxPrice" data-wc-bind--value="context.maxPrice"
data-wc-on--input="actions.setMaxPrice"
data-wc-on--change="actions.updateProducts" data-wc-on--change="actions.updateProducts"
/>', />',
esc_attr( $max_price ) esc_attr( $max_price )
@ -202,41 +197,43 @@ final class CollectionPriceFilter extends AbstractBlock {
ob_start(); ob_start();
?> ?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>> <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div <div data-wc-context="<?php echo esc_attr( wp_json_encode( $data ) ); ?>" >
class="range" <div
style="<?php echo esc_attr( $range_style ); ?>" class="range"
data-wc-bind--style="state.rangeStyle" 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"
> >
<input <div class="range-bar"></div>
type="range" <input
class="max" type="range"
min="<?php echo esc_attr( $min_range ); ?>" class="min"
max="<?php echo esc_attr( $max_range ); ?>" name="min"
value="<?php echo esc_attr( $max_price ); ?>" min="<?php echo esc_attr( $min_range ); ?>"
data-wc-bind--max="state.maxRange" max="<?php echo esc_attr( $max_range ); ?>"
data-wc-bind--value="state.maxPrice" value="<?php echo esc_attr( $min_price ); ?>"
data-wc-class--active="state.isMaxActive" data-wc-bind--min="context.minRange"
data-wc-on--input="actions.setMaxPrice" data-wc-bind--max="context.maxRange"
data-wc-on--change="actions.updateProducts" data-wc-bind--value="context.minPrice"
> data-wc-on--change="actions.updateProducts"
</div> >
<div class="text"> <input
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?> type="range"
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> class="max"
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 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>
</div> </div>
<?php <?php