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 10
/
hydration.js
216 lines (191 loc) · 6.27 KB
/
hydration.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import { Consumer, createProvider } from './react-context';
import { createGlobal, matcherFromSource } from './utils';
import { EnvContext, hydrate } from './wordpress-element';
import { unmountComponentAtNode } from 'react-dom';
const blockViews = createGlobal('blockViews', new Map());
const elementsToHydrate = createGlobal('elementsToHydrate', new Map());
const Children = ({ value }) => (
<wp-inner-blocks
suppressHydrationWarning={true}
dangerouslySetInnerHTML={{ __html: value }}
/>
);
Children.shouldComponentUpdate = () => false;
const Wrappers = ({ wrappers, children }) => {
let result = children;
wrappers.forEach((wrapper) => {
result = wrapper({ children: result });
});
return result;
};
class WpBlock extends HTMLElement {
#blockType = null;
#hydration = false;
#attributes = {};
#blockContext = {};
hydrate() {
const Providers = [];
// Get the block type, block props (class and style), inner blocks,
// frontend component and options.
const innerBlocks = this.querySelector('wp-inner-blocks');
const { Component, options } = blockViews.get(this.#blockType);
const { class: className, style } = JSON.parse(
this.getAttribute('data-wp-block-props')
);
// Temporary element to translate style strings to style objects.
const el = document.createElement('div');
el.style.cssText = style;
const blockProps = { className, style: el.style };
el.remove();
// Get the React Context from their parents.
options?.usesContext?.forEach((context) => {
const event = new CustomEvent('wp-react-context', {
detail: { context },
bubbles: true,
cancelable: true,
});
this.dispatchEvent(event);
if (typeof event.detail.Provider === 'function') {
Providers.push(event.detail.Provider);
}
});
// Share the React Context with their children.
if (options?.providesContext?.length > 0) {
this.addEventListener('wp-react-context', (event) => {
for (const context of options.providesContext) {
// We compare the provided context with the received context.
if (event.detail.context === context) {
// If there's a match, we stop propagation.
event.stopPropagation();
// We return a Provider that is subscribed to the parent Provider.
event.detail.Provider = createProvider({
element: this,
context,
});
// We can stop the iteration.
break;
}
}
});
}
const media = this.getAttribute('data-wp-block-hydration-media');
hydrate(
<EnvContext.Provider value="view">
{/* Wrap the component with all the React Providers */}
<Wrappers wrappers={Providers}>
<Component
attributes={this.#attributes}
blockProps={blockProps}
context={this.#blockContext}
>
{/* Update the value each time one of the React Contexts changes */}
{options?.providesContext?.map((context, index) => (
<Consumer
key={index}
element={this}
context={context}
/>
))}
{/* Render the inner blocks */}
{innerBlocks && (
<Children
value={innerBlocks.innerHTML}
suppressHydrationWarning={true}
/>
)}
</Component>
</Wrappers>
<template
className="wp-inner-blocks"
suppressHydrationWarning={true}
/>
</EnvContext.Provider>,
this,
{ technique: this.#hydration, media }
);
}
connectedCallback() {
this.#blockType = this.getAttribute('data-wp-block-type');
// When connectedCallback is triggered, the children nodes have not been
// created yet, so we need a setTimeout to be able to access the sourced
// attributes and the inner blocks.
setTimeout(() => {
// Get the block attributes.
this.#attributes = JSON.parse(
this.getAttribute('data-wp-block-attributes')
);
// Add the sourced attributes to the attributes object.
const sourcedAttributes = JSON.parse(
this.getAttribute('data-wp-block-sourced-attributes')
);
for (const attr in sourcedAttributes) {
this.#attributes[attr] = matcherFromSource(
sourcedAttributes[attr]
)(this);
}
// Get the Block Context from their parents.
const usesBlockContext = JSON.parse(
this.getAttribute('data-wp-block-uses-block-context')
);
if (usesBlockContext) {
const event = new CustomEvent('wp-block-context', {
detail: { context: {} },
bubbles: true,
cancelable: true,
});
this.dispatchEvent(event);
// Select only the parts of the context that the block declared in the
// `usesContext` of its block.json.
usesBlockContext.forEach(
(key) =>
(this.#blockContext[key] = event.detail.context[key])
);
}
// Share the Block Context with their children.
const providesBlockContext = JSON.parse(
this.getAttribute('data-wp-block-provides-block-context')
);
if (providesBlockContext) {
this.addEventListener('wp-block-context', (event) => {
// Select only the parts of the context that the block declared in
// the `providesContext` of its block.json.
Object.entries(providesBlockContext).forEach(
([key, attribute]) => {
if (!event.detail.context[key]) {
event.detail.context[key] =
this.#attributes[attribute];
}
}
);
});
}
// Hydrate the interactive blocks.
this.#hydration = this.getAttribute('data-wp-block-hydration');
if (this.#hydration) {
// Check if a View has been registered for this block type before. If
// not, we add it to the list of elements that need hydration so when
// the View finally comes, the hydration happens.
if (!blockViews.has(this.#blockType)) {
if (!elementsToHydrate.has(this.#blockType)) {
elementsToHydrate.set(this.#blockType, [this]);
} else {
elementsToHydrate.get(this.#blockType).push(this);
}
return;
}
this.hydrate();
}
});
}
disconnectedCallback() {
// Unmount the React component, running callbacks and cleaning up its state.
unmountComponentAtNode(this);
}
}
// 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.
// This should not happen in the Gutenberg version because this file should only
// be enqueued/bundled once.
if (customElements.get('wp-block') === undefined) {
customElements.define('wp-block', WpBlock);
}