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

Support BlockContext between different blocks #7

Merged
merged 11 commits into from
May 24, 2022
6 changes: 3 additions & 3 deletions block-hydration-experiments.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: block-hydration-experiments
*/

function block_hydration_experiments_init() {
register_block_type( __DIR__ . '/build' );
register_block_type( plugin_dir_path( __FILE__ ) . 'build/blocks/block-hydration-experiments-child/' );
register_block_type( plugin_dir_path( __FILE__ ) . 'build/blocks/block-hydration-experiments-parent/' );
}
add_action( 'init', 'block_hydration_experiments_init' );
add_action( 'init', 'block_hydration_experiments_init' );
9,016 changes: 4,149 additions & 4,867 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/blocks/block-hydration-experiments-child/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "luisherranz/block-hydration-experiments-child",
"version": "0.1.0",
"title": "BHE - Child",
"category": "text",
"icon": "flag",
"description": "",
"usesContext": ["message"],
"supports": {
"color": {
"text": true
},
"html": true
},
"textdomain": "block-hydration-experiments-child",
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"viewScript": "file:./view.js"
}
19 changes: 19 additions & 0 deletions src/blocks/block-hydration-experiments-child/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This import is needed to ensure that the `wp.blockEditor` global is available
// by the time this component gets loaded. The `Title` component consumes the
// global but cannot import it because it shouldn't be loaded on the frontend of
// the site.
import '@wordpress/block-editor';
Comment on lines +1 to +5
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this a little bit? This block doesn't use the Title component, does it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it seems that it does work fine even if I remove the import '@wordpress/block-editor'; which makes sense.

I have added it because it seemed make the warning about wp.RichText go away, but looks like I don't need it after all.

import { useBlockProps } from '@wordpress/block-editor';

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

return (
<div {...blockProps}>
<p>Child element</p>
{context?.message}
</div>
);
};

export default Text;
10 changes: 10 additions & 0 deletions src/blocks/block-hydration-experiments-child/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Text from '../../frontend/text';
import { registerBlockType } from '../../gutenberg-packages/wordpress-blocks';
import Edit from './edit';
import './style.scss';

registerBlockType( 'luisherranz/block-hydration-experiments-child', {
edit: Edit,
// The Save component is derived from the Frontend component.
frontend: Text,
} );
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.wp-block-luisherranz-block-hydration-experiments {
.wp-block-luisherranz-block-hydration-experiments-child {
padding: 15px 10px 15px 50px;
background-color: rgb(238, 237, 237);
}
Expand Down
4 changes: 4 additions & 0 deletions src/blocks/block-hydration-experiments-child/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Text from '../../frontend/text';
import { registerBlockType } from '../../gutenberg-packages/frontend';

registerBlockType( 'luisherranz/block-hydration-experiments-child', Text );
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "luisherranz/block-hydration-experiments",
"name": "luisherranz/block-hydration-experiments-parent",
"version": "0.1.0",
"title": "Block Hydration Experiments",
"title": "BHE - Parent",
"category": "text",
"icon": "flag",
"description": "",
Expand All @@ -21,7 +21,10 @@
},
"html": true
},
"textdomain": "block-hydration-experiments",
"providesContext": {
"message": "message"
},
"textdomain": "block-hydration-experiments-parent",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import '@wordpress/block-editor';

import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import Title from '../shared/title';
import Title from '../../shared/title';

export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
Expand Down
10 changes: 10 additions & 0 deletions src/blocks/block-hydration-experiments-parent/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Frontend from '../../frontend';
import { registerBlockType } from '../../gutenberg-packages/wordpress-blocks';
import Edit from './edit';
import './style.scss';

registerBlockType( 'luisherranz/block-hydration-experiments-parent', {
edit: Edit,
// The Save component is derived from the Frontend component.
frontend: Frontend,
} );
9 changes: 9 additions & 0 deletions src/blocks/block-hydration-experiments-parent/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.wp-block-luisherranz-block-hydration-experiments-parent {
padding: 15px 10px 15px 50px;
background-color: rgb(238, 237, 237);
}

gutenberg-block,
gutenberg-inner-blocks {
display: contents;
}
4 changes: 4 additions & 0 deletions src/blocks/block-hydration-experiments-parent/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Frontend from '../../frontend';
import { registerBlockType } from '../../gutenberg-packages/frontend';

registerBlockType( 'luisherranz/block-hydration-experiments-parent', Frontend );
Empty file removed src/editor.scss
Empty file.
10 changes: 10 additions & 0 deletions src/frontend/text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const Text = ( { blockProps, context } ) => {
return (
<div {...blockProps}>
<p>Child element</p>
{context?.message}
</div>
);
};

export default Text;
66 changes: 60 additions & 6 deletions src/gutenberg-packages/frontend.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import { pickKeys } from './utils';
import { EnvContext, hydrate } from './wordpress-element';

const blockTypes = new Map();
// 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.
Comment on lines +4 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the comment I explain why I'm doing window.blockTypes (it's a hack).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this should be part of the Gutenberg APIs, so we can keep the hack for now 🙂

if ( typeof window.blockTypes === 'undefined' ) {
window.blockTypes = new Map();
}

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

const Children = ( { value } ) => {
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 ) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying this is right or wrong, but what's your reasoning for adding the event listener to the guntenberg-inner-blocks instead of the block wrapper? 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is the only option we have if I'm not mistaken. Lemme explain in a video:

2022-05-23_20-24-04.mp4

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for the video, Michal 🙂

I think those problems can be circumvented, but we shouldn't worry for now, this is perfectly fine as it is. If there's a simpler solution, it will present itself once we do more complex stuff!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for completeness, I also thought we could wrap the Comp like in a "useless" div like:

<div
  style={{ display: 'contents' }}
  ref={el => { /* add the eventListener here */ } }
>
  <Comp
    attributes={attributes}
    blockProps={blockProps}
    suppressHydrationWarning={true}
    context={context}
  >
    <Children
      value={innerBlocks && innerBlocks.innerHTML}
      suppressHydrationWarning={true}
    />
  </Comp>
</div>
					

but that doesn't work well - it was e.g. breaking some styles. And that <div> does not have the blockProps applied to it which can cause other problems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the benefit of using an extra div versus using this as you explain in the video?

this points to the <gutenberg-interactive-block> wrapper.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would be no benefit, that's what I was trying to say. But the issue is that you only want to ask the parent that is one level up for the context:

I've realized that we could also do this:

// inside of the connectedCallback()

this.addEventListener( 'gutenberg-context', ( event ) => {
  if ( this !== event.target ) { // <- only set the context if we're in the parent. 
    event.stopPropagation();
    event.detail.context = providedContext;
  }
} );

// ping the parent for the context
const event = new CustomEvent( 'gutenberg-context', {
  detail: {},
  bubbles: true,
  cancelable: true,
} );

this.dispatchEvent( event );

So, this way we can both listen to and fire the event on the same element (the <interactive-gutenberg-block>) but only set the context if we're in the parent. This works because the event bubbles up.

Was this what you were suggesting or did you have another idea in mind?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so 🙂

I think you could also add the event listener after you've dispatched the event so they don't interfere.

event.stopPropagation();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to stop the propagation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! I explain below in the video 🙂

event.detail.context = providedContext;
} );
}
}}
suppressHydrationWarning={true}
dangerouslySetInnerHTML={{ __html: value }}
/>
Expand All @@ -22,17 +42,42 @@ Children.shouldComponentUpdate = () => false;
class GutenbergBlock extends HTMLElement {
connectedCallback() {
setTimeout( () => {
const blockType = this.getAttribute( 'data-gutenberg-block-type' );
// ping the parent for the context
const event = new CustomEvent( 'gutenberg-context', {
detail: {},
bubbles: true,
cancelable: true,
} );
this.dispatchEvent( event );

const usesContext = JSON.parse(
this.getAttribute( 'data-gutenberg-context-used' ),
);
const providesContext = JSON.parse(
this.getAttribute( 'data-gutenberg-context-provided' ),
);
const attributes = JSON.parse(
this.getAttribute( 'data-gutenberg-attributes' ),
);

// pass the context to children if needed
const providedContext = 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-gutenberg-block-type' );
const blockProps = JSON.parse(
this.getAttribute( 'data-gutenberg-block-props' ),
);
const innerBlocks = this.querySelector(
'template.gutenberg-inner-blocks',
);
const Comp = blockTypes.get( blockType );
const Comp = window.blockTypes.get( blockType );
const technique = this.getAttribute( 'data-gutenberg-hydrate' );
const media = this.getAttribute( 'data-gutenberg-media' );
const hydrationOptions = { technique, media };
Expand All @@ -42,10 +87,12 @@ class GutenbergBlock extends HTMLElement {
attributes={attributes}
blockProps={blockProps}
suppressHydrationWarning={true}
context={context}
>
<Children
value={innerBlocks && innerBlocks.innerHTML}
suppressHydrationWarning={true}
providedContext={providedContext}
/>
</Comp>
<template
Expand All @@ -60,4 +107,11 @@ class GutenbergBlock extends HTMLElement {
}
}

customElements.define( 'gutenberg-interactive-block', GutenbergBlock );
// 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 ) {
customElements.define( 'gutenberg-interactive-block', GutenbergBlock );
}
Comment on lines -63 to +117
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directing your attention to the comment 🙂

25 changes: 25 additions & 0 deletions src/gutenberg-packages/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,28 @@ export const getFrontendAttributes = ( blockName, attributes ) => {

return frontendAttributes;
};

export const getBlockContext = ( blockName ) => {
const blockType = getBlockType( blockName );
const { usesContext, providesContext } = blockType;
return { usesContext, providesContext };
};

/**
* 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;
};
1 change: 1 addition & 0 deletions src/gutenberg-packages/wordpress-blockeditor.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@wordpress/block-editor';
import { useBlockEnvironment } from './wordpress-element';

export const RichText = ( { tagName: Tag, children, ...props } ) => {
Expand Down
5 changes: 4 additions & 1 deletion src/gutenberg-packages/wordpress-blocks.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import { registerBlockType as gutenbergRegisterBlockType } from '@wordpress/blocks';
import { getFrontendAttributes } from './utils';
import { getBlockContext, getFrontendAttributes } from './utils';

const save = ( name, Comp ) =>
( { attributes } ) => {
const blockProps = useBlockProps.save();
const frontendAttributes = getFrontendAttributes( name, attributes );
const { usesContext, providesContext } = getBlockContext( name );
return (
<gutenberg-interactive-block
data-gutenberg-block-type={name}
data-gutenberg-context-used={JSON.stringify( usesContext )}
data-gutenberg-context-provided={JSON.stringify( providesContext )}
data-gutenberg-attributes={JSON.stringify( frontendAttributes )}
data-gutenberg-block-props={JSON.stringify( blockProps )}
data-gutenberg-hydrate='idle'
Expand Down
11 changes: 0 additions & 11 deletions src/index.js

This file was deleted.

4 changes: 0 additions & 4 deletions src/view.js

This file was deleted.