-
Notifications
You must be signed in to change notification settings - Fork 48.8k
Refactor DOM attribute code #11804
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 attribute code #11804
Changes from all commits
24fa7e5
91a32f2
e88f4bd
9370492
171795b
1208a54
7022000
4018f7b
915b1c6
22646c7
ec83eac
2484117
388498e
632dabd
50c7624
57d4169
463c4f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,51 +3,33 @@ | |
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow | ||
*/ | ||
|
||
import { | ||
ID_ATTRIBUTE_NAME, | ||
ROOT_ATTRIBUTE_NAME, | ||
getPropertyInfo, | ||
shouldSetAttribute, | ||
shouldSkipAttribute, | ||
shouldTreatAttributeValueAsNull, | ||
isAttributeNameSafe, | ||
} from '../shared/DOMProperty'; | ||
|
||
// shouldIgnoreValue() is currently duplicated in DOMMarkupOperations. | ||
// TODO: Find a better place for this. | ||
function shouldIgnoreValue(propertyInfo, value) { | ||
return ( | ||
value == null || | ||
(propertyInfo.hasBooleanValue && !value) || | ||
(propertyInfo.hasNumericValue && isNaN(value)) || | ||
(propertyInfo.hasPositiveNumericValue && value < 1) || | ||
(propertyInfo.hasOverloadedBooleanValue && value === false) | ||
); | ||
} | ||
|
||
/** | ||
* Operations for dealing with DOM properties. | ||
*/ | ||
|
||
export function setAttributeForID(node, id) { | ||
node.setAttribute(ID_ATTRIBUTE_NAME, id); | ||
} | ||
|
||
export function setAttributeForRoot(node) { | ||
node.setAttribute(ROOT_ATTRIBUTE_NAME, ''); | ||
} | ||
|
||
/** | ||
* 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, name, expected) { | ||
export function getValueForProperty( | ||
node: Element, | ||
name: string, | ||
expected: mixed, | ||
): mixed { | ||
if (__DEV__) { | ||
const propertyInfo = getPropertyInfo(name); | ||
if (propertyInfo) { | ||
if (propertyInfo.mustUseProperty) { | ||
return node[propertyInfo.propertyName]; | ||
const {propertyName} = propertyInfo; | ||
return (node: any)[propertyName]; | ||
} else { | ||
const attributeName = propertyInfo.attributeName; | ||
|
||
|
@@ -59,16 +41,16 @@ export function getValueForProperty(node, name, expected) { | |
if (value === '') { | ||
return true; | ||
} | ||
if (shouldIgnoreValue(propertyInfo, expected)) { | ||
if (shouldTreatAttributeValueAsNull(name, expected, false)) { | ||
return value; | ||
} | ||
if (value === '' + expected) { | ||
if (value === '' + (expected: any)) { | ||
return expected; | ||
} | ||
return value; | ||
} | ||
} else if (node.hasAttribute(attributeName)) { | ||
if (shouldIgnoreValue(propertyInfo, expected)) { | ||
if (shouldTreatAttributeValueAsNull(name, expected, false)) { | ||
// We had an attribute but shouldn't have had one, so read it | ||
// for the error message. | ||
return node.getAttribute(attributeName); | ||
|
@@ -85,9 +67,9 @@ export function getValueForProperty(node, name, expected) { | |
stringValue = node.getAttribute(attributeName); | ||
} | ||
|
||
if (shouldIgnoreValue(propertyInfo, expected)) { | ||
if (shouldTreatAttributeValueAsNull(name, expected, false)) { | ||
return stringValue === null ? expected : stringValue; | ||
} else if (stringValue === '' + expected) { | ||
} else if (stringValue === '' + (expected: any)) { | ||
return expected; | ||
} else { | ||
return stringValue; | ||
|
@@ -102,7 +84,11 @@ export function getValueForProperty(node, name, expected) { | |
* The third argument is used as a hint of what the expected value is. Some | ||
* attributes have multiple equivalent values. | ||
*/ | ||
export function getValueForAttribute(node, name, expected) { | ||
export function getValueForAttribute( | ||
node: Element, | ||
name: string, | ||
expected: mixed, | ||
): mixed { | ||
if (__DEV__) { | ||
if (!isAttributeNameSafe(name)) { | ||
return; | ||
|
@@ -111,7 +97,7 @@ export function getValueForAttribute(node, name, expected) { | |
return expected === undefined ? undefined : null; | ||
} | ||
const value = node.getAttribute(name); | ||
if (value === '' + expected) { | ||
if (value === '' + (expected: any)) { | ||
return expected; | ||
} | ||
return value; | ||
|
@@ -125,84 +111,64 @@ export function getValueForAttribute(node, name, expected) { | |
* @param {string} name | ||
* @param {*} value | ||
*/ | ||
export function setValueForProperty(node, name, value) { | ||
const propertyInfo = getPropertyInfo(name); | ||
|
||
if (propertyInfo && shouldSetAttribute(name, value)) { | ||
if (shouldIgnoreValue(propertyInfo, value)) { | ||
deleteValueForProperty(node, name); | ||
return; | ||
} else if (propertyInfo.mustUseProperty) { | ||
// Contrary to `setAttribute`, object properties are properly | ||
// `toString`ed by IE8/9. | ||
node[propertyInfo.propertyName] = value; | ||
} else { | ||
const attributeName = propertyInfo.attributeName; | ||
const namespace = propertyInfo.attributeNamespace; | ||
// `setAttribute` with objects becomes only `[object]` in IE8/9, | ||
// ('' + value) makes it output the correct toString()-value. | ||
if (namespace) { | ||
node.setAttributeNS(namespace, attributeName, '' + value); | ||
} else if ( | ||
propertyInfo.hasBooleanValue || | ||
(propertyInfo.hasOverloadedBooleanValue && value === true) | ||
) { | ||
node.setAttribute(attributeName, ''); | ||
export function setValueForProperty( | ||
node: Element, | ||
name: string, | ||
value: mixed, | ||
isCustomComponentTag: boolean, | ||
) { | ||
if (shouldSkipAttribute(name, isCustomComponentTag)) { | ||
return; | ||
} | ||
const propertyInfo = isCustomComponentTag ? null : getPropertyInfo(name); | ||
if (shouldTreatAttributeValueAsNull(name, value, isCustomComponentTag)) { | ||
value = null; | ||
} | ||
// If the prop isn't in the special list, treat it as a simple attribute. | ||
if (!propertyInfo) { | ||
if (isAttributeNameSafe(name)) { | ||
const attributeName = name; | ||
if (value == null) { | ||
node.removeAttribute(attributeName); | ||
} else { | ||
node.setAttribute(attributeName, '' + value); | ||
node.setAttribute(attributeName, '' + (value: any)); | ||
} | ||
} | ||
} else { | ||
setValueForAttribute( | ||
node, | ||
name, | ||
shouldSetAttribute(name, value) ? value : null, | ||
); | ||
return; | ||
} | ||
} | ||
|
||
export function setValueForAttribute(node, name, value) { | ||
if (!isAttributeNameSafe(name)) { | ||
const { | ||
hasBooleanValue, | ||
hasOverloadedBooleanValue, | ||
mustUseProperty, | ||
} = propertyInfo; | ||
if (mustUseProperty) { | ||
const {propertyName} = propertyInfo; | ||
if (value === null) { | ||
(node: any)[propertyName] = hasBooleanValue ? false : ''; | ||
} else { | ||
// Contrary to `setAttribute`, object properties are properly | ||
// `toString`ed by IE8/9. | ||
(node: any)[propertyName] = value; | ||
} | ||
return; | ||
} | ||
if (value == null) { | ||
node.removeAttribute(name); | ||
// The rest are treated as attributes with special cases. | ||
const {attributeName, attributeNamespace} = propertyInfo; | ||
if (value === null) { | ||
node.removeAttribute(attributeName); | ||
} else { | ||
node.setAttribute(name, '' + value); | ||
} | ||
} | ||
|
||
/** | ||
* Deletes an attributes from a node. | ||
* | ||
* @param {DOMElement} node | ||
* @param {string} name | ||
*/ | ||
export function deleteValueForAttribute(node, name) { | ||
node.removeAttribute(name); | ||
} | ||
|
||
/** | ||
* Deletes the value for a property on a node. | ||
* | ||
* @param {DOMElement} node | ||
* @param {string} name | ||
*/ | ||
export function deleteValueForProperty(node, name) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you rename this test since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we care? There's plenty of internal names, even in test files (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using names of internal APIs that exist is different. In this case, it's referrencing a function that no longer exists. If I was looking to contribute and encountered that, I would be confused that the function didn't exist anywhere in the codebase. It's not critical, but since you remove it feels like it's in scope to fix any lingering references. If we're going to restructure the tests soon anyways, it probably doesn't matter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess what I'm referring to is that there are already plenty examples where internal names don't match tests. It used to be important for them to be in sync when unit test tested internals, but they don't anymore. I agree it's confusing, but IMO mismatches in test file names are even more confusing. Conceptually those tests are about deleting. I wouldn't move them into |
||
const propertyInfo = getPropertyInfo(name); | ||
if (propertyInfo) { | ||
if (propertyInfo.mustUseProperty) { | ||
const propName = propertyInfo.propertyName; | ||
if (propertyInfo.hasBooleanValue) { | ||
node[propName] = false; | ||
} else { | ||
node[propName] = ''; | ||
} | ||
let attributeValue; | ||
if (hasBooleanValue || (hasOverloadedBooleanValue && value === true)) { | ||
attributeValue = ''; | ||
} else { | ||
// `setAttribute` with objects becomes only `[object]` in IE8/9, | ||
// ('' + value) makes it output the correct toString()-value. | ||
attributeValue = '' + (value: any); | ||
} | ||
if (attributeNamespace) { | ||
node.setAttributeNS(attributeNamespace, attributeName, attributeValue); | ||
} else { | ||
node.removeAttribute(propertyInfo.attributeName); | ||
node.setAttribute(attributeName, attributeValue); | ||
} | ||
} else { | ||
node.removeAttribute(name); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All attributes that must use property are boolean values, so technically this check is unnecessary. We could add a new attribute type like
HAS_BOOLEAN_PROPERTY
to simplify this, and get rid of the bitmasks again (happy to do in a follow up)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's follow up. This diff is getting hard to deal with.