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:
parent
d14be998f5
commit
0c5d01a6ac
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
Comment: Convert the Price Filter `state` to `context` to enhance its reactivity.
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue