From d61391cf240e82b5edafbd8f3af081f41331318d Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Fri, 22 Jul 2022 16:53:00 +0200 Subject: [PATCH] Finish and refactor frontend --- block-hydration-experiments.php | 4 +- .../block.json | 2 +- .../block-hydration-experiments-child/edit.js | 5 +- .../frontend.js | 10 +- .../block.json | 2 +- src/gutenberg-packages/frontend.js | 261 ++++++++---------- src/gutenberg-packages/react-context.js | 66 +++++ src/gutenberg-packages/utils.js | 29 +- src/gutenberg-packages/wordpress-blocks.js | 15 +- src/gutenberg-packages/wordpress-element.js | 12 +- 10 files changed, 213 insertions(+), 193 deletions(-) create mode 100644 src/gutenberg-packages/react-context.js diff --git a/block-hydration-experiments.php b/block-hydration-experiments.php index 4ee9f5af..9c19f592 100644 --- a/block-hydration-experiments.php +++ b/block-hydration-experiments.php @@ -53,8 +53,8 @@ function bhe_block_wrapper( $block_content, $block, $instance ) { $block_wrapper = sprintf( '', diff --git a/src/blocks/block-hydration-experiments-child/block.json b/src/blocks/block-hydration-experiments-child/block.json index ff300949..26d8385a 100644 --- a/src/blocks/block-hydration-experiments-child/block.json +++ b/src/blocks/block-hydration-experiments-child/block.json @@ -7,7 +7,7 @@ "category": "text", "icon": "flag", "description": "", - "usesContext": ["message"], + "usesContext": ["bhe/title"], "supports": { "color": { "text": true diff --git a/src/blocks/block-hydration-experiments-child/edit.js b/src/blocks/block-hydration-experiments-child/edit.js index 7ea9a311..8999ddab 100644 --- a/src/blocks/block-hydration-experiments-child/edit.js +++ b/src/blocks/block-hydration-experiments-child/edit.js @@ -7,10 +7,11 @@ import { useBlockProps } from '@wordpress/block-editor'; const Edit = ( { context } ) => { const blockProps = useBlockProps(); + return (
-

Child element

-

Block Context: {context?.message}

+

Child block

+

Block Context - "bhe/title": {context['bhe/title']}

); }; diff --git a/src/blocks/block-hydration-experiments-child/frontend.js b/src/blocks/block-hydration-experiments-child/frontend.js index 65fd2898..076671bb 100644 --- a/src/blocks/block-hydration-experiments-child/frontend.js +++ b/src/blocks/block-hydration-experiments-child/frontend.js @@ -4,14 +4,14 @@ import { useContext } from '../../gutenberg-packages/wordpress-element'; const Frontend = ( { blockProps, context } ) => { const theme = useContext( ThemeContext ); - const value = useContext( CounterContext ); + const counter = useContext( CounterContext ); return (
-

Child element

-

Block Context: {context?.message}

-

React Context: {value}

-

Theme: {theme}

+

Child block

+

Block Context - "bhe/title": {context['bhe/title']}

+

React Context - "counter": {counter}

+

React Context - "theme": {theme}

); }; diff --git a/src/blocks/block-hydration-experiments-parent/block.json b/src/blocks/block-hydration-experiments-parent/block.json index 7fd50445..ab737f89 100644 --- a/src/blocks/block-hydration-experiments-parent/block.json +++ b/src/blocks/block-hydration-experiments-parent/block.json @@ -30,7 +30,7 @@ "html": true }, "providesContext": { - "message": "message" + "bhe/title": "message" }, "textdomain": "block-hydration-experiments-parent", "editorScript": "file:./index.js", diff --git a/src/gutenberg-packages/frontend.js b/src/gutenberg-packages/frontend.js index 5a51e6c0..b210a80f 100644 --- a/src/gutenberg-packages/frontend.js +++ b/src/gutenberg-packages/frontend.js @@ -1,195 +1,157 @@ -import { matcherFromSource, pickKeys } from './utils'; -import { EnvContext, hydrate, useEffect, useState } from './wordpress-element'; - -// 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. - -// We create a variable for weakmap just to have a quick switch for testing, -// but we can update it later on Gutenberg or other projects. -const createGlobalMap = ( { mapName, weakmap = false } ) => { - if ( typeof window[mapName] === 'undefined' ) { - window[mapName] = weakmap ? new WeakMap() : new Map(); - } -}; -createGlobalMap( { mapName: 'blockTypes' } ); +import { Consumer, createProvider } from './react-context'; +import { createGlobal, matcherFromSource } from './utils'; +import { EnvContext, hydrate } from './wordpress-element'; -export const registerBlockType = ( name, Comp, options ) => { - window.blockTypes.set( name, { Component: Comp, options } ); -}; +const blockTypes = createGlobal( 'gutenbergBlockTypes', new Map() ); -const Children = ( { value, providedContext } ) => { - if ( !value ) { - return null; - } - return ( - { - if ( el !== null ) { - // listen for the ping from the child - el.addEventListener( 'gutenberg-context', ( event ) => { - event.stopPropagation(); - event.detail.context = providedContext; - } ); - } - }} - suppressHydrationWarning={true} - dangerouslySetInnerHTML={{ __html: value }} - /> - ); +export const registerBlockType = ( name, Component, options ) => { + blockTypes.set( name, { Component, options } ); }; -Children.shouldComponentUpdate = () => false; -const ConditionalWrapper = ( { condition, wrapper, children } ) => - condition ? wrapper( children ) : children; - -// We assign `subscribers` to window to not duplicate its creation. -createGlobalMap( { mapName: 'subscribers', weakmap: true } ); -createGlobalMap( { mapName: 'currentValue', weakmap: true } ); - -const subscribers = window.subscribers; -const currentValue = window.currentValue; - -const subscribeProvider = ( Context, setValue, block ) => { - if ( !subscribers.has( block ) ) { - subscribers.set( block, new Map() ); - } - if ( !subscribers.get( block ).has( Context ) ) { - subscribers.get( block ).set( Context, new Set() ); - } - subscribers.get( block ).get( Context ).add( setValue ); -}; +const Children = ( { value } ) => ( + +); +Children.shouldComponentUpdate = () => false; -const updateProviders = ( Context, value, block ) => { - if ( !currentValue.has( block ) ) { - currentValue.set( block, new Map() ); - } - if ( !currentValue.get( block ).has( Context ) ) { - currentValue.get( block ).set( Context, value ); - } - if ( subscribers.has( block ) && subscribers.get( block ).has( Context ) ) { - // This setTimeout prevents a React warning about calling setState in a render() function. - setTimeout( () => { - subscribers - .get( block ) - .get( Context ) - .forEach(setValue => setValue( value )); - } ); - } +const Wrappers = ( { wrappers, children } ) => { + let result = children; + wrappers.forEach( ( wrapper ) => { + result = wrapper( { children: result } ); + } ); + return result; }; class GutenbergBlock extends HTMLElement { connectedCallback() { setTimeout( () => { - let Provider; - // ping the parent for the context - const event = new CustomEvent( 'gutenberg-context', { - detail: {}, - bubbles: true, - cancelable: true, - } ); - this.dispatchEvent( event ); + const blockContext = {}; + const Providers = []; - const usesContext = JSON.parse( - this.getAttribute( 'data-gutenberg-context-used' ), - ); - const providesContext = JSON.parse( - this.getAttribute( 'data-gutenberg-context-provided' ), - ); + // Get the block attributes. const attributes = JSON.parse( this.getAttribute( 'data-gutenberg-attributes' ), ); + + // Add the sourced attributes to the attributes object. const sourcedAttributes = JSON.parse( this.getAttribute( 'data-gutenberg-sourced-attributes' ), ); - 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 ) ); + // Get the Block Context from their parents. + const usesBlockContext = JSON.parse( + this.getAttribute( 'data-gutenberg-uses-block-context' ), + ); + if ( usesBlockContext ) { + const event = new CustomEvent( 'gutenberg-block-context', { + detail: { context: {} }, + bubbles: true, + cancelable: true, + } ); + this.dispatchEvent( event ); + + // Select only the parts of the context that the block declared in the + // `usesContext` of its block.json. + usesBlockContext.forEach(key => + blockContext[key] = event.detail.context[key] + ); + } - // 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 ); + // Prepare to share the Block Context with their children. + const providesBlockContext = JSON.parse( + this.getAttribute( 'data-gutenberg-provides-block-context' ), + ); + if ( providesBlockContext ) { + this.addEventListener( 'gutenberg-block-context', ( event ) => { + // Select only the parts of the context that the block declared in + // the `providesContext` of its block.json. + Object.entries( providesBlockContext ).forEach( + ( [ key, attribute ] ) => { + if ( !event.detail.context[key] ) { + event.detail.context[key] = attributes[attribute]; + } + }, + ); + } ); + } + // Get the block type, block props, inner blocks, frontend component and + // options. const blockType = this.getAttribute( 'data-gutenberg-block-type' ); const blockProps = { className: this.children[0].className, style: this.children[0].style, }; - const innerBlocks = this.querySelector( 'gutenberg-inner-blocks' ); - const { Component, options } = window.blockTypes.get( blockType ); + const { Component, options } = blockTypes.get( blockType ); + + // Get the React Context from their parents. + options?.usesContext?.forEach( ( context ) => { + const event = new CustomEvent( 'gutenberg-react-context', { + detail: { context }, + bubbles: true, + cancelable: true, + } ); + this.dispatchEvent( event ); + Providers.push( event.detail.Provider ); + } ); + + // Prepare to share the React Context with their children. if ( options?.providesContext?.length > 0 ) { - options?.providesContext.forEach( ( providedContext, index ) => { - this.addEventListener( `react-context-${index}`, ( event ) => { - // we compare provided and used context - if ( event.detail.context === providedContext ) { - const Context = providedContext; - const Provider = ( { children } ) => { - const [ value, setValue ] = useState( - currentValue.get( this ).get( Context ), - ); - useEffect( () => { - subscribeProvider( Context, setValue, this ); - }, [] ); - return ( - {children} - ); - }; - event.detail.Provider = Provider; + this.addEventListener( 'gutenberg-react-context', ( event ) => { + for ( const context of options.providesContext ) { + // We compare the provided context with the received context. + if ( event.detail.context === context ) { + // If there's a match, we stop propagation. event.stopPropagation(); + + // We return a Provider that is subscribed to the parent Provider. + event.detail.Provider = createProvider( { + element: this, + context, + } ); + + // We can stop the iteration. + break; } - } ); - } ); - } - if ( options?.usesContext?.length > 0 ) { - options?.usesContext.forEach( ( usesContext, index ) => { - const contextEvent = new CustomEvent( `react-context-${index}`, { - detail: { context: usesContext }, - bubbles: true, - cancelable: true, - } ); - this.dispatchEvent( contextEvent ); - Provider = contextEvent.detail.Provider; + } } ); } + + // Get the hydration technique. const technique = this.getAttribute( 'data-gutenberg-hydrate' ); const media = this.getAttribute( 'data-gutenberg-media' ); const hydrationOptions = { technique, media }; + hydrate( - {children}} - > + {/* Wrap the component with all the React Providers */} + - {options?.providesContext?.length > 0 && - options.providesContext.map(( Context, index ) => ( - - {value => updateProviders( Context, value, this )} - - ))} - + {/* Update the value each time one of the React Contexts changes */} + {options?.providesContext?.map(( context, index ) => ( + + ))} + + {/* Render the inner blocks */} + {innerBlocks && ( + + )} - + +