Skip to content

Commit 73deff0

Browse files
authored
Refactor DOMProperty and CSSProperty (#26513)
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](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.
1 parent 2d51251 commit 73deff0

11 files changed

+456
-393
lines changed

packages/react-dom-bindings/src/client/CSSPropertyOperations.js

+3-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {shorthandToLonghand} from './CSSShorthandProperty';
99

1010
import hyphenateStyleName from '../shared/hyphenateStyleName';
1111
import warnValidStyle from '../shared/warnValidStyle';
12-
import {isUnitlessNumber} from '../shared/CSSProperty';
12+
import isUnitlessNumber from '../shared/isUnitlessNumber';
1313
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
1414

1515
/**
@@ -42,10 +42,7 @@ export function createDangerousStringForStyles(styles) {
4242
if (
4343
typeof value === 'number' &&
4444
value !== 0 &&
45-
!(
46-
isUnitlessNumber.hasOwnProperty(styleName) &&
47-
isUnitlessNumber[styleName]
48-
)
45+
!isUnitlessNumber(styleName)
4946
) {
5047
serialized +=
5148
delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
@@ -101,10 +98,7 @@ export function setValueForStyles(node, styles) {
10198
} else if (
10299
typeof value === 'number' &&
103100
value !== 0 &&
104-
!(
105-
isUnitlessNumber.hasOwnProperty(styleName) &&
106-
isUnitlessNumber[styleName]
107-
)
101+
!isUnitlessNumber(styleName)
108102
) {
109103
style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
110104
} else {

packages/react-dom-bindings/src/client/DOMPropertyOperations.js

+108-128
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
*/
99

1010
import {
11-
getPropertyInfo,
12-
isAttributeNameSafe,
1311
BOOLEAN,
1412
OVERLOADED_BOOLEAN,
1513
NUMERIC,
1614
POSITIVE_NUMERIC,
1715
} from '../shared/DOMProperty';
16+
17+
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
1818
import sanitizeURL from '../shared/sanitizeURL';
1919
import {
2020
enableTrustedTypesIntegration,
@@ -38,11 +38,6 @@ export function getValueForProperty(
3838
propertyInfo: PropertyInfo,
3939
): mixed {
4040
if (__DEV__) {
41-
if (propertyInfo.mustUseProperty) {
42-
const {propertyName} = propertyInfo;
43-
return (node: any)[propertyName];
44-
}
45-
4641
const attributeName = propertyInfo.attributeName;
4742

4843
if (!node.hasAttribute(attributeName)) {
@@ -287,152 +282,137 @@ export function getValueForAttributeOnCustomComponent(
287282
* @param {string} name
288283
* @param {*} value
289284
*/
290-
export function setValueForProperty(node: Element, name: string, value: mixed) {
291-
if (
292-
// shouldIgnoreAttribute
293-
// We have already filtered out reserved words.
294-
name.length > 2 &&
295-
(name[0] === 'o' || name[0] === 'O') &&
296-
(name[1] === 'n' || name[1] === 'N')
297-
) {
285+
export function setValueForProperty(
286+
node: Element,
287+
propertyInfo: PropertyInfo,
288+
value: mixed,
289+
) {
290+
const attributeName = propertyInfo.attributeName;
291+
292+
if (value === null) {
293+
node.removeAttribute(attributeName);
298294
return;
299295
}
300296

301-
const propertyInfo = getPropertyInfo(name);
302-
if (propertyInfo !== null) {
303-
if (propertyInfo.mustUseProperty) {
304-
// We assume mustUseProperty are of BOOLEAN type because that's the only way we use it
305-
// right now.
306-
(node: any)[propertyInfo.propertyName] =
307-
value && typeof value !== 'function' && typeof value !== 'symbol';
297+
// shouldRemoveAttribute
298+
switch (typeof value) {
299+
case 'undefined':
300+
case 'function':
301+
case 'symbol': // eslint-disable-line
302+
node.removeAttribute(attributeName);
308303
return;
304+
case 'boolean': {
305+
if (!propertyInfo.acceptsBooleans) {
306+
node.removeAttribute(attributeName);
307+
return;
308+
}
309309
}
310-
311-
// The rest are treated as attributes with special cases.
312-
313-
const attributeName = propertyInfo.attributeName;
314-
315-
if (value === null) {
310+
}
311+
if (enableFilterEmptyStringAttributesDOM) {
312+
if (propertyInfo.removeEmptyString && value === '') {
313+
if (__DEV__) {
314+
if (attributeName === 'src') {
315+
console.error(
316+
'An empty string ("") was passed to the %s attribute. ' +
317+
'This may cause the browser to download the whole page again over the network. ' +
318+
'To fix this, either do not render the element at all ' +
319+
'or pass null to %s instead of an empty string.',
320+
attributeName,
321+
attributeName,
322+
);
323+
} else {
324+
console.error(
325+
'An empty string ("") was passed to the %s attribute. ' +
326+
'To fix this, either do not render the element at all ' +
327+
'or pass null to %s instead of an empty string.',
328+
attributeName,
329+
attributeName,
330+
);
331+
}
332+
}
316333
node.removeAttribute(attributeName);
317334
return;
318335
}
336+
}
319337

320-
// shouldRemoveAttribute
321-
switch (typeof value) {
322-
case 'undefined':
323-
case 'function':
324-
case 'symbol': // eslint-disable-line
338+
switch (propertyInfo.type) {
339+
case BOOLEAN:
340+
if (value) {
341+
node.setAttribute(attributeName, '');
342+
} else {
325343
node.removeAttribute(attributeName);
326344
return;
327-
case 'boolean': {
328-
if (!propertyInfo.acceptsBooleans) {
329-
node.removeAttribute(attributeName);
330-
return;
345+
}
346+
break;
347+
case OVERLOADED_BOOLEAN:
348+
if (value === true) {
349+
node.setAttribute(attributeName, '');
350+
} else if (value === false) {
351+
node.removeAttribute(attributeName);
352+
} else {
353+
if (__DEV__) {
354+
checkAttributeStringCoercion(value, attributeName);
331355
}
356+
node.setAttribute(attributeName, (value: any));
332357
}
333-
}
334-
if (enableFilterEmptyStringAttributesDOM) {
335-
if (propertyInfo.removeEmptyString && value === '') {
358+
return;
359+
case NUMERIC:
360+
if (!isNaN(value)) {
336361
if (__DEV__) {
337-
if (name === 'src') {
338-
console.error(
339-
'An empty string ("") was passed to the %s attribute. ' +
340-
'This may cause the browser to download the whole page again over the network. ' +
341-
'To fix this, either do not render the element at all ' +
342-
'or pass null to %s instead of an empty string.',
343-
name,
344-
name,
345-
);
346-
} else {
347-
console.error(
348-
'An empty string ("") was passed to the %s attribute. ' +
349-
'To fix this, either do not render the element at all ' +
350-
'or pass null to %s instead of an empty string.',
351-
name,
352-
name,
353-
);
354-
}
362+
checkAttributeStringCoercion(value, attributeName);
355363
}
364+
node.setAttribute(attributeName, (value: any));
365+
} else {
356366
node.removeAttribute(attributeName);
357-
return;
358367
}
359-
}
360-
361-
switch (propertyInfo.type) {
362-
case BOOLEAN:
363-
if (value) {
364-
node.setAttribute(attributeName, '');
365-
} else {
366-
node.removeAttribute(attributeName);
367-
return;
368-
}
369-
break;
370-
case OVERLOADED_BOOLEAN:
371-
if (value === true) {
372-
node.setAttribute(attributeName, '');
373-
} else if (value === false) {
374-
node.removeAttribute(attributeName);
375-
} else {
376-
if (__DEV__) {
377-
checkAttributeStringCoercion(value, attributeName);
378-
}
379-
node.setAttribute(attributeName, (value: any));
380-
}
381-
return;
382-
case NUMERIC:
383-
if (!isNaN(value)) {
384-
if (__DEV__) {
385-
checkAttributeStringCoercion(value, attributeName);
386-
}
387-
node.setAttribute(attributeName, (value: any));
388-
} else {
389-
node.removeAttribute(attributeName);
390-
}
391-
break;
392-
case POSITIVE_NUMERIC:
393-
if (!isNaN(value) && (value: any) >= 1) {
394-
if (__DEV__) {
395-
checkAttributeStringCoercion(value, attributeName);
396-
}
397-
node.setAttribute(attributeName, (value: any));
398-
} else {
399-
node.removeAttribute(attributeName);
400-
}
401-
break;
402-
default: {
368+
break;
369+
case POSITIVE_NUMERIC:
370+
if (!isNaN(value) && (value: any) >= 1) {
403371
if (__DEV__) {
404372
checkAttributeStringCoercion(value, attributeName);
405373
}
406-
let attributeValue;
407-
// `setAttribute` with objects becomes only `[object]` in IE8/9,
408-
// ('' + value) makes it output the correct toString()-value.
409-
if (enableTrustedTypesIntegration) {
410-
if (propertyInfo.sanitizeURL) {
411-
attributeValue = (sanitizeURL(value): any);
412-
} else {
413-
attributeValue = (value: any);
414-
}
374+
node.setAttribute(attributeName, (value: any));
375+
} else {
376+
node.removeAttribute(attributeName);
377+
}
378+
break;
379+
default: {
380+
if (__DEV__) {
381+
checkAttributeStringCoercion(value, attributeName);
382+
}
383+
let attributeValue;
384+
// `setAttribute` with objects becomes only `[object]` in IE8/9,
385+
// ('' + value) makes it output the correct toString()-value.
386+
if (enableTrustedTypesIntegration) {
387+
if (propertyInfo.sanitizeURL) {
388+
attributeValue = (sanitizeURL(value): any);
415389
} else {
416-
// We have already verified this above.
417-
// eslint-disable-next-line react-internal/safe-string-coercion
418-
attributeValue = '' + (value: any);
419-
if (propertyInfo.sanitizeURL) {
420-
attributeValue = sanitizeURL(attributeValue);
421-
}
390+
attributeValue = (value: any);
422391
}
423-
const attributeNamespace = propertyInfo.attributeNamespace;
424-
if (attributeNamespace) {
425-
node.setAttributeNS(
426-
attributeNamespace,
427-
attributeName,
428-
attributeValue,
429-
);
430-
} else {
431-
node.setAttribute(attributeName, attributeValue);
392+
} else {
393+
// We have already verified this above.
394+
// eslint-disable-next-line react-internal/safe-string-coercion
395+
attributeValue = '' + (value: any);
396+
if (propertyInfo.sanitizeURL) {
397+
attributeValue = sanitizeURL(attributeValue);
432398
}
433399
}
400+
const attributeNamespace = propertyInfo.attributeNamespace;
401+
if (attributeNamespace) {
402+
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
403+
} else {
404+
node.setAttribute(attributeName, attributeValue);
405+
}
434406
}
435-
} else if (isAttributeNameSafe(name)) {
407+
}
408+
}
409+
410+
export function setValueForAttribute(
411+
node: Element,
412+
name: string,
413+
value: mixed,
414+
) {
415+
if (isAttributeNameSafe(name)) {
436416
// If the prop isn't in the special list, treat it as a simple attribute.
437417
// shouldRemoveAttribute
438418
if (value === null) {

0 commit comments

Comments
 (0)