Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor DOM Bindings Completely Off of DOMProperty Meta Programming #26546

Merged
merged 16 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/react-dom-bindings/src/client/CSSPropertyOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ export function createDangerousStringForStyles(styles) {
* @param {object} styles
*/
export function setValueForStyles(node, styles) {
if (styles != null && typeof styles !== 'object') {
throw new Error(
'The `style` prop expects a mapping from style properties to values, ' +
"not a string. For example, style={{marginRight: spacing + 'em'}} when " +
'using JSX.',
);
}
if (__DEV__) {
if (styles) {
// 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(styles);
}
}

const style = node.style;
for (const styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
Expand Down
328 changes: 29 additions & 299 deletions packages/react-dom-bindings/src/client/DOMPropertyOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,181 +7,14 @@
* @flow
*/

import {
BOOLEAN,
OVERLOADED_BOOLEAN,
NUMERIC,
POSITIVE_NUMERIC,
} from '../shared/DOMProperty';

import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import sanitizeURL from '../shared/sanitizeURL';
import {
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
enableFilterEmptyStringAttributesDOM,
} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';

import type {PropertyInfo} from '../shared/DOMProperty';

/**
* Get the value for a property on a node. Only used in DEV for SSR validation.
* The "expected" argument is used as a hint of what the expected value is.
* Some properties have multiple equivalent values.
*/
export function getValueForProperty(
node: Element,
name: string,
expected: mixed,
propertyInfo: PropertyInfo,
): mixed {
if (__DEV__) {
const attributeName = propertyInfo.attributeName;

if (!node.hasAttribute(attributeName)) {
// shouldRemoveAttribute
switch (typeof expected) {
case 'function':
case 'symbol': // eslint-disable-line
return expected;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
return expected;
}
}
}
switch (propertyInfo.type) {
case BOOLEAN: {
if (!expected) {
return expected;
}
break;
}
case OVERLOADED_BOOLEAN: {
if (expected === false) {
return expected;
}
break;
}
case NUMERIC: {
if (isNaN(expected)) {
return expected;
}
break;
}
case POSITIVE_NUMERIC: {
if (isNaN(expected) || (expected: any) < 1) {
return expected;
}
break;
}
}
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && expected === '') {
if (__DEV__) {
if (name === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
}
}
return expected;
}
}
return expected === undefined ? undefined : null;
}

// Even if this property uses a namespace we use getAttribute
// because we assume its namespaced name is the same as our config.
// To use getAttributeNS we need the local name which we don't have
// in our config atm.
const value = node.getAttribute(attributeName);

if (expected == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}

// shouldRemoveAttribute
switch (typeof expected) {
case 'function':
case 'symbol': // eslint-disable-line
return value;
}
switch (propertyInfo.type) {
case BOOLEAN: {
if (expected) {
// If this was a boolean, it doesn't matter what the value is
// the fact that we have it is the same as the expected.
// As long as it's positive.
return expected;
}
return value;
}
case OVERLOADED_BOOLEAN: {
if (value === '') {
return true;
}
if (expected === false) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
break;
}
case NUMERIC: {
if (isNaN(expected)) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
break;
}
case POSITIVE_NUMERIC: {
if (isNaN(expected) || (expected: any) < 1) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
break;
}
}
if (__DEV__) {
checkAttributeStringCoercion(expected, name);
}
if (propertyInfo.sanitizeURL) {
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
if (value === '' + (sanitizeURL(expected): any)) {
return expected;
}
return value;
}
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
if (value === '' + (expected: any)) {
return expected;
}
return value;
}
}

/**
* Get the value for a attribute on a node. Only used in DEV for SSR validation.
* The third argument is used as a hint of what the expected value is. Some
Expand Down Expand Up @@ -271,138 +104,6 @@ export function getValueForAttributeOnCustomComponent(
}
}

/**
* Sets the value for a property on a node.
*
* @param {DOMElement} node
* @param {string} name
* @param {*} value
*/
export function setValueForProperty(
node: Element,
propertyInfo: PropertyInfo,
value: mixed,
) {
const attributeName = propertyInfo.attributeName;

if (value === null) {
node.removeAttribute(attributeName);
return;
}

// shouldRemoveAttribute
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol': // eslint-disable-line
node.removeAttribute(attributeName);
return;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
node.removeAttribute(attributeName);
return;
}
}
}
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && value === '') {
if (__DEV__) {
if (attributeName === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
attributeName,
attributeName,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
attributeName,
attributeName,
);
}
}
node.removeAttribute(attributeName);
return;
}
}

switch (propertyInfo.type) {
case BOOLEAN:
if (value) {
node.setAttribute(attributeName, '');
} else {
node.removeAttribute(attributeName);
return;
}
break;
case OVERLOADED_BOOLEAN:
if (value === true) {
node.setAttribute(attributeName, '');
} else if (value === false) {
node.removeAttribute(attributeName);
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
}
return;
case NUMERIC:
if (!isNaN(value)) {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
} else {
node.removeAttribute(attributeName);
}
break;
case POSITIVE_NUMERIC:
if (!isNaN(value) && (value: any) >= 1) {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
} else {
node.removeAttribute(attributeName);
}
break;
default: {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
let attributeValue;
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
if (enableTrustedTypesIntegration) {
if (propertyInfo.sanitizeURL) {
attributeValue = (sanitizeURL(value): any);
} else {
attributeValue = (value: any);
}
} else {
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
attributeValue = '' + (value: any);
if (propertyInfo.sanitizeURL) {
attributeValue = sanitizeURL(attributeValue);
}
}
const attributeNamespace = propertyInfo.attributeNamespace;
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
}
}
}
}

export function setValueForAttribute(
node: Element,
name: string,
Expand Down Expand Up @@ -439,6 +140,35 @@ export function setValueForAttribute(
}
}

export function setValueForNamespacedAttribute(
node: Element,
namespace: string,
name: string,
value: mixed,
) {
if (value === null) {
node.removeAttribute(name);
return;
}
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
case 'boolean': {
node.removeAttribute(name);
return;
}
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
node.setAttributeNS(
namespace,
name,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
);
}

export function setValueForPropertyOnCustomComponent(
node: Element,
name: string,
Expand Down
Loading