Skip to content

Commit

Permalink
Refactor DOMProperty and CSSProperty (facebook#26513)
Browse files Browse the repository at this point in the history
This is a step towards getting rid of the meta programming in
DOMProperty and CSSProperty.

This moves isAttributeNameSafe and isUnitlessNumber to a separate shared
modules.

isUnitlessNumber is now a single switch instead of meta-programming.
There is a slight behavior change here in that I hard code a specific
set of vendor-prefixed attributes instead of prefixing all the unitless
properties. I based this list on what getComputedStyle returns in
current browsers. I removed Opera prefixes because they were [removed in
Opera](https://dev.opera.com/blog/css-vendor-prefixes-in-opera-12-50-snapshots/)
itself. I included the ms ones mentioned [in the original
PR](facebook@5abcce5).
These shouldn't really be used anymore anyway so should be pretty safe.
Worst case, they'll fallback to the other property if you specify both.

Finally I inline the mustUseProperty special cases - which are also the
only thing that uses propertyName. These are really all controlled
components and all booleans.

I'm making a small breaking change here by treating `checked` and
`selected` specially only on the `input` and `option` tags instead of
all tags. That's because those are the only DOM nodes that actually have
those properties but we used to set them as expandos instead of
attributes before. That's why one of the tests is updated to now use
`input` instead of testing an expando on a `div` which isn't a real use
case. Interestingly this also uncovered that we update checked twice for
some reason but keeping that logic for now.

Ideally `multiple` and `muted` should move into `select` and
`audio`/`video` respectively for the same reason.

No change to the attribute-behavior fixture.
  • Loading branch information
sebmarkbage authored and AndyPengc12 committed Apr 15, 2024
1 parent daa0042 commit 9cd32a5
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 393 deletions.
12 changes: 3 additions & 9 deletions packages/react-dom-bindings/src/client/CSSPropertyOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {shorthandToLonghand} from './CSSShorthandProperty';

import hyphenateStyleName from '../shared/hyphenateStyleName';
import warnValidStyle from '../shared/warnValidStyle';
import {isUnitlessNumber} from '../shared/CSSProperty';
import isUnitlessNumber from '../shared/isUnitlessNumber';
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';

/**
Expand Down Expand Up @@ -42,10 +42,7 @@ export function createDangerousStringForStyles(styles) {
if (
typeof value === 'number' &&
value !== 0 &&
!(
isUnitlessNumber.hasOwnProperty(styleName) &&
isUnitlessNumber[styleName]
)
!isUnitlessNumber(styleName)
) {
serialized +=
delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
Expand Down Expand Up @@ -101,10 +98,7 @@ export function setValueForStyles(node, styles) {
} else if (
typeof value === 'number' &&
value !== 0 &&
!(
isUnitlessNumber.hasOwnProperty(styleName) &&
isUnitlessNumber[styleName]
)
!isUnitlessNumber(styleName)
) {
style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
} else {
Expand Down
236 changes: 108 additions & 128 deletions packages/react-dom-bindings/src/client/DOMPropertyOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
*/

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

import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import sanitizeURL from '../shared/sanitizeURL';
import {
enableTrustedTypesIntegration,
Expand All @@ -38,11 +38,6 @@ export function getValueForProperty(
propertyInfo: PropertyInfo,
): mixed {
if (__DEV__) {
if (propertyInfo.mustUseProperty) {
const {propertyName} = propertyInfo;
return (node: any)[propertyName];
}

const attributeName = propertyInfo.attributeName;

if (!node.hasAttribute(attributeName)) {
Expand Down Expand Up @@ -287,152 +282,137 @@ export function getValueForAttributeOnCustomComponent(
* @param {string} name
* @param {*} value
*/
export function setValueForProperty(node: Element, name: string, value: mixed) {
if (
// shouldIgnoreAttribute
// We have already filtered out reserved words.
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
export function setValueForProperty(
node: Element,
propertyInfo: PropertyInfo,
value: mixed,
) {
const attributeName = propertyInfo.attributeName;

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

const propertyInfo = getPropertyInfo(name);
if (propertyInfo !== null) {
if (propertyInfo.mustUseProperty) {
// We assume mustUseProperty are of BOOLEAN type because that's the only way we use it
// right now.
(node: any)[propertyInfo.propertyName] =
value && typeof value !== 'function' && typeof value !== 'symbol';
// 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;
}
}

// The rest are treated as attributes with special cases.

const attributeName = propertyInfo.attributeName;

if (value === null) {
}
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;
}
}

// shouldRemoveAttribute
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol': // eslint-disable-line
switch (propertyInfo.type) {
case BOOLEAN:
if (value) {
node.setAttribute(attributeName, '');
} else {
node.removeAttribute(attributeName);
return;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
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));
}
}
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && value === '') {
return;
case NUMERIC:
if (!isNaN(value)) {
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,
);
}
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
} else {
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: {
break;
case POSITIVE_NUMERIC:
if (!isNaN(value) && (value: any) >= 1) {
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);
}
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 {
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
attributeValue = '' + (value: any);
if (propertyInfo.sanitizeURL) {
attributeValue = sanitizeURL(attributeValue);
}
attributeValue = (value: any);
}
const attributeNamespace = propertyInfo.attributeNamespace;
if (attributeNamespace) {
node.setAttributeNS(
attributeNamespace,
attributeName,
attributeValue,
);
} else {
node.setAttribute(attributeName, attributeValue);
} 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);
}
}
} else if (isAttributeNameSafe(name)) {
}
}

export function setValueForAttribute(
node: Element,
name: string,
value: mixed,
) {
if (isAttributeNameSafe(name)) {
// If the prop isn't in the special list, treat it as a simple attribute.
// shouldRemoveAttribute
if (value === null) {
Expand Down
Loading

0 comments on commit 9cd32a5

Please sign in to comment.