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
⚛️ Add directives to handle providesContext
and usesContext
#68
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ea7caf0
Expose `addHook` from `toVdom`
DAreRodz 258694f
Refactor `toVdom` and prepare it for directives
DAreRodz 1886ab2
Create directive for block components
DAreRodz f1baed8
Move directives to a folder
DAreRodz 35f54d3
Add wp-block-context directive
DAreRodz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { | ||
createContext, | ||
useContext, | ||
useMemo, | ||
createElement as h, | ||
} from 'preact/compat'; | ||
import { directive } from '../gutenberg-packages/directives'; | ||
|
||
// Single context reused by all providers. | ||
const blockContexts = createContext({}); | ||
blockContexts.displayName = 'blockContexts'; | ||
|
||
/** | ||
* Wrapper that provides the specified attributes. If there are ancestor that are also providers, it | ||
* merges all context together. | ||
* | ||
* @param {*} props Component props. | ||
* @param {*} props.provides Map of context names and block attributes. | ||
* @param {*} props.attributes Block attributes. | ||
* | ||
* @returns Block context provider. | ||
*/ | ||
const BlockContextProvider = ({ provides, attributes, children }) => { | ||
// Get previous context. | ||
const allContexts = useContext(blockContexts); | ||
|
||
// Get provided context from attributes. | ||
const context = {}; | ||
for (const key in provides) { | ||
context[key] = attributes[provides[key]]; | ||
} | ||
|
||
// Provide merged contexts. | ||
return ( | ||
<blockContexts.Provider value={{ ...allContexts, ...context }}> | ||
{children} | ||
</blockContexts.Provider> | ||
); | ||
}; | ||
|
||
/** | ||
* HOC that injects only the required attributes from `blockContexts` value. | ||
* | ||
* @param {*} Comp Component. | ||
* @param {*} options Options. | ||
* @param {*} options.uses Array of required contexts. | ||
* | ||
* @returns HOC function. | ||
*/ | ||
const withBlockContext = (Comp, { uses }) => { | ||
const hoc = (props) => { | ||
const allContexts = useContext(blockContexts); | ||
|
||
// Memoize only those attributes that are needed. | ||
const context = useMemo( | ||
() => | ||
uses.reduce((acc, attribute) => { | ||
acc[attribute] = allContexts[attribute]; | ||
return acc; | ||
}, {}), | ||
uses.map((attribute) => allContexts[attribute]) | ||
); | ||
|
||
// Inject context. | ||
return <Comp {...props} context={context} />; | ||
}; | ||
|
||
hoc.displayName = 'withBlockContext'; | ||
return hoc; | ||
}; | ||
|
||
directive('providesBlockContext', (props) => { | ||
const { providesBlockContext: provides, attributes } = props.wpBlock; | ||
const [block] = props.children; | ||
|
||
// The property `provides` can be null... | ||
if (!provides || !Object.keys(provides).length) return; | ||
|
||
block.props.children = h( | ||
BlockContextProvider, | ||
{ provides, attributes }, | ||
block.props.children | ||
); | ||
}); | ||
|
||
directive('usesBlockContext', (props) => { | ||
const { usesBlockContext: uses } = props.wpBlock; | ||
|
||
if (!uses.length) return; | ||
|
||
const [block] = props.children; | ||
block.type = withBlockContext(block.type, { uses }); | ||
}); |
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,25 @@ | ||
import { createElement as h } from 'preact/compat'; | ||
import { createGlobal } from '../gutenberg-packages/utils'; | ||
import { directive } from '../gutenberg-packages/directives'; | ||
|
||
const blockViews = createGlobal('blockViews', new Map()); | ||
|
||
// Handle block components. | ||
directive('type', (props) => { | ||
const { | ||
type, | ||
attributes, | ||
context = {}, | ||
props: blockProps, | ||
innerBlocks: children, | ||
} = props.wpBlock; | ||
|
||
// Do nothing if there's no component for this block. | ||
if (!blockViews.has(type)) return; | ||
|
||
const { Component } = blockViews.get(type); | ||
|
||
props.children = [ | ||
h(Component, { context, attributes, blockProps, children }), | ||
]; | ||
}); |
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,36 @@ | ||
import { h, options } from 'preact'; | ||
|
||
// WordPress Directives. | ||
const directives = {}; | ||
|
||
// Expose function to add directives. | ||
export const directive = (name, cb) => { | ||
directives[name] = cb; | ||
}; | ||
|
||
const WpDirective = (props) => { | ||
for (const d in props.wpBlock) { | ||
directives[d]?.(props); | ||
} | ||
props._wrapped = true; | ||
const { wp, tag, children, ...rest } = props; | ||
return h(tag, rest, children); | ||
}; | ||
|
||
const old = options.vnode; | ||
|
||
options.vnode = (vnode) => { | ||
const wpBlock = vnode.props.wpBlock; | ||
const wrapped = vnode.props._wrapped; | ||
|
||
if (wpBlock) { | ||
if (!wrapped) { | ||
vnode.props.tag = vnode.type; | ||
vnode.type = WpDirective; | ||
} | ||
} else if (wrapped) { | ||
delete vnode.props._wrapped; | ||
} | ||
|
||
if (old) old(vnode); | ||
}; |
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,17 +1,10 @@ | ||
import { hydrate, createElement } from 'preact/compat'; | ||
import { createGlobal } from './utils'; | ||
import 'preact/debug'; | ||
import { hydrate } from 'preact/compat'; | ||
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; | ||
import '../directives/wp-block'; | ||
import '../directives/wp-block-context'; | ||
|
||
const dom = document.querySelector('.wp-site-blocks'); | ||
const vdom = toVdom(dom, visitor, createElement).props.children; | ||
const vdom = toVdom(dom).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 |
---|---|---|
@@ -1,41 +1,113 @@ | ||
export default function toVdom(node, visitor, h) { | ||
walk.visitor = visitor; | ||
walk.h = h; | ||
return walk(node); | ||
} | ||
import { h } from 'preact'; | ||
import { matcherFromSource } from './utils'; | ||
|
||
// Prefix used by WP directives. | ||
const prefix = 'data-wp-block-'; | ||
|
||
function walk(n) { | ||
// Reference to the last <inner-blocks> wrapper found. | ||
let innerBlocksFound = null; | ||
|
||
// Recursive function that transfoms a DOM tree into vDOM. | ||
export default function toVdom(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; | ||
// Get the node type. | ||
const type = String(n.nodeName).toLowerCase(); | ||
|
||
let out = walk.h( | ||
nodeName, | ||
getProps(n.attributes), | ||
walkChildren(n.childNodes) | ||
); | ||
if (walk.visitor) walk.visitor(out, n); | ||
if (type === 'script') return null; | ||
|
||
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]; | ||
// Extract props from node attributes. | ||
const props = {}; | ||
const wpBlock = {}; | ||
for (const { name, value } of n.attributes) { | ||
// Store block directives in `wpBlock`. | ||
if (name.startsWith(prefix)) { | ||
const propName = getWpBlockPropName(name); | ||
try { | ||
wpBlock[propName] = JSON.parse(value); | ||
} catch (e) { | ||
wpBlock[propName] = value; | ||
} | ||
} | ||
// Add the original property, and the rest of them. | ||
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; | ||
// Include wpBlock prop if needed. | ||
if (Object.keys(wpBlock).length) { | ||
props.wpBlock = wpBlock; | ||
|
||
// Handle special cases with wpBlock props. | ||
handleSourcedAttributes(props, n); | ||
handleBlockProps(props); | ||
} | ||
|
||
// Walk child nodes and return vDOM children. | ||
const children = [].map.call(n.childNodes, toVdom).filter(exists); | ||
|
||
// Add inner blocks. | ||
if (type === 'wp-block' && innerBlocksFound) { | ||
wpBlock.innerBlocks = innerBlocksFound; | ||
innerBlocksFound = null; | ||
|
||
// Set wpBlock prop again, just in case it's missing. | ||
props.wpBlock = wpBlock; | ||
} | ||
|
||
// Create vNode. Note that all `wpBlock` props should exist now to make directives work. | ||
const vNode = h(type, props, children); | ||
|
||
// Save a renference to this vNode if it's an <inner-blocks>` wrapper. | ||
if (type === 'wp-inner-blocks') { | ||
innerBlocksFound = vNode; | ||
} | ||
|
||
return vNode; | ||
} | ||
|
||
let exists = (x) => x; | ||
const getWpBlockPropName = (name) => toCamelCase(name.replace(prefix, '')); | ||
|
||
// Get sourced attributes and place them in `attributes`. | ||
const handleSourcedAttributes = ({ wpBlock }, domNode) => { | ||
if (wpBlock && wpBlock.sourcedAttributes) { | ||
const { sourcedAttributes, attributes = {} } = wpBlock; | ||
for (const attr in sourcedAttributes) { | ||
attributes[attr] = matcherFromSource(sourcedAttributes[attr])( | ||
domNode | ||
); | ||
} | ||
wpBlock.attributes = attributes; | ||
} | ||
}; | ||
|
||
// Adapt block props to React/Preact format. | ||
const handleBlockProps = ({ wpBlock }) => { | ||
if (!wpBlock.props) return; | ||
|
||
const { class: className, style } = wpBlock.props; | ||
wpBlock.props = { className, style: cssObject(style) }; | ||
}; | ||
|
||
// Return an object of camelCased CSS properties. | ||
const cssObject = (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; | ||
}; | ||
|
||
const exists = (x) => x; | ||
|
||
const toCamelCase = (str) => | ||
str.replace(/-(.)/g, (_, initial) => initial.toUpperCase()); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I've assumed that
options.vnode
would run every timeh()
is used. Maybe I'm wrong here. 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're not. That's the way it works.