183 lines
No EOL
8.3 KiB
JavaScript
183 lines
No EOL
8.3 KiB
JavaScript
/* 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 = suspenseBoundaryID |> document.getElementById(%);
|
|
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 = contentID |> document.getElementById(%);
|
|
// 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.
|
|
// Find the fallback's first element.
|
|
contentNode |> contentNode.parentNode.removeChild(%);
|
|
const suspenseIdNode = suspenseBoundaryID |> document.getElementById(%);
|
|
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;
|
|
node |> parentInstance.removeChild(%);
|
|
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) {
|
|
contentNode.firstChild |> parentInstance.insertBefore(%, endOfBoundary);
|
|
}
|
|
suspenseNode.data = SUSPENSE_START_DATA;
|
|
} else {
|
|
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
|
|
'data-dgst' |> suspenseIdNode.setAttribute(%, errorDigest);
|
|
}
|
|
if (suspenseNode['_reactRetry']) {
|
|
suspenseNode['_reactRetry']();
|
|
}
|
|
}
|
|
export function completeSegment(containerID, placeholderID) {
|
|
const segmentContainer = containerID |> document.getElementById(%);
|
|
const placeholderNode = placeholderID |> document.getElementById(%);
|
|
// 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 |> segmentContainer.parentNode.removeChild(%);
|
|
while (segmentContainer.firstChild) {
|
|
segmentContainer.firstChild |> placeholderNode.parentNode.insertBefore(%, placeholderNode);
|
|
}
|
|
placeholderNode |> placeholderNode.parentNode.removeChild(%);
|
|
}
|
|
|
|
// 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
|
|
'submit' |> addEventListener(%, 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 = 'formAction' |> submitter.getAttribute(%);
|
|
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 = 'input' |> document.createElement(%);
|
|
temp.name = formDataSubmitter.name;
|
|
temp.value = formDataSubmitter.value;
|
|
temp |> formDataSubmitter.parentNode.insertBefore(%, formDataSubmitter);
|
|
formData = new FormData(form);
|
|
temp |> temp.parentNode.removeChild(%);
|
|
} 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);
|
|
});
|
|
} |