diff --git a/src/blocks/interactive-parent/edit.js b/src/blocks/interactive-parent/edit.js
index 49a07e4b..9460b7d4 100644
--- a/src/blocks/interactive-parent/edit.js
+++ b/src/blocks/interactive-parent/edit.js
@@ -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,
+}) => (
<>
-
setAttributes({ title: val })}
placeholder="This will be passed through context to child blocks"
- >
- {title}
-
-
+ />
+
diff --git a/src/blocks/interactive-parent/shared/button.js b/src/blocks/interactive-parent/shared/button.js
deleted file mode 100644
index 2384a54a..00000000
--- a/src/blocks/interactive-parent/shared/button.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const Button = ({ handler, children }) => {
- return
;
-};
-
-export default Button;
diff --git a/src/blocks/interactive-parent/shared/title.js b/src/blocks/interactive-parent/shared/title.js
deleted file mode 100644
index 2c554e4b..00000000
--- a/src/blocks/interactive-parent/shared/title.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const Title = ({ children, ...props }) => (
-
- {children}
-
-);
-
-export default Title;
diff --git a/src/blocks/interactive-parent/view.js b/src/blocks/interactive-parent/view.js
index 00e525dc..68b4d7f9 100644
--- a/src/blocks/interactive-parent/view.js
+++ b/src/blocks/interactive-parent/view.js
@@ -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: {
@@ -27,9 +24,9 @@ const View = ({
fontWeight: bold ? 900 : fontWeight,
}}
>
-
{title}
-
-
+
{title}
+
+
diff --git a/src/blocks/non-interactive-parent/edit.js b/src/blocks/non-interactive-parent/edit.js
index 6871ad27..566ae8f4 100644
--- a/src/blocks/non-interactive-parent/edit.js
+++ b/src/blocks/non-interactive-parent/edit.js
@@ -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 }) => (
@@ -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}
-
+ />
);
diff --git a/src/blocks/non-interactive-parent/view.js b/src/blocks/non-interactive-parent/view.js
index c381f3cb..2df201b5 100644
--- a/src/blocks/non-interactive-parent/view.js
+++ b/src/blocks/non-interactive-parent/view.js
@@ -1,6 +1,6 @@
const View = ({ attributes, blockProps, children }) => (
-
{attributes.title}
+
{attributes.title}
{children}
);
diff --git a/src/context/counter.js b/src/context/counter.js
index 6061f470..388f8aca 100644
--- a/src/context/counter.js
+++ b/src/context/counter.js
@@ -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;
diff --git a/src/context/theme.js b/src/context/theme.js
index 8aefc8ec..331c1942 100644
--- a/src/context/theme.js
+++ b/src/context/theme.js
@@ -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;
diff --git a/src/gutenberg-packages/hydration.js b/src/gutenberg-packages/hydration.js
index 0402b02f..b0f26a1d 100644
--- a/src/gutenberg-packages/hydration.js
+++ b/src/gutenberg-packages/hydration.js
@@ -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);
diff --git a/src/gutenberg-packages/to-vdom.js b/src/gutenberg-packages/to-vdom.js
new file mode 100644
index 00000000..abe86eb6
--- /dev/null
+++ b/src/gutenberg-packages/to-vdom.js
@@ -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;
diff --git a/src/gutenberg-packages/visitor.js b/src/gutenberg-packages/visitor.js
new file mode 100644
index 00000000..ebe737d9
--- /dev/null
+++ b/src/gutenberg-packages/visitor.js
@@ -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;
+}
\ No newline at end of file
diff --git a/src/gutenberg-packages/wordpress-element.js b/src/gutenberg-packages/wordpress-element.js
index 17135d9a..0201d3a2 100644
--- a/src/gutenberg-packages/wordpress-element.js
+++ b/src/gutenberg-packages/wordpress-element.js
@@ -1,28 +1,26 @@
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';
}
@@ -30,13 +28,13 @@ export const useBlockEnvironment = () => {
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;
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index f70789a4..2674dcc3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -4,15 +4,6 @@ module.exports = [
defaultConfig,
{
...defaultConfig,
- resolve: {
- alias: {
- '@wordpress/element': 'preact/compat',
- react: 'preact/compat',
- 'react-dom/test-utils': 'preact/test-utils',
- 'react-dom': 'preact/compat', // Must be below test-utils
- 'react/jsx-runtime': 'preact/jsx-runtime',
- },
- },
entry: {
'gutenberg-packages/hydration':
'./src/gutenberg-packages/hydration.js',
@@ -21,6 +12,20 @@ module.exports = [
'blocks/interactive-parent/register-view':
'./src/blocks/interactive-parent/register-view.js',
},
+ optimization: {
+ runtimeChunk: {
+ name: 'vendors',
+ },
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ },
+ },
+ },
module: {
rules: [
...defaultConfig.module.rules,