Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Commit

Permalink
Finish and refactor frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
luisherranz committed Jul 22, 2022
1 parent b9e60dc commit d61391c
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 193 deletions.
4 changes: 2 additions & 2 deletions block-hydration-experiments.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ function bhe_block_wrapper( $block_content, $block, $instance ) {
$block_wrapper = sprintf(
'<gutenberg-interactive-block ' .
'data-gutenberg-block-type="%1$s" ' .
'data-gutenberg-context-used="%2$s" ' .
'data-gutenberg-context-provided="%3$s" ' .
'data-gutenberg-uses-block-context="%2$s" ' .
'data-gutenberg-provides-block-context="%3$s" ' .
'data-gutenberg-attributes="%4$s" ' .
'data-gutenberg-sourced-attributes="%5$s" ' .
'data-gutenberg-hydrate="idle">',
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/block-hydration-experiments-child/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"category": "text",
"icon": "flag",
"description": "",
"usesContext": ["message"],
"usesContext": ["bhe/title"],
"supports": {
"color": {
"text": true
Expand Down
5 changes: 3 additions & 2 deletions src/blocks/block-hydration-experiments-child/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { useBlockProps } from '@wordpress/block-editor';

const Edit = ( { context } ) => {
const blockProps = useBlockProps();

return (
<div {...blockProps}>
<p>Child element</p>
<p>Block Context: {context?.message}</p>
<p>Child block</p>
<p>Block Context - "bhe/title": {context['bhe/title']}</p>
</div>
);
};
Expand Down
10 changes: 5 additions & 5 deletions src/blocks/block-hydration-experiments-child/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div {...blockProps}>
<p>Child element</p>
<p>Block Context: {context?.message}</p>
<p>React Context: {value}</p>
<p>Theme: {theme}</p>
<p>Child block</p>
<p>Block Context - "bhe/title": {context['bhe/title']}</p>
<p>React Context - "counter": {counter}</p>
<p>React Context - "theme": {theme}</p>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/block-hydration-experiments-parent/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"html": true
},
"providesContext": {
"message": "message"
"bhe/title": "message"
},
"textdomain": "block-hydration-experiments-parent",
"editorScript": "file:./index.js",
Expand Down
261 changes: 110 additions & 151 deletions src/gutenberg-packages/frontend.js
Original file line number Diff line number Diff line change
@@ -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 (
<gutenberg-inner-blocks
ref={( el ) => {
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 } ) => (
<gutenberg-inner-blocks
suppressHydrationWarning={true}
dangerouslySetInnerHTML={{ __html: 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 (
<Context.Provider value={value}>{children}</Context.Provider>
);
};
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(
<EnvContext.Provider value='frontend'>
<ConditionalWrapper
condition={Provider !== undefined}
wrapper={children => <Provider>{children}</Provider>}
>
{/* Wrap the component with all the React Providers */}
<Wrappers wrappers={Providers}>
<Component
attributes={attributes}
blockProps={blockProps}
suppressHydrationWarning={true}
context={context}
context={blockContext}
>
{options?.providesContext?.length > 0 &&
options.providesContext.map(( Context, index ) => (
<Context.Consumer key={index}>
{value => updateProviders( Context, value, this )}
</Context.Consumer>
))}
<Children
value={innerBlocks && innerBlocks.innerHTML}
suppressHydrationWarning={true}
providedContext={providedContext}
/>
{/* Update the value each time one of the React Contexts changes */}
{options?.providesContext?.map(( context, index ) => (
<Consumer key={index} element={this} context={context} />
))}

{/* Render the inner blocks */}
{innerBlocks && (
<Children
value={innerBlocks.innerHTML}
suppressHydrationWarning={true}
/>
)}
</Component>
</ConditionalWrapper>
</Wrappers>

<template
className='gutenberg-inner-blocks'
suppressHydrationWarning={true}
Expand All @@ -202,9 +164,6 @@ class GutenbergBlock extends HTMLElement {
}
}

// 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( 'gutenberg-interactive-block' ) === undefined ) {
Expand Down
Loading

0 comments on commit d61391c

Please sign in to comment.