mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
1202 lines
40 KiB
JavaScript
1202 lines
40 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
// TODO: direct imports like some-package/src/* are bad. Fix me.
|
|
import ReactDebugCurrentFiber from 'react-reconciler/src/ReactDebugCurrentFiber';
|
|
import {registrationNameModules} from 'events/EventPluginRegistry';
|
|
import emptyFunction from 'fbjs/lib/emptyFunction';
|
|
import warning from 'fbjs/lib/warning';
|
|
|
|
import * as DOMPropertyOperations from './DOMPropertyOperations';
|
|
import * as ReactDOMFiberInput from './ReactDOMFiberInput';
|
|
import * as ReactDOMFiberOption from './ReactDOMFiberOption';
|
|
import * as ReactDOMFiberSelect from './ReactDOMFiberSelect';
|
|
import * as ReactDOMFiberTextarea from './ReactDOMFiberTextarea';
|
|
import * as inputValueTracking from './inputValueTracking';
|
|
import setInnerHTML from './setInnerHTML';
|
|
import setTextContent from './setTextContent';
|
|
import {listenTo, trapBubbledEvent} from '../events/ReactBrowserEventEmitter';
|
|
import {mediaEventTypes} from '../events/BrowserEventConstants';
|
|
import * as CSSPropertyOperations from '../shared/CSSPropertyOperations';
|
|
import {Namespaces, getIntrinsicNamespace} from '../shared/DOMNamespaces';
|
|
import {
|
|
getPropertyInfo,
|
|
shouldIgnoreAttribute,
|
|
shouldRemoveAttribute,
|
|
} from '../shared/DOMProperty';
|
|
import assertValidProps from '../shared/assertValidProps';
|
|
import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType';
|
|
import isCustomComponent from '../shared/isCustomComponent';
|
|
import possibleStandardNames from '../shared/possibleStandardNames';
|
|
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
|
|
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
|
|
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
|
|
|
|
const {
|
|
getCurrentFiberOwnerName,
|
|
getCurrentFiberStackAddendum,
|
|
} = ReactDebugCurrentFiber;
|
|
let didWarnInvalidHydration = false;
|
|
let didWarnShadyDOM = false;
|
|
|
|
const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
|
|
const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
|
|
const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
|
|
const AUTOFOCUS = 'autoFocus';
|
|
const CHILDREN = 'children';
|
|
const STYLE = 'style';
|
|
const HTML = '__html';
|
|
|
|
const {html: HTML_NAMESPACE} = Namespaces;
|
|
|
|
let getStack = emptyFunction.thatReturns('');
|
|
|
|
let warnedUnknownTags;
|
|
let suppressHydrationWarning;
|
|
|
|
let validatePropertiesInDevelopment;
|
|
let warnForTextDifference;
|
|
let warnForPropDifference;
|
|
let warnForExtraAttributes;
|
|
let warnForInvalidEventListener;
|
|
|
|
let normalizeMarkupForTextOrAttribute;
|
|
let normalizeHTML;
|
|
|
|
if (__DEV__) {
|
|
getStack = getCurrentFiberStackAddendum;
|
|
|
|
warnedUnknownTags = {
|
|
// Chrome is the only major browser not shipping <time>. But as of July
|
|
// 2017 it intends to ship it due to widespread usage. We intentionally
|
|
// *don't* warn for <time> even if it's unrecognized by Chrome because
|
|
// it soon will be, and many apps have been using it anyway.
|
|
time: true,
|
|
// There are working polyfills for <dialog>. Let people use it.
|
|
dialog: true,
|
|
};
|
|
|
|
validatePropertiesInDevelopment = function(type, props) {
|
|
validateARIAProperties(type, props);
|
|
validateInputProperties(type, props);
|
|
validateUnknownProperties(type, props, /* canUseEventSystem */ true);
|
|
};
|
|
|
|
// HTML parsing normalizes CR and CRLF to LF.
|
|
// It also can turn \u0000 into \uFFFD inside attributes.
|
|
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
|
|
// If we have a mismatch, it might be caused by that.
|
|
// We will still patch up in this case but not fire the warning.
|
|
const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
|
|
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
|
|
|
|
normalizeMarkupForTextOrAttribute = function(markup: mixed): string {
|
|
const markupString =
|
|
typeof markup === 'string' ? markup : '' + (markup: any);
|
|
return markupString
|
|
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
|
|
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
|
|
};
|
|
|
|
warnForTextDifference = function(
|
|
serverText: string,
|
|
clientText: string | number,
|
|
) {
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
|
|
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
|
|
if (normalizedServerText === normalizedClientText) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
warning(
|
|
false,
|
|
'Text content did not match. Server: "%s" Client: "%s"',
|
|
normalizedServerText,
|
|
normalizedClientText,
|
|
);
|
|
};
|
|
|
|
warnForPropDifference = function(
|
|
propName: string,
|
|
serverValue: mixed,
|
|
clientValue: mixed,
|
|
) {
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
const normalizedClientValue = normalizeMarkupForTextOrAttribute(
|
|
clientValue,
|
|
);
|
|
const normalizedServerValue = normalizeMarkupForTextOrAttribute(
|
|
serverValue,
|
|
);
|
|
if (normalizedServerValue === normalizedClientValue) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
warning(
|
|
false,
|
|
'Prop `%s` did not match. Server: %s Client: %s',
|
|
propName,
|
|
JSON.stringify(normalizedServerValue),
|
|
JSON.stringify(normalizedClientValue),
|
|
);
|
|
};
|
|
|
|
warnForExtraAttributes = function(attributeNames: Set<string>) {
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
const names = [];
|
|
attributeNames.forEach(function(name) {
|
|
names.push(name);
|
|
});
|
|
warning(false, 'Extra attributes from the server: %s', names);
|
|
};
|
|
|
|
warnForInvalidEventListener = function(registrationName, listener) {
|
|
if (listener === false) {
|
|
warning(
|
|
false,
|
|
'Expected `%s` listener to be a function, instead got `false`.\n\n' +
|
|
'If you used to conditionally omit it with %s={condition && value}, ' +
|
|
'pass %s={condition ? value : undefined} instead.%s',
|
|
registrationName,
|
|
registrationName,
|
|
registrationName,
|
|
getCurrentFiberStackAddendum(),
|
|
);
|
|
} else {
|
|
warning(
|
|
false,
|
|
'Expected `%s` listener to be a function, instead got a value of `%s` type.%s',
|
|
registrationName,
|
|
typeof listener,
|
|
getCurrentFiberStackAddendum(),
|
|
);
|
|
}
|
|
};
|
|
|
|
// Parse the HTML and read it back to normalize the HTML string so that it
|
|
// can be used for comparison.
|
|
normalizeHTML = function(parent: Element, html: string) {
|
|
// We could have created a separate document here to avoid
|
|
// re-initializing custom elements if they exist. But this breaks
|
|
// how <noscript> is being handled. So we use the same document.
|
|
// See the discussion in https://github.com/facebook/react/pull/11157.
|
|
const testElement =
|
|
parent.namespaceURI === HTML_NAMESPACE
|
|
? parent.ownerDocument.createElement(parent.tagName)
|
|
: parent.ownerDocument.createElementNS(
|
|
(parent.namespaceURI: any),
|
|
parent.tagName,
|
|
);
|
|
testElement.innerHTML = html;
|
|
return testElement.innerHTML;
|
|
};
|
|
}
|
|
|
|
function ensureListeningTo(rootContainerElement, registrationName) {
|
|
const isDocumentOrFragment =
|
|
rootContainerElement.nodeType === DOCUMENT_NODE ||
|
|
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
|
|
const doc = isDocumentOrFragment
|
|
? rootContainerElement
|
|
: rootContainerElement.ownerDocument;
|
|
listenTo(registrationName, doc);
|
|
}
|
|
|
|
function getOwnerDocumentFromRootContainer(
|
|
rootContainerElement: Element | Document,
|
|
): Document {
|
|
return rootContainerElement.nodeType === DOCUMENT_NODE
|
|
? (rootContainerElement: any)
|
|
: rootContainerElement.ownerDocument;
|
|
}
|
|
|
|
function trapClickOnNonInteractiveElement(node: HTMLElement) {
|
|
// Mobile Safari does not fire properly bubble click events on
|
|
// non-interactive elements, which means delegated click listeners do not
|
|
// fire. The workaround for this bug involves attaching an empty click
|
|
// listener on the target node.
|
|
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
|
// Just set it using the onclick property so that we don't have to manage any
|
|
// bookkeeping for it. Not sure if we need to clear it when the listener is
|
|
// removed.
|
|
// TODO: Only do this for the relevant Safaris maybe?
|
|
node.onclick = emptyFunction;
|
|
}
|
|
|
|
function setInitialDOMProperties(
|
|
tag: string,
|
|
domElement: Element,
|
|
rootContainerElement: Element | Document,
|
|
nextProps: Object,
|
|
isCustomComponentTag: boolean,
|
|
): void {
|
|
for (const propKey in nextProps) {
|
|
if (!nextProps.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const nextProp = nextProps[propKey];
|
|
if (propKey === STYLE) {
|
|
if (__DEV__) {
|
|
if (nextProp) {
|
|
// Freeze the next style object so that we can assume it won't be
|
|
// mutated. We have already warned for this in the past.
|
|
Object.freeze(nextProp);
|
|
}
|
|
}
|
|
// Relies on `updateStylesByID` not mutating `styleUpdates`.
|
|
CSSPropertyOperations.setValueForStyles(domElement, nextProp, getStack);
|
|
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
|
const nextHtml = nextProp ? nextProp[HTML] : undefined;
|
|
if (nextHtml != null) {
|
|
setInnerHTML(domElement, nextHtml);
|
|
}
|
|
} else if (propKey === CHILDREN) {
|
|
if (typeof nextProp === 'string') {
|
|
// Avoid setting initial textContent when the text is empty. In IE11 setting
|
|
// textContent on a <textarea> will cause the placeholder to not
|
|
// show within the <textarea> until it has been focused and blurred again.
|
|
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
|
|
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
|
|
if (canSetTextContent) {
|
|
setTextContent(domElement, nextProp);
|
|
}
|
|
} else if (typeof nextProp === 'number') {
|
|
setTextContent(domElement, '' + nextProp);
|
|
}
|
|
} else if (
|
|
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
|
|
propKey === SUPPRESS_HYDRATION_WARNING
|
|
) {
|
|
// Noop
|
|
} else if (propKey === AUTOFOCUS) {
|
|
// We polyfill it separately on the client during commit.
|
|
// We blacklist it here rather than in the property list because we emit it in SSR.
|
|
} else if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
if (nextProp != null) {
|
|
if (__DEV__ && typeof nextProp !== 'function') {
|
|
warnForInvalidEventListener(propKey, nextProp);
|
|
}
|
|
ensureListeningTo(rootContainerElement, propKey);
|
|
}
|
|
} else if (nextProp != null) {
|
|
DOMPropertyOperations.setValueForProperty(
|
|
domElement,
|
|
propKey,
|
|
nextProp,
|
|
isCustomComponentTag,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateDOMProperties(
|
|
domElement: Element,
|
|
updatePayload: Array<any>,
|
|
wasCustomComponentTag: boolean,
|
|
isCustomComponentTag: boolean,
|
|
): void {
|
|
// TODO: Handle wasCustomComponentTag
|
|
for (let i = 0; i < updatePayload.length; i += 2) {
|
|
const propKey = updatePayload[i];
|
|
const propValue = updatePayload[i + 1];
|
|
if (propKey === STYLE) {
|
|
CSSPropertyOperations.setValueForStyles(domElement, propValue, getStack);
|
|
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
|
setInnerHTML(domElement, propValue);
|
|
} else if (propKey === CHILDREN) {
|
|
setTextContent(domElement, propValue);
|
|
} else {
|
|
DOMPropertyOperations.setValueForProperty(
|
|
domElement,
|
|
propKey,
|
|
propValue,
|
|
isCustomComponentTag,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createElement(
|
|
type: string,
|
|
props: Object,
|
|
rootContainerElement: Element | Document,
|
|
parentNamespace: string,
|
|
): Element {
|
|
let isCustomComponentTag;
|
|
|
|
// We create tags in the namespace of their parent container, except HTML
|
|
// tags get no namespace.
|
|
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
|
|
rootContainerElement,
|
|
);
|
|
let domElement: Element;
|
|
let namespaceURI = parentNamespace;
|
|
if (namespaceURI === HTML_NAMESPACE) {
|
|
namespaceURI = getIntrinsicNamespace(type);
|
|
}
|
|
if (namespaceURI === HTML_NAMESPACE) {
|
|
if (__DEV__) {
|
|
isCustomComponentTag = isCustomComponent(type, props);
|
|
// Should this check be gated by parent namespace? Not sure we want to
|
|
// allow <SVG> or <mATH>.
|
|
warning(
|
|
isCustomComponentTag || type === type.toLowerCase(),
|
|
'<%s /> is using uppercase HTML. Always use lowercase HTML tags ' +
|
|
'in React.',
|
|
type,
|
|
);
|
|
}
|
|
|
|
if (type === 'script') {
|
|
// Create the script via .innerHTML so its "parser-inserted" flag is
|
|
// set to true and it does not execute
|
|
const div = ownerDocument.createElement('div');
|
|
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
|
|
// This is guaranteed to yield a script element.
|
|
const firstChild = ((div.firstChild: any): HTMLScriptElement);
|
|
domElement = div.removeChild(firstChild);
|
|
} else if (typeof props.is === 'string') {
|
|
// $FlowIssue `createElement` should be updated for Web Components
|
|
domElement = ownerDocument.createElement(type, {is: props.is});
|
|
} else {
|
|
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
|
|
// See discussion in https://github.com/facebook/react/pull/6896
|
|
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
|
|
domElement = ownerDocument.createElement(type);
|
|
}
|
|
} else {
|
|
domElement = ownerDocument.createElementNS(namespaceURI, type);
|
|
}
|
|
|
|
if (__DEV__) {
|
|
if (namespaceURI === HTML_NAMESPACE) {
|
|
if (
|
|
!isCustomComponentTag &&
|
|
Object.prototype.toString.call(domElement) ===
|
|
'[object HTMLUnknownElement]' &&
|
|
!Object.prototype.hasOwnProperty.call(warnedUnknownTags, type)
|
|
) {
|
|
warnedUnknownTags[type] = true;
|
|
warning(
|
|
false,
|
|
'The tag <%s> is unrecognized in this browser. ' +
|
|
'If you meant to render a React component, start its name with ' +
|
|
'an uppercase letter.',
|
|
type,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return domElement;
|
|
}
|
|
|
|
export function createTextNode(
|
|
text: string,
|
|
rootContainerElement: Element | Document,
|
|
): Text {
|
|
return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(
|
|
text,
|
|
);
|
|
}
|
|
|
|
export function setInitialProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
rawProps: Object,
|
|
rootContainerElement: Element | Document,
|
|
): void {
|
|
const isCustomComponentTag = isCustomComponent(tag, rawProps);
|
|
if (__DEV__) {
|
|
validatePropertiesInDevelopment(tag, rawProps);
|
|
if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {
|
|
warning(
|
|
false,
|
|
'%s is using shady DOM. Using shady DOM with React can ' +
|
|
'cause things to break subtly.',
|
|
getCurrentFiberOwnerName() || 'A component',
|
|
);
|
|
didWarnShadyDOM = true;
|
|
}
|
|
}
|
|
|
|
// TODO: Make sure that we check isMounted before firing any of these events.
|
|
let props: Object;
|
|
switch (tag) {
|
|
case 'iframe':
|
|
case 'object':
|
|
trapBubbledEvent('topLoad', 'load', domElement);
|
|
props = rawProps;
|
|
break;
|
|
case 'video':
|
|
case 'audio':
|
|
// Create listener for each media event
|
|
for (const event in mediaEventTypes) {
|
|
if (mediaEventTypes.hasOwnProperty(event)) {
|
|
trapBubbledEvent(event, mediaEventTypes[event], domElement);
|
|
}
|
|
}
|
|
props = rawProps;
|
|
break;
|
|
case 'source':
|
|
trapBubbledEvent('topError', 'error', domElement);
|
|
props = rawProps;
|
|
break;
|
|
case 'img':
|
|
case 'image':
|
|
case 'link':
|
|
trapBubbledEvent('topError', 'error', domElement);
|
|
trapBubbledEvent('topLoad', 'load', domElement);
|
|
props = rawProps;
|
|
break;
|
|
case 'form':
|
|
trapBubbledEvent('topReset', 'reset', domElement);
|
|
trapBubbledEvent('topSubmit', 'submit', domElement);
|
|
props = rawProps;
|
|
break;
|
|
case 'details':
|
|
trapBubbledEvent('topToggle', 'toggle', domElement);
|
|
props = rawProps;
|
|
break;
|
|
case 'input':
|
|
ReactDOMFiberInput.initWrapperState(domElement, rawProps);
|
|
props = ReactDOMFiberInput.getHostProps(domElement, rawProps);
|
|
trapBubbledEvent('topInvalid', 'invalid', domElement);
|
|
// For controlled components we always need to ensure we're listening
|
|
// to onChange. Even if there is no listener.
|
|
ensureListeningTo(rootContainerElement, 'onChange');
|
|
break;
|
|
case 'option':
|
|
ReactDOMFiberOption.validateProps(domElement, rawProps);
|
|
props = ReactDOMFiberOption.getHostProps(domElement, rawProps);
|
|
break;
|
|
case 'select':
|
|
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
|
|
props = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
|
|
trapBubbledEvent('topInvalid', 'invalid', domElement);
|
|
// For controlled components we always need to ensure we're listening
|
|
// to onChange. Even if there is no listener.
|
|
ensureListeningTo(rootContainerElement, 'onChange');
|
|
break;
|
|
case 'textarea':
|
|
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
|
|
props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps);
|
|
trapBubbledEvent('topInvalid', 'invalid', domElement);
|
|
// For controlled components we always need to ensure we're listening
|
|
// to onChange. Even if there is no listener.
|
|
ensureListeningTo(rootContainerElement, 'onChange');
|
|
break;
|
|
default:
|
|
props = rawProps;
|
|
}
|
|
|
|
assertValidProps(tag, props, getStack);
|
|
|
|
setInitialDOMProperties(
|
|
tag,
|
|
domElement,
|
|
rootContainerElement,
|
|
props,
|
|
isCustomComponentTag,
|
|
);
|
|
|
|
switch (tag) {
|
|
case 'input':
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
inputValueTracking.track((domElement: any));
|
|
ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
|
|
break;
|
|
case 'textarea':
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
inputValueTracking.track((domElement: any));
|
|
ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
|
|
break;
|
|
case 'option':
|
|
ReactDOMFiberOption.postMountWrapper(domElement, rawProps);
|
|
break;
|
|
case 'select':
|
|
ReactDOMFiberSelect.postMountWrapper(domElement, rawProps);
|
|
break;
|
|
default:
|
|
if (typeof props.onClick === 'function') {
|
|
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
|
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate the diff between the two objects.
|
|
export function diffProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
lastRawProps: Object,
|
|
nextRawProps: Object,
|
|
rootContainerElement: Element | Document,
|
|
): null | Array<mixed> {
|
|
if (__DEV__) {
|
|
validatePropertiesInDevelopment(tag, nextRawProps);
|
|
}
|
|
|
|
let updatePayload: null | Array<any> = null;
|
|
|
|
let lastProps: Object;
|
|
let nextProps: Object;
|
|
switch (tag) {
|
|
case 'input':
|
|
lastProps = ReactDOMFiberInput.getHostProps(domElement, lastRawProps);
|
|
nextProps = ReactDOMFiberInput.getHostProps(domElement, nextRawProps);
|
|
updatePayload = [];
|
|
break;
|
|
case 'option':
|
|
lastProps = ReactDOMFiberOption.getHostProps(domElement, lastRawProps);
|
|
nextProps = ReactDOMFiberOption.getHostProps(domElement, nextRawProps);
|
|
updatePayload = [];
|
|
break;
|
|
case 'select':
|
|
lastProps = ReactDOMFiberSelect.getHostProps(domElement, lastRawProps);
|
|
nextProps = ReactDOMFiberSelect.getHostProps(domElement, nextRawProps);
|
|
updatePayload = [];
|
|
break;
|
|
case 'textarea':
|
|
lastProps = ReactDOMFiberTextarea.getHostProps(domElement, lastRawProps);
|
|
nextProps = ReactDOMFiberTextarea.getHostProps(domElement, nextRawProps);
|
|
updatePayload = [];
|
|
break;
|
|
default:
|
|
lastProps = lastRawProps;
|
|
nextProps = nextRawProps;
|
|
if (
|
|
typeof lastProps.onClick !== 'function' &&
|
|
typeof nextProps.onClick === 'function'
|
|
) {
|
|
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
|
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
|
}
|
|
break;
|
|
}
|
|
|
|
assertValidProps(tag, nextProps, getStack);
|
|
|
|
let propKey;
|
|
let styleName;
|
|
let styleUpdates = null;
|
|
for (propKey in lastProps) {
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) ||
|
|
!lastProps.hasOwnProperty(propKey) ||
|
|
lastProps[propKey] == null
|
|
) {
|
|
continue;
|
|
}
|
|
if (propKey === STYLE) {
|
|
const lastStyle = lastProps[propKey];
|
|
for (styleName in lastStyle) {
|
|
if (lastStyle.hasOwnProperty(styleName)) {
|
|
if (!styleUpdates) {
|
|
styleUpdates = {};
|
|
}
|
|
styleUpdates[styleName] = '';
|
|
}
|
|
}
|
|
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
|
|
// Noop. This is handled by the clear text mechanism.
|
|
} else if (
|
|
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
|
|
propKey === SUPPRESS_HYDRATION_WARNING
|
|
) {
|
|
// Noop
|
|
} else if (propKey === AUTOFOCUS) {
|
|
// Noop. It doesn't work on updates anyway.
|
|
} else if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
// This is a special case. If any listener updates we need to ensure
|
|
// that the "current" fiber pointer gets updated so we need a commit
|
|
// to update this element.
|
|
if (!updatePayload) {
|
|
updatePayload = [];
|
|
}
|
|
} else {
|
|
// For all other deleted properties we add it to the queue. We use
|
|
// the whitelist in the commit phase instead.
|
|
(updatePayload = updatePayload || []).push(propKey, null);
|
|
}
|
|
}
|
|
for (propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
|
|
if (
|
|
!nextProps.hasOwnProperty(propKey) ||
|
|
nextProp === lastProp ||
|
|
(nextProp == null && lastProp == null)
|
|
) {
|
|
continue;
|
|
}
|
|
if (propKey === STYLE) {
|
|
if (__DEV__) {
|
|
if (nextProp) {
|
|
// Freeze the next style object so that we can assume it won't be
|
|
// mutated. We have already warned for this in the past.
|
|
Object.freeze(nextProp);
|
|
}
|
|
}
|
|
if (lastProp) {
|
|
// Unset styles on `lastProp` but not on `nextProp`.
|
|
for (styleName in lastProp) {
|
|
if (
|
|
lastProp.hasOwnProperty(styleName) &&
|
|
(!nextProp || !nextProp.hasOwnProperty(styleName))
|
|
) {
|
|
if (!styleUpdates) {
|
|
styleUpdates = {};
|
|
}
|
|
styleUpdates[styleName] = '';
|
|
}
|
|
}
|
|
// Update styles that changed since `lastProp`.
|
|
for (styleName in nextProp) {
|
|
if (
|
|
nextProp.hasOwnProperty(styleName) &&
|
|
lastProp[styleName] !== nextProp[styleName]
|
|
) {
|
|
if (!styleUpdates) {
|
|
styleUpdates = {};
|
|
}
|
|
styleUpdates[styleName] = nextProp[styleName];
|
|
}
|
|
}
|
|
} else {
|
|
// Relies on `updateStylesByID` not mutating `styleUpdates`.
|
|
if (!styleUpdates) {
|
|
if (!updatePayload) {
|
|
updatePayload = [];
|
|
}
|
|
updatePayload.push(propKey, styleUpdates);
|
|
}
|
|
styleUpdates = nextProp;
|
|
}
|
|
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
|
const nextHtml = nextProp ? nextProp[HTML] : undefined;
|
|
const lastHtml = lastProp ? lastProp[HTML] : undefined;
|
|
if (nextHtml != null) {
|
|
if (lastHtml !== nextHtml) {
|
|
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
|
|
}
|
|
} else {
|
|
// TODO: It might be too late to clear this if we have children
|
|
// inserted already.
|
|
}
|
|
} else if (propKey === CHILDREN) {
|
|
if (
|
|
lastProp !== nextProp &&
|
|
(typeof nextProp === 'string' || typeof nextProp === 'number')
|
|
) {
|
|
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
|
|
}
|
|
} else if (
|
|
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
|
|
propKey === SUPPRESS_HYDRATION_WARNING
|
|
) {
|
|
// Noop
|
|
} else if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
if (nextProp != null) {
|
|
// We eagerly listen to this even though we haven't committed yet.
|
|
if (__DEV__ && typeof nextProp !== 'function') {
|
|
warnForInvalidEventListener(propKey, nextProp);
|
|
}
|
|
ensureListeningTo(rootContainerElement, propKey);
|
|
}
|
|
if (!updatePayload && lastProp !== nextProp) {
|
|
// This is a special case. If any listener updates we need to ensure
|
|
// that the "current" props pointer gets updated so we need a commit
|
|
// to update this element.
|
|
updatePayload = [];
|
|
}
|
|
} else {
|
|
// For any other property we always add it to the queue and then we
|
|
// filter it out using the whitelist during the commit.
|
|
(updatePayload = updatePayload || []).push(propKey, nextProp);
|
|
}
|
|
}
|
|
if (styleUpdates) {
|
|
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
|
|
}
|
|
return updatePayload;
|
|
}
|
|
|
|
// Apply the diff.
|
|
export function updateProperties(
|
|
domElement: Element,
|
|
updatePayload: Array<any>,
|
|
tag: string,
|
|
lastRawProps: Object,
|
|
nextRawProps: Object,
|
|
): void {
|
|
// Update checked *before* name.
|
|
// In the middle of an update, it is possible to have multiple checked.
|
|
// When a checked radio tries to change name, browser makes another radio's checked false.
|
|
if (
|
|
tag === 'input' &&
|
|
nextRawProps.type === 'radio' &&
|
|
nextRawProps.name != null
|
|
) {
|
|
ReactDOMFiberInput.updateChecked(domElement, nextRawProps);
|
|
}
|
|
|
|
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
|
|
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
|
|
// Apply the diff.
|
|
updateDOMProperties(
|
|
domElement,
|
|
updatePayload,
|
|
wasCustomComponentTag,
|
|
isCustomComponentTag,
|
|
);
|
|
|
|
// TODO: Ensure that an update gets scheduled if any of the special props
|
|
// changed.
|
|
switch (tag) {
|
|
case 'input':
|
|
// Update the wrapper around inputs *after* updating props. This has to
|
|
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
|
|
// raise warnings and prevent the new value from being assigned.
|
|
ReactDOMFiberInput.updateWrapper(domElement, nextRawProps);
|
|
break;
|
|
case 'textarea':
|
|
ReactDOMFiberTextarea.updateWrapper(domElement, nextRawProps);
|
|
break;
|
|
case 'select':
|
|
// <select> value update needs to occur after <option> children
|
|
// reconciliation
|
|
ReactDOMFiberSelect.postUpdateWrapper(domElement, nextRawProps);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function getPossibleStandardName(propName: string): string | null {
|
|
if (__DEV__) {
|
|
const lowerCasedName = propName.toLowerCase();
|
|
if (!possibleStandardNames.hasOwnProperty(lowerCasedName)) {
|
|
return null;
|
|
}
|
|
return possibleStandardNames[lowerCasedName] || null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function diffHydratedProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
rawProps: Object,
|
|
parentNamespace: string,
|
|
rootContainerElement: Element | Document,
|
|
): null | Array<mixed> {
|
|
let isCustomComponentTag;
|
|
let extraAttributeNames: Set<string>;
|
|
|
|
if (__DEV__) {
|
|
suppressHydrationWarning = rawProps[SUPPRESS_HYDRATION_WARNING] === true;
|
|
isCustomComponentTag = isCustomComponent(tag, rawProps);
|
|
validatePropertiesInDevelopment(tag, rawProps);
|
|
if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {
|
|
warning(
|
|
false,
|
|
'%s is using shady DOM. Using shady DOM with React can ' +
|
|
'cause things to break subtly.',
|
|
getCurrentFiberOwnerName() || 'A component',
|
|
);
|
|
didWarnShadyDOM = true;
|
|
}
|
|
}
|
|
|
|
// TODO: Make sure that we check isMounted before firing any of these events.
|
|
switch (tag) {
|
|
case 'iframe':
|
|
case 'object':
|
|
trapBubbledEvent('topLoad', 'load', domElement);
|
|
break;
|
|
case 'video':
|
|
case 'audio':
|
|
// Create listener for each media event
|
|
for (const event in mediaEventTypes) {
|
|
if (mediaEventTypes.hasOwnProperty(event)) {
|
|
trapBubbledEvent(event, mediaEventTypes[event], domElement);
|
|
}
|
|
}
|
|
break;
|
|
case 'source':
|
|
trapBubbledEvent('topError', 'error', domElement);
|
|
break;
|
|
case 'img':
|
|
case 'image':
|
|
case 'link':
|
|
trapBubbledEvent('topError', 'error', domElement);
|
|
trapBubbledEvent('topLoad', 'load', domElement);
|
|
break;
|
|
case 'form':
|
|
trapBubbledEvent('topReset', 'reset', domElement);
|
|
trapBubbledEvent('topSubmit', 'submit', domElement);
|
|
break;
|
|
case 'details':
|
|
trapBubbledEvent('topToggle', 'toggle', domElement);
|
|
break;
|
|
case 'input':
|
|
ReactDOMFiberInput.initWrapperState(domElement, rawProps);
|
|
trapBubbledEvent('topInvalid', 'invalid', domElement);
|
|
// For controlled components we always need to ensure we're listening
|
|
// to onChange. Even if there is no listener.
|
|
ensureListeningTo(rootContainerElement, 'onChange');
|
|
break;
|
|
case 'option':
|
|
ReactDOMFiberOption.validateProps(domElement, rawProps);
|
|
break;
|
|
case 'select':
|
|
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
|
|
trapBubbledEvent('topInvalid', 'invalid', domElement);
|
|
// For controlled components we always need to ensure we're listening
|
|
// to onChange. Even if there is no listener.
|
|
ensureListeningTo(rootContainerElement, 'onChange');
|
|
break;
|
|
case 'textarea':
|
|
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
|
|
trapBubbledEvent('topInvalid', 'invalid', domElement);
|
|
// For controlled components we always need to ensure we're listening
|
|
// to onChange. Even if there is no listener.
|
|
ensureListeningTo(rootContainerElement, 'onChange');
|
|
break;
|
|
}
|
|
|
|
assertValidProps(tag, rawProps, getStack);
|
|
|
|
if (__DEV__) {
|
|
extraAttributeNames = new Set();
|
|
const attributes = domElement.attributes;
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
const name = attributes[i].name.toLowerCase();
|
|
switch (name) {
|
|
// Built-in SSR attribute is whitelisted
|
|
case 'data-reactroot':
|
|
break;
|
|
// Controlled attributes are not validated
|
|
// TODO: Only ignore them on controlled tags.
|
|
case 'value':
|
|
break;
|
|
case 'checked':
|
|
break;
|
|
case 'selected':
|
|
break;
|
|
default:
|
|
// Intentionally use the original name.
|
|
// See discussion in https://github.com/facebook/react/pull/10676.
|
|
extraAttributeNames.add(attributes[i].name);
|
|
}
|
|
}
|
|
}
|
|
|
|
let updatePayload = null;
|
|
for (const propKey in rawProps) {
|
|
if (!rawProps.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const nextProp = rawProps[propKey];
|
|
if (propKey === CHILDREN) {
|
|
// For text content children we compare against textContent. This
|
|
// might match additional HTML that is hidden when we read it using
|
|
// textContent. E.g. "foo" will match "f<span>oo</span>" but that still
|
|
// satisfies our requirement. Our requirement is not to produce perfect
|
|
// HTML and attributes. Ideally we should preserve structure but it's
|
|
// ok not to if the visible content is still enough to indicate what
|
|
// even listeners these nodes might be wired up to.
|
|
// TODO: Warn if there is more than a single textNode as a child.
|
|
// TODO: Should we use domElement.firstChild.nodeValue to compare?
|
|
if (typeof nextProp === 'string') {
|
|
if (domElement.textContent !== nextProp) {
|
|
if (__DEV__ && !suppressHydrationWarning) {
|
|
warnForTextDifference(domElement.textContent, nextProp);
|
|
}
|
|
updatePayload = [CHILDREN, nextProp];
|
|
}
|
|
} else if (typeof nextProp === 'number') {
|
|
if (domElement.textContent !== '' + nextProp) {
|
|
if (__DEV__ && !suppressHydrationWarning) {
|
|
warnForTextDifference(domElement.textContent, nextProp);
|
|
}
|
|
updatePayload = [CHILDREN, '' + nextProp];
|
|
}
|
|
}
|
|
} else if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
if (nextProp != null) {
|
|
if (__DEV__ && typeof nextProp !== 'function') {
|
|
warnForInvalidEventListener(propKey, nextProp);
|
|
}
|
|
ensureListeningTo(rootContainerElement, propKey);
|
|
}
|
|
} else if (
|
|
__DEV__ &&
|
|
// Convince Flow we've calculated it (it's DEV-only in this method.)
|
|
typeof isCustomComponentTag === 'boolean'
|
|
) {
|
|
// Validate that the properties correspond to their expected values.
|
|
let serverValue;
|
|
const propertyInfo = getPropertyInfo(propKey);
|
|
if (suppressHydrationWarning) {
|
|
// Don't bother comparing. We're ignoring all these warnings.
|
|
} else if (
|
|
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
|
|
propKey === SUPPRESS_HYDRATION_WARNING ||
|
|
// Controlled attributes are not validated
|
|
// TODO: Only ignore them on controlled tags.
|
|
propKey === 'value' ||
|
|
propKey === 'checked' ||
|
|
propKey === 'selected'
|
|
) {
|
|
// Noop
|
|
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
|
const rawHtml = nextProp ? nextProp[HTML] || '' : '';
|
|
const serverHTML = domElement.innerHTML;
|
|
const expectedHTML = normalizeHTML(domElement, rawHtml);
|
|
if (expectedHTML !== serverHTML) {
|
|
warnForPropDifference(propKey, serverHTML, expectedHTML);
|
|
}
|
|
} else if (propKey === STYLE) {
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
extraAttributeNames.delete(propKey);
|
|
const expectedStyle = CSSPropertyOperations.createDangerousStringForStyles(
|
|
nextProp,
|
|
);
|
|
serverValue = domElement.getAttribute('style');
|
|
if (expectedStyle !== serverValue) {
|
|
warnForPropDifference(propKey, serverValue, expectedStyle);
|
|
}
|
|
} else if (isCustomComponentTag) {
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
extraAttributeNames.delete(propKey.toLowerCase());
|
|
serverValue = DOMPropertyOperations.getValueForAttribute(
|
|
domElement,
|
|
propKey,
|
|
nextProp,
|
|
);
|
|
|
|
if (nextProp !== serverValue) {
|
|
warnForPropDifference(propKey, serverValue, nextProp);
|
|
}
|
|
} else if (
|
|
!shouldIgnoreAttribute(propKey, propertyInfo, isCustomComponentTag) &&
|
|
!shouldRemoveAttribute(
|
|
propKey,
|
|
nextProp,
|
|
propertyInfo,
|
|
isCustomComponentTag,
|
|
)
|
|
) {
|
|
let isMismatchDueToBadCasing = false;
|
|
if (propertyInfo !== null) {
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
extraAttributeNames.delete(propertyInfo.attributeName);
|
|
serverValue = DOMPropertyOperations.getValueForProperty(
|
|
domElement,
|
|
propKey,
|
|
nextProp,
|
|
propertyInfo,
|
|
);
|
|
} else {
|
|
let ownNamespace = parentNamespace;
|
|
if (ownNamespace === HTML_NAMESPACE) {
|
|
ownNamespace = getIntrinsicNamespace(tag);
|
|
}
|
|
if (ownNamespace === HTML_NAMESPACE) {
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
extraAttributeNames.delete(propKey.toLowerCase());
|
|
} else {
|
|
const standardName = getPossibleStandardName(propKey);
|
|
if (standardName !== null && standardName !== propKey) {
|
|
// If an SVG prop is supplied with bad casing, it will
|
|
// be successfully parsed from HTML, but will produce a mismatch
|
|
// (and would be incorrectly rendered on the client).
|
|
// However, we already warn about bad casing elsewhere.
|
|
// So we'll skip the misleading extra mismatch warning in this case.
|
|
isMismatchDueToBadCasing = true;
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
extraAttributeNames.delete(standardName);
|
|
}
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
extraAttributeNames.delete(propKey);
|
|
}
|
|
serverValue = DOMPropertyOperations.getValueForAttribute(
|
|
domElement,
|
|
propKey,
|
|
nextProp,
|
|
);
|
|
}
|
|
|
|
if (nextProp !== serverValue && !isMismatchDueToBadCasing) {
|
|
warnForPropDifference(propKey, serverValue, nextProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (__DEV__) {
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
|
|
// $FlowFixMe - Should be inferred as not undefined.
|
|
warnForExtraAttributes(extraAttributeNames);
|
|
}
|
|
}
|
|
|
|
switch (tag) {
|
|
case 'input':
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
inputValueTracking.track((domElement: any));
|
|
ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
|
|
break;
|
|
case 'textarea':
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
inputValueTracking.track((domElement: any));
|
|
ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
|
|
break;
|
|
case 'select':
|
|
case 'option':
|
|
// For input and textarea we current always set the value property at
|
|
// post mount to force it to diverge from attributes. However, for
|
|
// option and select we don't quite do the same thing and select
|
|
// is not resilient to the DOM state changing so we don't do that here.
|
|
// TODO: Consider not doing this for input and textarea.
|
|
break;
|
|
default:
|
|
if (typeof rawProps.onClick === 'function') {
|
|
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
|
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
|
}
|
|
break;
|
|
}
|
|
|
|
return updatePayload;
|
|
}
|
|
|
|
export function diffHydratedText(textNode: Text, text: string): boolean {
|
|
const isDifferent = textNode.nodeValue !== text;
|
|
return isDifferent;
|
|
}
|
|
|
|
export function warnForUnmatchedText(textNode: Text, text: string) {
|
|
if (__DEV__) {
|
|
warnForTextDifference(textNode.nodeValue, text);
|
|
}
|
|
}
|
|
|
|
export function warnForDeletedHydratableElement(
|
|
parentNode: Element | Document,
|
|
child: Element,
|
|
) {
|
|
if (__DEV__) {
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
warning(
|
|
false,
|
|
'Did not expect server HTML to contain a <%s> in <%s>.',
|
|
child.nodeName.toLowerCase(),
|
|
parentNode.nodeName.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function warnForDeletedHydratableText(
|
|
parentNode: Element | Document,
|
|
child: Text,
|
|
) {
|
|
if (__DEV__) {
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
warning(
|
|
false,
|
|
'Did not expect server HTML to contain the text node "%s" in <%s>.',
|
|
child.nodeValue,
|
|
parentNode.nodeName.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function warnForInsertedHydratedElement(
|
|
parentNode: Element | Document,
|
|
tag: string,
|
|
props: Object,
|
|
) {
|
|
if (__DEV__) {
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
warning(
|
|
false,
|
|
'Expected server HTML to contain a matching <%s> in <%s>.',
|
|
tag,
|
|
parentNode.nodeName.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function warnForInsertedHydratedText(
|
|
parentNode: Element | Document,
|
|
text: string,
|
|
) {
|
|
if (__DEV__) {
|
|
if (text === '') {
|
|
// We expect to insert empty text nodes since they're not represented in
|
|
// the HTML.
|
|
// TODO: Remove this special case if we can just avoid inserting empty
|
|
// text nodes.
|
|
return;
|
|
}
|
|
if (didWarnInvalidHydration) {
|
|
return;
|
|
}
|
|
didWarnInvalidHydration = true;
|
|
warning(
|
|
false,
|
|
'Expected server HTML to contain a matching text node for "%s" in <%s>.',
|
|
text,
|
|
parentNode.nodeName.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function restoreControlledState(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
): void {
|
|
switch (tag) {
|
|
case 'input':
|
|
ReactDOMFiberInput.restoreControlledState(domElement, props);
|
|
return;
|
|
case 'textarea':
|
|
ReactDOMFiberTextarea.restoreControlledState(domElement, props);
|
|
return;
|
|
case 'select':
|
|
ReactDOMFiberSelect.restoreControlledState(domElement, props);
|
|
return;
|
|
}
|
|
}
|