Skip to content

Commit

Permalink
Merge pull request #21 from woocommerce/try/hydrating-all-products-block
Browse files Browse the repository at this point in the history
All Products block: Try hydrating
  • Loading branch information
michalczaplinski authored Aug 4, 2022
2 parents f11f11c + 8bbbac9 commit 7dc3f43
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 29 deletions.
64 changes: 64 additions & 0 deletions assets/js/base/utils/bhe-element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import { hydrate as ReactHydrate } from 'react-dom';
import { ReactElement } from 'react';

type HydrateOptions = {
technique?: 'media' | 'view' | 'idle';
media?: string;
};

export const hydrate = (
element: ReactElement,
container: Element,
hydrationOptions: HydrateOptions = {}
) => {
const { technique, media } = hydrationOptions;
const cb = () => {
ReactHydrate( element, container );
};
switch ( technique ) {
case 'media':
if ( media ) {
const mql = matchMedia( media );
if ( mql.matches ) {
cb();
} else {
mql.addEventListener( 'change', cb, { once: true } );
}
}
break;
// Hydrate the element when is visible in the viewport.
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
case 'view':
try {
const io = new IntersectionObserver( ( entries ) => {
for ( const entry of entries ) {
if ( ! entry.isIntersecting ) {
continue;
}
// As soon as we hydrate, disconnect this IntersectionObserver.
io.disconnect();
cb();
break; // break loop on first match
}
} );
io.observe( container.children[ 0 ] );
} catch ( e ) {
cb();
}
break;
case 'idle':
// Safari does not support requestIdleCalback, we use a timeout instead. https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
if ( 'requestIdleCallback' in window ) {
window.requestIdleCallback( cb );
} else {
setTimeout( cb, 200 );
}
break;
// Hydrate this component immediately.
default:
cb();
}
};
162 changes: 162 additions & 0 deletions assets/js/base/utils/bhe-frontend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* External dependencies
*/
import { ReactElement } from 'react';

/**
* Internal dependencies
*/
import { matcherFromSource, pickKeys } from './utils';
import { hydrate } from './bhe-element';

declare global {
interface Window {
blockTypes: Map< string, ReactElement >;
}
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-unused-vars
namespace JSX {
interface IntrinsicElements {
// eslint-disable-next-line @typescript-eslint/naming-convention
'wp-inner-blocks': React.DetailedHTMLProps<
React.HTMLAttributes< HTMLElement >,
HTMLElement
>;
}
}
}

// We assign `blockTypes` to window to make sure it's a global singleton.
//
// Have to do this because of the way we are currently bundling the code
// in this repo, each block gets its own copy of this file.
//
// We COULD fix this by doing some webpack magic to spit out the code in
// `gutenberg-packages` to a shared chunk but assigning `blockTypes` to window
// is a cheap hack for now that will be fixed once we can merge this code into Gutenberg.
if ( typeof window.blockTypes === 'undefined' ) {
window.blockTypes = new Map();
}

export const registerBlockType = ( name: string, Comp: ReactElement ) => {
window.blockTypes.set( name, Comp );
};

const Children = ( { value, providedContext } ) => {
if ( ! value ) {
return null;
}
return (
<wp-inner-blocks
ref={ ( el ) => {
if ( el !== null ) {
// listen for the ping from the child
el.addEventListener( 'wp-block-context', ( event ) => {
// We have to also destructure `event.detail.context` because there can
// already exist a property in the context with the same name.
event.detail.context = {
...providedContext,
...event?.detail?.context,
};
} );
}
} }
suppressHydrationWarning={ true }
dangerouslySetInnerHTML={ { __html: value } }
/>
);
};
Children.shouldComponentUpdate = () => false;

class WpBlock extends HTMLElement {
connectedCallback() {
setTimeout( () => {
// ping the parent for the context
const event = new CustomEvent( 'wp-block-context', {
detail: {},
bubbles: true,
cancelable: true,
} );
this.dispatchEvent( event );

const usesContext = JSON.parse(
this.getAttribute(
'data-wp-block-uses-block-context'
) as string
);
const providesContext = JSON.parse(
this.getAttribute(
'data-wp-block-provides-block-context'
) as string
);
const attributes = JSON.parse(
this.getAttribute( 'data-wp-block-attributes' ) as string
);
const sourcedAttributes = JSON.parse(
this.getAttribute(
'data-wp-block-sourced-attributes'
) as string
);

for ( const attr in sourcedAttributes ) {
attributes[ attr ] = matcherFromSource(
sourcedAttributes[ attr ]
)( this );
}

// pass the context to children if needed
const providedContext =
providesContext &&
pickKeys( attributes, Object.keys( providesContext ) );

// select only the parts of the context that the block declared in
// the `usesContext` of its block.json
const context = pickKeys( event.detail.context, usesContext );

const blockType = this.getAttribute( 'data-wp-block-type' );
const blockProps = {
className: this.children[ 0 ].className,
style: this.children[ 0 ].style,
};

const innerBlocks = this.querySelector( 'wp-inner-blocks' );
const Comp = window.blockTypes.get( blockType );
const technique = this.getAttribute( 'data-wp-block-hydrate' );
const media = this.getAttribute( 'data-wp-block-media' );
const hydrationOptions = { technique, media };
hydrate(
<>
<Comp
attributes={ attributes }
blockProps={ blockProps }
suppressHydrationWarning={ true }
context={ context }
>
<Children
value={ innerBlocks && innerBlocks.innerHTML }
suppressHydrationWarning={ true }
providedContext={ providedContext }
/>
</Comp>
<template
className="wp-inner-blocks"
suppressHydrationWarning={ true }
/>
</>,
this,
hydrationOptions
);
} );
}
}

// We need to wrap the element registration code in a conditional for the same
// reason we assing `blockTypes` to window (see top of the file).
//
// We need to ensure that the component registration code is only run once
// because it throws if you try to register an element with the same name twice.
if ( customElements.get( 'wp-block' ) === undefined ) {
customElements.define( 'wp-block', WpBlock );
}
33 changes: 33 additions & 0 deletions assets/js/base/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { text } from 'hpq';

/**
* Pick the keys of an object that are present in the provided array.
*
* @param {Object} obj
* @param {Array} arr
*/
export const pickKeys = ( obj, arr ) => {
if ( obj === undefined ) {
return;
}

const result = {};
for ( const key of arr ) {
if ( obj[ key ] !== undefined ) {
result[ key ] = obj[ key ];
}
}
return result;
};

// See https://github.com/WordPress/gutenberg/blob/trunk/packages/blocks/src/api/parser/get-block-attributes.js#L185
export const matcherFromSource = ( sourceConfig ) => {
switch ( sourceConfig.source ) {
// TODO: Add cases for other source types.
case 'text':
return text( sourceConfig.selector );
}
};
21 changes: 14 additions & 7 deletions assets/js/blocks/products/all-products/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,33 @@
},
"attributes": {
"columns": {
"type": "number"
"type": "number",
"public": "true"
},
"rows": {
"type": "number"
"type": "number",
"public": "true"
},
"alignButtons": {
"type": "boolean"
"type": "boolean",
"public": "true"
},
"contentVisibility": {
"type": "object"
"type": "object",
"public": "true"
},
"orderby": {
"type": "string"
"type": "string",
"public": "true"
},
"layoutConfig": {
"type": "array"
"type": "array",
"public": "true"
},
"isPreview": {
"type": "boolean",
"default": false
"default": false,
"public": "true"
}
}
}
29 changes: 28 additions & 1 deletion assets/js/blocks/products/all-products/deprecated.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ import { getBlockClassName } from '../utils.js';

const { attributes: attributeDefinitions } = metadata;

const v2 = {
attributes: attributeDefinitions,
save( { attributes } ) {
const dataAttributes = {};
Object.keys( attributes )
.sort()
.forEach( ( key ) => {
dataAttributes[ key ] = attributes[ key ];
} );
const data = {
'data-attributes': JSON.stringify( dataAttributes ),
};

return (
<div
className={ getBlockClassName(
'wc-block-all-products',
attributes
) }
{ ...data }
>
<InnerBlocks.Content />
</div>
);
},
};

const v1 = {
attributes: Object.assign( {}, attributeDefinitions, {
rows: { type: 'number', default: 1 },
Expand All @@ -33,4 +60,4 @@ const v1 = {
},
};

export default [ v1 ];
export default [ v2, v1 ];
14 changes: 3 additions & 11 deletions assets/js/blocks/products/all-products/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* External dependencies
*/
import { StoreNoticesProvider } from '@woocommerce/base-context';
import { renderFrontend } from '@woocommerce/base-utils';

/**
* Internal dependencies
*/
import Block from './block';
import Block from './block.js';
import { registerBlockType } from '../../../base/utils/bhe-frontend';

/**
* Wrapper component to supply the notice provider.
Expand All @@ -22,12 +22,4 @@ const AllProductsFrontend = ( props ) => {
);
};

const getProps = ( el ) => ( {
attributes: JSON.parse( el.dataset.attributes ),
} );

renderFrontend( {
selector: '.wp-block-woocommerce-all-products',
Block: AllProductsFrontend,
getProps,
} );
registerBlockType( 'woocommerce/all-products', AllProductsFrontend );
10 changes: 0 additions & 10 deletions assets/js/blocks/products/all-products/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,12 @@ import { InnerBlocks } from '@wordpress/block-editor';
import { getBlockClassName } from '../utils.js';

export default function save( { attributes } ) {
const dataAttributes = {};
Object.keys( attributes )
.sort()
.forEach( ( key ) => {
dataAttributes[ key ] = attributes[ key ];
} );
const data = {
'data-attributes': JSON.stringify( dataAttributes ),
};
return (
<div
className={ getBlockClassName(
'wc-block-all-products',
attributes
) }
{ ...data }
>
<InnerBlocks.Content />
</div>
Expand Down
Loading

0 comments on commit 7dc3f43

Please sign in to comment.