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

Commit

Permalink
Merge pull request #66 from WordPress/full-vdom/hydrate-vdom-from-dom
Browse files Browse the repository at this point in the history
Hydrate the blocks with full vDOM (using `wp-block` and `wp-inner-blocks`)
  • Loading branch information
DAreRodz authored Sep 13, 2022
2 parents b39e7d6 + b0c6ed7 commit 7f5d00e
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 77 deletions.
9 changes: 8 additions & 1 deletion block-hydration-experiments.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
*/
function block_hydration_experiments_init()
{
wp_enqueue_script('vendors', plugin_dir_url(__FILE__) . 'build/vendors.js');

wp_register_script(
'hydration',
plugin_dir_url(__FILE__) . 'build/gutenberg-packages/hydration.js',
Expand Down Expand Up @@ -116,7 +118,12 @@ function bhe_block_wrapper($block_content, $block, $instance)
$template_wrapper,
sprintf($block_wrapper, $block_content . $empty_template)
);
return sprintf($block_wrapper, $block_content);

// The block content comes between two line breaks that seem to be included during block
// serialization, corresponding to those between the block markup and the block content.
//
// They need to be removed here; otherwise, the preact hydration fails.
return sprintf($block_wrapper, substr($block_content, 1, -1));
}

add_filter('render_block', 'bhe_block_wrapper', 10, 3);
8 changes: 6 additions & 2 deletions src/blocks/interactive-child/view.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import CounterContext from '../../context/counter';
import ThemeContext from '../../context/theme';
import { useContext } from '../../gutenberg-packages/wordpress-element';

const View = ({ blockProps, context }) => {
const theme = 'cool theme';
const counter = 0;
const theme = useContext(ThemeContext);
const counter = useContext(CounterContext);

return (
<div {...blockProps}>
Expand Down
20 changes: 11 additions & 9 deletions src/blocks/interactive-parent/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@
// the site.
import '@wordpress/block-editor';

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

const Edit = ({ attributes: { counter, title, secret }, setAttributes }) => (
const Edit = ({
attributes: { counter = 0, title, secret },
setAttributes,
}) => (
<>
<div {...useBlockProps()}>
<Title
<RichText
tagName="h2"
className="title"
value={title}
onChange={(val) => setAttributes({ title: val })}
placeholder="This will be passed through context to child blocks"
>
{title}
</Title>
<Button>Show</Button>
/>
<button>Show</button>
<button onClick={() => setAttributes({ counter: counter + 1 })}>
{counter}
</button>
Expand Down
5 changes: 0 additions & 5 deletions src/blocks/interactive-parent/shared/button.js

This file was deleted.

7 changes: 0 additions & 7 deletions src/blocks/interactive-parent/shared/title.js

This file was deleted.

15 changes: 6 additions & 9 deletions src/blocks/interactive-parent/view.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { createContext, useState } from 'preact/compat';
import Button from './shared/button';
import Title from './shared/title';

const Counter = createContext(null);
const Theme = createContext(null);
import Counter from '../../context/counter';
import Theme from '../../context/theme';
import { useState } from '../../gutenberg-packages/wordpress-element';

const View = ({
blockProps: {
Expand All @@ -27,9 +24,9 @@ const View = ({
fontWeight: bold ? 900 : fontWeight,
}}
>
<Title>{title}</Title>
<Button handler={() => setShow(!show)}>Show</Button>
<Button handler={() => setBold(!bold)}>Bold</Button>
<h2 className="title">{title}</h2>
<button onClick={() => setShow(!show)}>Show</button>
<button onClick={() => setBold(!bold)}>Bold</button>
<button onClick={() => setCounter(counter + 1)}>
{counter}
</button>
Expand Down
7 changes: 2 additions & 5 deletions src/blocks/non-interactive-parent/edit.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import { RichText } from '../../gutenberg-packages/wordpress-blockeditor';
import { InnerBlocks, useBlockProps, RichText } from '@wordpress/block-editor';

const Edit = ({ attributes, setAttributes }) => (
<div {...useBlockProps()}>
Expand All @@ -9,9 +8,7 @@ const Edit = ({ attributes, setAttributes }) => (
onChange={(val) => setAttributes({ title: val })}
placeholder="This will be passed through context to child blocks"
value={attributes.title}
>
{attributes.title}
</RichText>
/>
<InnerBlocks />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/non-interactive-parent/view.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const View = ({ attributes, blockProps, children }) => (
<div {...blockProps}>
<p className="title">{attributes.title}</p>
<h4 className="title">{attributes.title}</h4>
{children}
</div>
);
Expand Down
14 changes: 9 additions & 5 deletions src/context/counter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createContext } from '@wordpress/element';
import { createContext } from 'preact/compat';

if (typeof window.reactContext === 'undefined') {
window.reactContext = createContext(null);
if (typeof window.counterContext === 'undefined') {
window.counterContext = window.wp.element
? window.wp.element.createContext(null)
: createContext(null);

window.counterContext.displayName = 'CounterContext';
}
window.reactContext.displayName = 'CounterContext';
export default window.reactContext;

export default window.counterContext;
14 changes: 9 additions & 5 deletions src/context/theme.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createContext } from '@wordpress/element';
import { createContext } from 'preact/compat';

if (typeof window.themeReactContext === 'undefined') {
window.themeReactContext = createContext(null);
if (typeof window.themeContext === 'undefined') {
window.themeContext = window.wp.element
? window.wp.element.createContext('initial')
: createContext('initial');

window.themeContext.displayName = 'ThemeContext';
}
window.themeReactContext.displayName = 'ThemeContext';
export default window.themeReactContext;

export default window.themeContext;
14 changes: 14 additions & 0 deletions src/gutenberg-packages/hydration.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { hydrate, createElement } from 'preact/compat';
import { createGlobal } from './utils';
import toVdom from './to-vdom';
import visitor from './visitor';

const blockViews = createGlobal('blockViews', new Map());

const components = Object.fromEntries(
[...blockViews.entries()].map(([k, v]) => [k, v.Component])
);

visitor.map = components;

const dom = document.querySelector('.wp-site-blocks');
const vdom = toVdom(dom, visitor, createElement).props.children;

setTimeout(() => console.log('hydrated', hydrate(vdom, dom)), 3000);
41 changes: 41 additions & 0 deletions src/gutenberg-packages/to-vdom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export default function toVdom(node, visitor, h) {
walk.visitor = visitor;
walk.h = h;
return walk(node);
}

function walk(n) {
if (n.nodeType === 3) return n.data;
if (n.nodeType !== 1) return null;
let nodeName = String(n.nodeName).toLowerCase();

// Do not allow script tags (for now).
if (nodeName === 'script') return null;

let out = walk.h(
nodeName,
getProps(n.attributes),
walkChildren(n.childNodes)
);
if (walk.visitor) walk.visitor(out, n);

return out;
}

function getProps(attrs) {
let len = attrs && attrs.length;
if (!len) return null;
let props = {};
for (let i = 0; i < len; i++) {
let { name, value } = attrs[i];
props[name] = value;
}
return props;
}

function walkChildren(children) {
let c = children && Array.prototype.map.call(children, walk).filter(exists);
return c && c.length ? c : null;
}

let exists = (x) => x;
100 changes: 100 additions & 0 deletions src/gutenberg-packages/visitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { h } from "preact";
import { matcherFromSource } from './utils';

export default function visitor(vNode, domNode) {
const name = (vNode.type || '').toLowerCase();
const map = visitor.map;

if (name === 'wp-block' && map) {
processWpBlock({ vNode, domNode, map });
} else {
vNode.type = name.replace(/[^a-z0-9-]/i, '');
}
}

function processWpBlock({ vNode, domNode, map }) {
const blockType = vNode.props['data-wp-block-type'];
const Component = map[blockType];

if (!Component) return vNode;

const block = h(Component, {
attributes: getAttributes(vNode, domNode),
context: {},
blockProps: getBlockProps(vNode),
children: getChildren(vNode),
});

vNode.props = {
...vNode.props,
children: [block]
};
}

function getBlockProps(vNode) {
const { class: className, style } = JSON.parse(
vNode.props['data-wp-block-props']
);
return { className, style: getStyleProp(style) };
}

function getAttributes(vNode, domNode) {
// Get the block attributes.
const attributes = JSON.parse(
vNode.props['data-wp-block-attributes']
);

// Add the sourced attributes to the attributes object.
const sourcedAttributes = JSON.parse(
vNode.props['data-wp-block-sourced-attributes']
);
for (const attr in sourcedAttributes) {
attributes[attr] = matcherFromSource(sourcedAttributes[attr])(
domNode
);
}

return attributes;
}

function getChildren(vNode) {
return getChildrenFromWrapper(vNode.props.children) || vNode.props.children;
}

function getChildrenFromWrapper(children) {
if (!children?.length) return null;

for (const child of children) {
if (isChildrenWrapper(child)) return [child] || [];
}

// Try with the next nesting level.
return getChildrenFromWrapper(
[].concat(...children.map((child) => child?.props?.children || []))
);
}

function isChildrenWrapper(vNode) {
return vNode.type === 'wp-inner-blocks';
}

function toCamelCase(name) {
return name.replace(/-(.)/g, (match, letter) => letter.toUpperCase());
}

export function getStyleProp(cssText) {
if (!cssText) return {};

const el = document.createElement('div');
const { style } = el;
style.cssText = cssText;

const output = {};
for (let i = 0; i < style.length; i += 1) {
const key = style.item(0);
output[toCamelCase(key)] = style.getPropertyValue(key);
}

el.remove();
return output;
}
36 changes: 17 additions & 19 deletions src/gutenberg-packages/wordpress-element.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,40 @@
import {
createContext,
useContext as useReactContext,
useEffect as useReactEffect,
useState as useReactState,
} from '@wordpress/element';
useContext as usePreactContext,
useEffect as usePreactEffect,
useState as usePreactState,
} from 'preact/compat';

export const EnvContext = createContext(null);
export const EnvContext = createContext('view');

/**
* A React hook that returns the name of the environment.
*
* This is still a bit hacky. Ideally, Save components should support React
* hooks and all the environments (Edit, Save and View) should populate a
* normal context. Also, more environments could be added in the future.
* Based on the workaround used for the Island Hydration approach, but only to differentiate between
* Save and View, so this function and related hooks cannot be used inside Edit.
*
* Note that the other approach was a bit hacky; this is a bit more hacky.
*
* @returns {"edit" | "save" | "view"}
* @returns {"save" | "view"}
*/
export const useBlockEnvironment = () => {
try {
const env = useReactContext(EnvContext);
if (env === 'view') {
return 'view';
}
return 'edit';
// This will fail if the hook runs inside something that's not a Preact component.
return usePreactContext(EnvContext);
} catch (e) {
return 'save';
}
};

const noop = () => {};

export const useState = (init) =>
useBlockEnvironment() !== 'save' ? useReactState(init) : [init, noop];
export const useState = (init) =>
useBlockEnvironment() !== 'save' ? usePreactState(init) : [init, noop];

export const useEffect = (...args) =>
useBlockEnvironment() !== 'save' ? useReactEffect(...args) : noop;
useBlockEnvironment() !== 'save' ? usePreactEffect(...args) : noop;

export const useContext = (Context) =>
useBlockEnvironment() !== 'save'
? useReactContext(Context)
: Context._currentValue;
? usePreactContext(Context)
: Context._currentValue;
Loading

0 comments on commit 7f5d00e

Please sign in to comment.