-
Notifications
You must be signed in to change notification settings - Fork 46k
/
ReactDOMFizzInstructionSetShared.js
209 lines (191 loc) · 8.31 KB
/
ReactDOMFizzInstructionSetShared.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
/* eslint-disable dot-notation */
// Shared implementation and constants between the inline script and external
// runtime instruction sets.
export const COMMENT_NODE = 8;
export const SUSPENSE_START_DATA = '$';
export const SUSPENSE_END_DATA = '/$';
export const SUSPENSE_PENDING_START_DATA = '$?';
export const SUSPENSE_FALLBACK_START_DATA = '$!';
// TODO: Symbols that are referenced outside this module use dynamic accessor
// notation instead of dot notation to prevent Closure's advanced compilation
// mode from renaming. We could use extern files instead, but I couldn't get it
// working. Closure converts it to a dot access anyway, though, so it's not an
// urgent issue.
export function clientRenderBoundary(
suspenseBoundaryID,
errorDigest,
errorMsg,
errorStack,
errorComponentStack,
) {
// Find the fallback's first element.
const suspenseIdNode = document.getElementById(suspenseBoundaryID);
if (!suspenseIdNode) {
// The user must have already navigated away from this tree.
// E.g. because the parent was hydrated.
return;
}
// Find the boundary around the fallback. This is always the previous node.
const suspenseNode = suspenseIdNode.previousSibling;
// Tag it to be client rendered.
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
// assign error metadata to first sibling
const dataset = suspenseIdNode.dataset;
if (errorDigest) dataset['dgst'] = errorDigest;
if (errorMsg) dataset['msg'] = errorMsg;
if (errorStack) dataset['stck'] = errorStack;
if (errorComponentStack) dataset['cstck'] = errorComponentStack;
// Tell React to retry it if the parent already hydrated.
if (suspenseNode['_reactRetry']) {
suspenseNode['_reactRetry']();
}
}
export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
const contentNode = document.getElementById(contentID);
// We'll detach the content node so that regardless of what happens next we don't leave in the tree.
// This might also help by not causing recalcing each time we move a child from here to the target.
contentNode.parentNode.removeChild(contentNode);
// Find the fallback's first element.
const suspenseIdNode = document.getElementById(suspenseBoundaryID);
if (!suspenseIdNode) {
// The user must have already navigated away from this tree.
// E.g. because the parent was hydrated. That's fine there's nothing to do
// but we have to make sure that we already deleted the container node.
return;
}
// Find the boundary around the fallback. This is always the previous node.
const suspenseNode = suspenseIdNode.previousSibling;
if (!errorDigest) {
// Clear all the existing children. This is complicated because
// there can be embedded Suspense boundaries in the fallback.
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
const parentInstance = suspenseNode.parentNode;
let node = suspenseNode.nextSibling;
let depth = 0;
do {
if (node && node.nodeType === COMMENT_NODE) {
const data = node.data;
if (data === SUSPENSE_END_DATA) {
if (depth === 0) {
break;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA
) {
depth++;
}
}
const nextNode = node.nextSibling;
parentInstance.removeChild(node);
node = nextNode;
} while (node);
const endOfBoundary = node;
// Insert all the children from the contentNode between the start and end of suspense boundary.
while (contentNode.firstChild) {
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
}
suspenseNode.data = SUSPENSE_START_DATA;
} else {
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
suspenseIdNode.setAttribute('data-dgst', errorDigest);
}
if (suspenseNode['_reactRetry']) {
suspenseNode['_reactRetry']();
}
}
export function completeSegment(containerID, placeholderID) {
const segmentContainer = document.getElementById(containerID);
const placeholderNode = document.getElementById(placeholderID);
// We always expect both nodes to exist here because, while we might
// have navigated away from the main tree, we still expect the detached
// tree to exist.
segmentContainer.parentNode.removeChild(segmentContainer);
while (segmentContainer.firstChild) {
placeholderNode.parentNode.insertBefore(
segmentContainer.firstChild,
placeholderNode,
);
}
placeholderNode.parentNode.removeChild(placeholderNode);
}
// This is the exact URL string we expect that Fizz renders if we provide a function action.
// We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
// as a shared module for that reason.
const EXPECTED_FORM_ACTION_URL =
// eslint-disable-next-line no-script-url
"javascript:throw new Error('React form unexpectedly submitted.')";
export function listenToFormSubmissionsForReplaying() {
// A global replay queue ensures actions are replayed in order.
// This event listener should be above the React one. That way when
// we preventDefault in React's handling we also prevent this event
// from queing it. Since React listens to the root and the top most
// container you can use is the document, the window is fine.
// eslint-disable-next-line no-restricted-globals
addEventListener('submit', event => {
if (event.defaultPrevented) {
// We let earlier events to prevent the action from submitting.
return;
}
const form = event.target;
const submitter = event['submitter'];
let action = form.action;
let formDataSubmitter = submitter;
if (submitter) {
const submitterAction = submitter.getAttribute('formAction');
if (submitterAction != null) {
// The submitter overrides the action.
action = submitterAction;
// If the submitter overrides the action, and it passes the test below,
// that means that it was a function action which conceptually has no name.
// Therefore, we exclude the submitter from the formdata.
formDataSubmitter = null;
}
}
if (action !== EXPECTED_FORM_ACTION_URL) {
// The form is a regular form action, we can bail.
return;
}
// Prevent native navigation.
// This will also prevent other React's on the same page from listening.
event.preventDefault();
// Take a snapshot of the FormData at the time of the event.
let formData;
if (formDataSubmitter) {
// The submitter's value should be included in the FormData.
// It should be in the document order in the form.
// Since the FormData constructor invokes the formdata event it also
// needs to be available before that happens so after construction it's too
// late. We use a temporary fake node for the duration of this event.
// TODO: FormData takes a second argument that it's the submitter but this
// is fairly new so not all browsers support it yet. Switch to that technique
// when available.
const temp = document.createElement('input');
temp.name = formDataSubmitter.name;
temp.value = formDataSubmitter.value;
formDataSubmitter.parentNode.insertBefore(temp, formDataSubmitter);
formData = new FormData(form);
temp.parentNode.removeChild(temp);
} else {
formData = new FormData(form);
}
// Queue for replaying later. This field could potentially be shared with multiple
// Reacts on the same page since each one will preventDefault for the next one.
// This means that this protocol is shared with any React version that shares the same
// javascript: URL placeholder value. So we might not be the first to declare it.
// We attach it to the form's root node, which is the shared environment context
// where we preserve sequencing and where we'll pick it up from during hydration.
// If there's no ownerDocument, then this is the document.
const root = form.ownerDocument || form;
(root['$$reactFormReplay'] = root['$$reactFormReplay'] || []).push(
form,
submitter,
formData,
);
});
}