Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Product grid client side transitions with filters #7201

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
16ea028
Product Query: Fix pagination issue
gigitux Sep 12, 2022
f19fd99
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Sep 14, 2022
816ffe9
Merge branch 'trunk' of https://github.com/woocommerce/woocommerce-bl…
gigitux Sep 15, 2022
6e93dd0
Product Query - Add support for the Filter By Price Block #6790
gigitux Sep 15, 2022
b243528
fix query relation
gigitux Sep 16, 2022
e55a41d
fix on sale query
gigitux Sep 16, 2022
341d47b
Merge branch 'trunk' into add/price_filter_support
gigitux Sep 16, 2022
1fd15a8
Merge branch 'trunk' into add/price_filter_support
gigitux Sep 20, 2022
6d0de4d
Initial client-side navigation
luisherranz Sep 20, 2022
1508400
Add query-id class
luisherranz Sep 20, 2022
49ac769
Move full-vdom files to its own folder
luisherranz Sep 20, 2022
4fc1314
Hydrate only the Product Query blocks
luisherranz Sep 20, 2022
926f62c
Product Query - Add support for the Filter By Attributes block #6790
gigitux Sep 20, 2022
3176679
Merge branch 'trunk' into add/attribute_filter_support
gigitux Sep 21, 2022
1d56a2e
fix bugged pagination and on-sale filter after refactor
gigitux Sep 21, 2022
c9839f2
Merge branch 'add/price_filter_support' of https://github.com/woocomm…
gigitux Sep 21, 2022
9065ab3
Merge branch 'add/price_filter_support' of https://github.com/woocomm…
gigitux Sep 21, 2022
0be510e
Merge branch 'add/attribute_filter_support' of https://github.com/woo…
gigitux Sep 21, 2022
6f326eb
Merge ranch 'trunk' of https://github.com/woocommerce/woocommerce-blo…
gigitux Sep 22, 2022
d79142e
Merge branch 'add/attribute_filter_support' of https://github.com/woo…
gigitux Sep 22, 2022
1eea968
add compatibility with filters
gigitux Sep 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 43 additions & 37 deletions assets/js/blocks/attribute-filter/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,48 +275,54 @@ const AttributeFilterBlock = ( {
* @param {boolean} allFiltersRemoved If there are active filters or not.
*/
const updateFilterUrl = useCallback(
( query, allFiltersRemoved = false ) => {
if ( allFiltersRemoved ) {
if ( ! attributeObject?.taxonomy ) {
return;
}
const currentQueryArgKeys = Object.keys(
getQueryArgs( window.location.href )
);

const parsedTaxonomy = parseTaxonomyToGenerateURL(
attributeObject.taxonomy
);

const url = currentQueryArgKeys.reduce(
( currentUrl, queryArg ) =>
queryArg.includes(
PREFIX_QUERY_ARG_QUERY_TYPE + parsedTaxonomy
) ||
queryArg.includes(
PREFIX_QUERY_ARG_FILTER_TYPE + parsedTaxonomy
)
? removeQueryArgs( currentUrl, queryArg )
: currentUrl,
window.location.href
);

const newUrl = formatParams( url, query );
changeUrl( newUrl );
} else {
const newUrl = formatParams( pageUrl, query );
const currentQueryArgs = getQueryArgs( window.location.href );
const newUrlQueryArgs = getQueryArgs( newUrl );

if ( ! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs ) ) {
async ( query, allFiltersRemoved = false ) => {
return await new Promise( () => {
if ( allFiltersRemoved ) {
if ( ! attributeObject?.taxonomy ) {
return;
}
const currentQueryArgKeys = Object.keys(
getQueryArgs( window.location.href )
);

const parsedTaxonomy = parseTaxonomyToGenerateURL(
attributeObject.taxonomy
);

const url = currentQueryArgKeys.reduce(
( currentUrl, queryArg ) =>
queryArg.includes(
PREFIX_QUERY_ARG_QUERY_TYPE + parsedTaxonomy
) ||
queryArg.includes(
PREFIX_QUERY_ARG_FILTER_TYPE + parsedTaxonomy
)
? removeQueryArgs( currentUrl, queryArg )
: currentUrl,
window.location.href
);

const newUrl = formatParams( url, query );
changeUrl( newUrl );
} else {
const newUrl = formatParams( pageUrl, query );
const currentQueryArgs = getQueryArgs(
window.location.href
);
const newUrlQueryArgs = getQueryArgs( newUrl );

if (
! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs )
) {
changeUrl( newUrl );
}
}
}
} );
},
[ pageUrl, attributeObject?.taxonomy ]
);

const onSubmit = ( checkedFilters: string[] ) => {
const onSubmit = async ( checkedFilters: string[] ) => {
const query = updateAttributeFilter(
productAttributesQuery,
setProductAttributesQuery,
Expand All @@ -325,7 +331,7 @@ const AttributeFilterBlock = ( {
blockAttributes.queryType === 'or' ? 'in' : 'and'
);

updateFilterUrl( query, checkedFilters.length === 0 );
await updateFilterUrl( query, checkedFilters.length === 0 );
};

const updateCheckedFilters = useCallback(
Expand Down
46 changes: 24 additions & 22 deletions assets/js/blocks/price-filter/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,30 +169,32 @@ const PriceFilterBlock = ( {

// Updates the query based on slider values.
const onSubmit = useCallback(
( newMinPrice, newMaxPrice ) => {
const finalMaxPrice =
newMaxPrice >= Number( maxConstraint )
? undefined
: newMaxPrice;
const finalMinPrice =
newMinPrice <= Number( minConstraint )
? undefined
: newMinPrice;

if ( window ) {
const newUrl = formatParams( window.location.href, {
min_price: finalMinPrice / 10 ** currency.minorUnit,
max_price: finalMaxPrice / 10 ** currency.minorUnit,
} );

// If the params have changed, lets update the filter URL.
if ( window.location.href !== newUrl ) {
changeUrl( newUrl );
async ( newMinPrice, newMaxPrice ) => {
return await new Promise( () => {
const finalMaxPrice =
newMaxPrice >= Number( maxConstraint )
? undefined
: newMaxPrice;
const finalMinPrice =
newMinPrice <= Number( minConstraint )
? undefined
: newMinPrice;

if ( window ) {
const newUrl = formatParams( window.location.href, {
min_price: finalMinPrice / 10 ** currency.minorUnit,
max_price: finalMaxPrice / 10 ** currency.minorUnit,
} );

// If the params have changed, lets update the filter URL.
if ( window.location.href !== newUrl ) {
changeUrl( newUrl );
}
}
}

setMinPriceQuery( finalMinPrice );
setMaxPriceQuery( finalMaxPrice );
setMinPriceQuery( finalMinPrice );
setMaxPriceQuery( finalMaxPrice );
} );
},
[
minConstraint,
Expand Down
61 changes: 61 additions & 0 deletions assets/js/blocks/product-query/frontend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { options } from 'preact';
import { useEffect } from 'preact/hooks';

/**
* Internal dependencies
*/
import { prefetch, navigate, directive } from './full-vdom';

// The `wp-client-navigation` directive.
directive( 'clientNavigation', ( props ) => {
const {
wp: { clientNavigation },
href,
} = props;
const url = href.startsWith( '/' ) ? href : window.location.pathname + href;

useEffect( () => {
// Prefetch the page if it is in the directive options.
if ( clientNavigation?.prefetch ) {
prefetch( url );
}
}, [ url ] );

// Don't do anything if it's falsy.
if ( clientNavigation !== false ) {
props.onclick = async ( event ) => {
// Stop server-side navigation.
event.preventDefault();
// Start client-side navigation.
await navigate( url, { scroll: clientNavigation?.scroll } );
};
}
} );

// Manually add the `wp-client-navigation` directive to the virtual nodes.
// TODO: Move this to the HTML once WP_HTML_Walker is available.
const clientNavigationClassNames = [
'wp-block-query-pagination-next',
'wp-block-query-pagination-previous',
'page-numbers',
];
const old = options.vnode;
options.vnode = ( vnode ) => {
if ( vnode.type === 'a' ) {
clientNavigationClassNames.forEach( ( className ) => {
if ( vnode.props.class?.includes( className ) ) {
vnode.props.wp = {
clientNavigation: {
prefetch:
className === 'page-numbers' ? false : 'eager',
scroll: false,
},
};
}
} );
}
if ( old ) old( vnode );
};
39 changes: 39 additions & 0 deletions assets/js/blocks/product-query/full-vdom/directives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { h, options } from 'preact';

// WordPress Directives.
const directives = {};

// Expose function to add directives.
export const directive = ( name, cb ) => {
directives[ name ] = cb;
};

const WpDirective = ( props ) => {
for ( const d in props.wp ) {
directives[ d ]?.( props );
}
props._wrapped = true;
const { wp, tag, children, ...rest } = props;
return h( tag, rest, children );
};

const old = options.vnode;

options.vnode = ( vnode ) => {
const wp = vnode.props.class;
const wrapped = vnode.props._wrapped;

if ( wp ) {
if ( ! wrapped ) {
vnode.props.tag = vnode.type;
vnode.type = WpDirective;
}
} else if ( wrapped ) {
delete vnode.props._wrapped;
}

if ( old ) old( vnode );
};
2 changes: 2 additions & 0 deletions assets/js/blocks/product-query/full-vdom/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { navigate, prefetch } from './router';
export { directive } from './directives';
94 changes: 94 additions & 0 deletions assets/js/blocks/product-query/full-vdom/router-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* External dependencies
*/
import { hydrate, render } from 'preact';

/**
* Internal dependencies
*/
import { toVdom } from './vdom';

// Remove domain and hash from the URL. We are only interesting in the path and
// the query.
export const cleanUrl = ( url ) => {
const u = new URL( url, 'http://a.bc' );
return u.pathname + u.search;
};

// Helper to await until the CPU is idle.
export const idle = () =>
new Promise( ( resolve ) => window.requestIdleCallback( resolve ) );

// Get the id class from a Product Query element.
const getQueryId = ( query ) =>
Array.from( query.classList.values() ).find( ( className ) =>
className.startsWith( 'query-id-' )
);

// Root fragments where we will render the Product Query blocks.
const rootFragments = new Map();

// Create root fragments for each Product Query block.
export const createRootFragments = () => {
document.querySelectorAll( '.woo-product-query' ).forEach( ( query ) => {
rootFragments.set(
getQueryId( query ),
createRootFragment( query.parentElement, query )
);
} );
};

// Fetch a URL and return the HTML string.
const fetchUrl = async ( url ) => {
return await window.fetch( url ).then( ( res ) => res.text() );
};

// Parse a DOM from an HTML string.
const parseDom = ( html ) => {
return new window.DOMParser().parseFromString( html, 'text/html' );
};

// Fetch a page and return the virtual DOM of each Product Query block.
export const fetchPage = async ( url ) => {
const html = await fetchUrl( url );
const dom = parseDom( html );
return toVdoms( dom );
};

// Build a virtual DOM for each Product Query block found in a document.
export const toVdoms = ( dom ) => {
const vdoms = new Map();
dom.querySelectorAll( '.woo-product-query' ).forEach( ( query ) => {
vdoms.set( getQueryId( query ), toVdom( query ) );
} );
return vdoms;
};

// Render the virtual DOM of each Product Query block.
export const renderVdoms = ( vdoms, initial = false ) => {
const r = initial ? hydrate : render;
vdoms.forEach( ( vdom, id ) => {
r( vdom, rootFragments.get( id ) );
} );
};

// We use this for wrapperless hydration.
// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
const createRootFragment = ( parent, replaceNode ) => {
replaceNode = [].concat( replaceNode );
const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
function insert( c, r ) {
parent.insertBefore( c, r || s );
}
return ( parent.__k = {
nodeType: 1,
parentNode: parent,
firstChild: replaceNode[ 0 ],
childNodes: replaceNode,
insertBefore: insert,
appendChild: insert,
removeChild( c ) {
parent.removeChild( c );
},
} );
};
Loading