This repository has been archived by the owner on Jul 28, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #66 from WordPress/full-vdom/hydrate-vdom-from-dom
Hydrate the blocks with full vDOM (using `wp-block` and `wp-inner-blocks`)
- Loading branch information
Showing
15 changed files
with
238 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.