Skip to content

Commit

Permalink
Merge branch 'feature/add-custom-element' into try/hydrating-all-prod…
Browse files Browse the repository at this point in the history
…ucts-block
  • Loading branch information
ockham committed Jul 29, 2022
2 parents d77049b + f7b465b commit 6e39a2c
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 0 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 );
}
2 changes: 2 additions & 0 deletions assets/js/base/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from './get-valid-block-attributes';
export * from './product-data';
export * from './derive-selected-shipping-rates';
export * from './get-icons-from-payment-methods';
export * from './bhe-frontend';
export * from './bhe-element';
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 );
}
};
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
"dataloader": "2.1.0",
"dinero.js": "1.9.1",
"downshift": "6.1.7",
"hpq": "^1.3.0",
"html-react-parser": "0.14.3",
"react-number-format": "4.9.3",
"reakit": "1.3.11",
Expand Down

0 comments on commit 6e39a2c

Please sign in to comment.