Refactor DOM attribute code (#11804)

* Harden tests around init/addition/update/removal of aliased attributes

I noticed some patterns weren't being tested.

* Call setValueForProperty() for null and undefined

The branching before the call is unnecessary because setValueForProperty() already
has an internal branch that delegates to deleteValueForProperty() for null and
undefined through the shouldIgnoreValue() check.

The goal is to start unifying these methods because their separation doesn't
reflect the current behavior (e.g. for unknown properties) anymore, and obscures
what actually happens with different inputs.

* Inline deleteValueForProperty() into setValueForProperty()

Now we don't read propertyInfo twice in this case.

I also dropped a few early returns. I added them a while ago when we had
Stack-only tracking of DOM operations, and some operations were being
counted twice because of how this code is structured. This isn't a problem
anymore (both because we don't track operations, and because I've just
inlined this method call).

* Inline deleteValueForAttribute() into setValueForAttribute()

The special cases for null and undefined already exist in setValueForAttribute().

* Delete some dead code

* Make setValueForAttribute() a branch of setValueForProperty()

Their naming is pretty confusing by now. For example setValueForProperty()
calls setValueForAttribute() when shouldSetAttribute() is false (!). I want
to refactor (as in, inline and then maybe factor it out differently) the relation
between them. For now, I'm consolidating the callers to use setValueForProperty().

* Make it more obvious where we skip and when we reset attributes

The naming of these methods is still very vague and conflicting in some cases.
Will need further work.

* Rewrite setValueForProperty() with early exits

This makes the flow clearer in my opinion.

* Move shouldIgnoreValue() into DOMProperty

It was previously duplicated.

It's also suspiciously similar in purpose to shouldTreatAttributeValueAsNull()
so I want to see if there is a way to unify them.

* Use more specific methods for testing validity

* Unify shouldTreatAttributeValueAsNull() and shouldIgnoreValue()

* Remove shouldSetAttribute()

Its naming was confusing and it was used all over the place instead of more specific checks.
Now that we only have one call site, we might as well inline and get rid of it.

* Remove unnecessary condition

* Remove another unnecessary condition

* Add Flow coverage

* Oops

* Fix lint (ESLint complains about Flow suppression)
This commit is contained in:
Dan Abramov 2017-12-08 20:42:24 +00:00 committed by GitHub
parent cda9fb0499
commit 47783e878d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 307 additions and 196 deletions

View File

@ -442,6 +442,97 @@ describe('ReactDOMComponent', () => {
expect(container.firstChild.className).toEqual('');
});
it('should not set null/undefined attributes', () => {
var container = document.createElement('div');
// Initial render.
ReactDOM.render(<img src={null} data-foo={undefined} />, container);
var node = container.firstChild;
expect(node.hasAttribute('src')).toBe(false);
expect(node.hasAttribute('data-foo')).toBe(false);
// Update in one direction.
ReactDOM.render(<img src={undefined} data-foo={null} />, container);
expect(node.hasAttribute('src')).toBe(false);
expect(node.hasAttribute('data-foo')).toBe(false);
// Update in another direction.
ReactDOM.render(<img src={null} data-foo={undefined} />, container);
expect(node.hasAttribute('src')).toBe(false);
expect(node.hasAttribute('data-foo')).toBe(false);
// Removal.
ReactDOM.render(<img />, container);
expect(node.hasAttribute('src')).toBe(false);
expect(node.hasAttribute('data-foo')).toBe(false);
// Addition.
ReactDOM.render(<img src={undefined} data-foo={null} />, container);
expect(node.hasAttribute('src')).toBe(false);
expect(node.hasAttribute('data-foo')).toBe(false);
});
it('should apply React-specific aliases to HTML elements', () => {
var container = document.createElement('div');
ReactDOM.render(<form acceptCharset="foo" />, container);
var node = container.firstChild;
// Test attribute initialization.
expect(node.getAttribute('accept-charset')).toBe('foo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute update.
ReactDOM.render(<form acceptCharset="boo" />, container);
expect(node.getAttribute('accept-charset')).toBe('boo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute removal by setting to null.
ReactDOM.render(<form acceptCharset={null} />, container);
expect(node.hasAttribute('accept-charset')).toBe(false);
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Restore.
ReactDOM.render(<form acceptCharset="foo" />, container);
expect(node.getAttribute('accept-charset')).toBe('foo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute removal by setting to undefined.
ReactDOM.render(<form acceptCharset={undefined} />, container);
expect(node.hasAttribute('accept-charset')).toBe(false);
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Restore.
ReactDOM.render(<form acceptCharset="foo" />, container);
expect(node.getAttribute('accept-charset')).toBe('foo');
expect(node.hasAttribute('acceptCharset')).toBe(false);
// Test attribute removal.
ReactDOM.render(<form />, container);
expect(node.hasAttribute('accept-charset')).toBe(false);
expect(node.hasAttribute('acceptCharset')).toBe(false);
});
it('should apply React-specific aliases to SVG elements', () => {
var container = document.createElement('div');
ReactDOM.render(<svg arabicForm="foo" />, container);
var node = container.firstChild;
// Test attribute initialization.
expect(node.getAttribute('arabic-form')).toBe('foo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute update.
ReactDOM.render(<svg arabicForm="boo" />, container);
expect(node.getAttribute('arabic-form')).toBe('boo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute removal by setting to null.
ReactDOM.render(<svg arabicForm={null} />, container);
expect(node.hasAttribute('arabic-form')).toBe(false);
expect(node.hasAttribute('arabicForm')).toBe(false);
// Restore.
ReactDOM.render(<svg arabicForm="foo" />, container);
expect(node.getAttribute('arabic-form')).toBe('foo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute removal by setting to undefined.
ReactDOM.render(<svg arabicForm={undefined} />, container);
expect(node.hasAttribute('arabic-form')).toBe(false);
expect(node.hasAttribute('arabicForm')).toBe(false);
// Restore.
ReactDOM.render(<svg arabicForm="foo" />, container);
expect(node.getAttribute('arabic-form')).toBe('foo');
expect(node.hasAttribute('arabicForm')).toBe(false);
// Test attribute removal.
ReactDOM.render(<svg />, container);
expect(node.hasAttribute('arabic-form')).toBe(false);
expect(node.hasAttribute('arabicForm')).toBe(false);
});
it('should properly update custom attributes on custom elements', () => {
const container = document.createElement('div');
ReactDOM.render(<some-custom-element foo="bar" />, container);
@ -451,6 +542,25 @@ describe('ReactDOMComponent', () => {
expect(node.getAttribute('bar')).toBe('buzz');
});
it('should not apply React-specific aliases to custom elements', () => {
var container = document.createElement('div');
ReactDOM.render(<some-custom-element arabicForm="foo" />, container);
var node = container.firstChild;
// Should not get transformed to arabic-form as SVG would be.
expect(node.getAttribute('arabicForm')).toBe('foo');
expect(node.hasAttribute('arabic-form')).toBe(false);
// Test attribute update.
ReactDOM.render(<some-custom-element arabicForm="boo" />, container);
expect(node.getAttribute('arabicForm')).toBe('boo');
// Test attribute removal and addition.
ReactDOM.render(<some-custom-element acceptCharset="buzz" />, container);
// Verify the previous attribute was removed.
expect(node.hasAttribute('arabicForm')).toBe(false);
// Should not get transformed to accept-charset as HTML would be.
expect(node.getAttribute('acceptCharset')).toBe('buzz');
expect(node.hasAttribute('accept-charset')).toBe(false);
});
it('should clear a single style prop when changing `style`', () => {
let styles = {display: 'none', color: 'red'};
const container = document.createElement('div');

View File

@ -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) {
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: any));
}
}
return;
}
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[propertyInfo.propertyName] = value;
(node: any)[propertyName] = value;
}
return;
}
// The rest are treated as attributes with special cases.
const {attributeName, attributeNamespace} = propertyInfo;
if (value === null) {
node.removeAttribute(attributeName);
} else {
let attributeValue;
if (hasBooleanValue || (hasOverloadedBooleanValue && value === true)) {
attributeValue = '';
} 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, '');
} else {
node.setAttribute(attributeName, '' + value);
}
attributeValue = '' + (value: any);
}
} else {
setValueForAttribute(
node,
name,
shouldSetAttribute(name, value) ? value : null,
);
return;
}
}
export function setValueForAttribute(node, name, value) {
if (!isAttributeNameSafe(name)) {
return;
}
if (value == null) {
node.removeAttribute(name);
} 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) {
const propertyInfo = getPropertyInfo(name);
if (propertyInfo) {
if (propertyInfo.mustUseProperty) {
const propName = propertyInfo.propertyName;
if (propertyInfo.hasBooleanValue) {
node[propName] = false;
} else {
node[propName] = '';
}
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.removeAttribute(propertyInfo.attributeName);
node.setAttribute(attributeName, attributeValue);
}
} else {
node.removeAttribute(name);
}
}

View File

@ -24,7 +24,11 @@ import setTextContent from './setTextContent';
import {listenTo, trapBubbledEvent} from '../events/ReactBrowserEventEmitter';
import * as CSSPropertyOperations from '../shared/CSSPropertyOperations';
import {Namespaces, getIntrinsicNamespace} from '../shared/DOMNamespaces';
import {getPropertyInfo, shouldSetAttribute} from '../shared/DOMProperty';
import {
getPropertyInfo,
shouldSkipAttribute,
shouldTreatAttributeValueAsNull,
} from '../shared/DOMProperty';
import assertValidProps from '../shared/assertValidProps';
import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType';
import isCustomComponent from '../shared/isCustomComponent';
@ -314,13 +318,13 @@ function setInitialDOMProperties(
}
ensureListeningTo(rootContainerElement, propKey);
}
} else if (isCustomComponentTag) {
DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp);
} else if (nextProp != null) {
// If we're updating to null or undefined, we should remove the property
// from the DOM node instead of inadvertently setting to a string. This
// brings us in line with the same behavior we have on initial render.
DOMPropertyOperations.setValueForProperty(domElement, propKey, nextProp);
DOMPropertyOperations.setValueForProperty(
domElement,
propKey,
nextProp,
isCustomComponentTag,
);
}
}
}
@ -341,23 +345,13 @@ function updateDOMProperties(
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else if (isCustomComponentTag) {
if (propValue != null) {
DOMPropertyOperations.setValueForAttribute(
domElement,
propKey,
propValue,
);
} else {
DOMPropertyOperations.deleteValueForAttribute(domElement, propKey);
}
} else if (propValue != null) {
DOMPropertyOperations.setValueForProperty(domElement, propKey, propValue);
} else {
// If we're updating to null or undefined, we should remove the property
// from the DOM node instead of inadvertently setting to a string. This
// brings us in line with the same behavior we have on initial render.
DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
DOMPropertyOperations.setValueForProperty(
domElement,
propKey,
propValue,
isCustomComponentTag,
);
}
}
}
@ -965,7 +959,11 @@ export function diffHydratedProperties(
}
ensureListeningTo(rootContainerElement, propKey);
}
} else if (__DEV__) {
} 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;
let propertyInfo;
@ -1010,7 +1008,14 @@ export function diffHydratedProperties(
if (nextProp !== serverValue) {
warnForPropDifference(propKey, serverValue, nextProp);
}
} else if (shouldSetAttribute(propKey, nextProp)) {
} else if (
!shouldSkipAttribute(propKey, isCustomComponentTag) &&
!shouldTreatAttributeValueAsNull(
propKey,
nextProp,
isCustomComponentTag,
)
) {
if ((propertyInfo = getPropertyInfo(propKey))) {
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(propertyInfo.attributeName);

View File

@ -133,7 +133,7 @@ export function updateChecked(element: Element, props: Object) {
const node = ((element: any): InputWithWrapperState);
const checked = props.checked;
if (checked != null) {
DOMPropertyOperations.setValueForProperty(node, 'checked', checked);
DOMPropertyOperations.setValueForProperty(node, 'checked', checked, false);
}
}

View File

@ -3,30 +3,20 @@
*
* 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,
shouldAttributeAcceptBooleanValue,
shouldSetAttribute,
isAttributeNameSafe,
shouldSkipAttribute,
shouldTreatAttributeValueAsNull,
} from '../shared/DOMProperty';
import quoteAttributeValueForBrowser from './quoteAttributeValueForBrowser';
// shouldIgnoreValue() is currently duplicated in DOMPropertyOperations.
// 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.
*/
@ -37,11 +27,11 @@ function shouldIgnoreValue(propertyInfo, value) {
* @param {string} id Unescaped ID.
* @return {string} Markup string.
*/
export function createMarkupForID(id) {
export function createMarkupForID(id: string): string {
return ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id);
}
export function createMarkupForRoot() {
export function createMarkupForRoot(): string {
return ROOT_ATTRIBUTE_NAME + '=""';
}
@ -52,31 +42,27 @@ export function createMarkupForRoot() {
* @param {*} value
* @return {?string} Markup string, or null if the property was invalid.
*/
export function createMarkupForProperty(name, value) {
export function createMarkupForProperty(name: string, value: mixed): string {
if (name !== 'style' && shouldSkipAttribute(name, false)) {
return '';
}
if (shouldTreatAttributeValueAsNull(name, value, false)) {
return '';
}
const propertyInfo = getPropertyInfo(name);
if (propertyInfo) {
if (shouldIgnoreValue(propertyInfo, value)) {
return '';
}
const attributeName = propertyInfo.attributeName;
if (
propertyInfo.hasBooleanValue ||
(propertyInfo.hasOverloadedBooleanValue && value === true)
) {
return attributeName + '=""';
} else if (
typeof value !== 'boolean' ||
shouldAttributeAcceptBooleanValue(name)
) {
} else {
return attributeName + '=' + quoteAttributeValueForBrowser(value);
}
} else if (shouldSetAttribute(name, value)) {
if (value == null) {
return '';
}
} else {
return name + '=' + quoteAttributeValueForBrowser(value);
}
return null;
}
/**
@ -86,7 +72,10 @@ export function createMarkupForProperty(name, value) {
* @param {*} value
* @return {string} Markup string, or empty string if the property was invalid.
*/
export function createMarkupForCustomAttribute(name, value) {
export function createMarkupForCustomAttribute(
name: string,
value: mixed,
): string {
if (!isAttributeNameSafe(name) || value == null) {
return '';
}

View File

@ -3,10 +3,24 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import warning from 'fbjs/lib/warning';
type PropertyInfo = {|
attributeName: string,
attributeNamespace: string | null,
propertyName: string,
mustUseProperty: boolean,
hasBooleanValue: boolean,
hasNumericValue: boolean,
hasPositiveNumericValue: boolean,
hasOverloadedBooleanValue: boolean,
hasStringBooleanValue: boolean,
|};
// These attributes should be all lowercase to allow for
// case insensitive checks
const RESERVED_PROPS = {
@ -54,7 +68,7 @@ function injectDOMPropertyConfig(domPropertyConfig) {
const lowerCased = propName.toLowerCase();
const propConfig = Properties[propName];
const propertyInfo = {
const propertyInfo: PropertyInfo = {
attributeName: lowerCased,
attributeNamespace: null,
propertyName: propName,
@ -118,7 +132,7 @@ export const VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
const illegalAttributeNameCache = {};
const validatedAttributeNameCache = {};
export function isAttributeNameSafe(attributeName) {
export function isAttributeNameSafe(attributeName: string): boolean {
if (validatedAttributeNameCache.hasOwnProperty(attributeName)) {
return true;
}
@ -163,12 +177,14 @@ export function isAttributeNameSafe(attributeName) {
*/
export const properties = {};
/**
* Checks whether a property name is a writeable attribute.
* @method
*/
export function shouldSetAttribute(name, value) {
export function shouldSkipAttribute(
name: string,
isCustomComponentTag: boolean,
): boolean {
if (isReservedProp(name)) {
return true;
}
if (isCustomComponentTag) {
return false;
}
if (
@ -176,43 +192,69 @@ export function shouldSetAttribute(name, value) {
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return true;
}
return false;
}
export function isBadlyTypedAttributeValue(
name: string,
value: mixed,
isCustomComponentTag: boolean,
): boolean {
if (isReservedProp(name)) {
return false;
}
if (value === null) {
return true;
}
switch (typeof value) {
case 'boolean':
return shouldAttributeAcceptBooleanValue(name);
case 'undefined':
case 'number':
case 'string':
case 'object':
case 'function':
// $FlowIssue symbol is perfectly valid here
case 'symbol': // eslint-disable-line
return true;
case 'boolean':
break;
default:
// function, symbol
return false;
}
}
export function getPropertyInfo(name) {
return properties.hasOwnProperty(name) ? properties[name] : null;
}
export function shouldAttributeAcceptBooleanValue(name) {
if (isReservedProp(name)) {
return true;
if (isCustomComponentTag) {
return false;
}
let propertyInfo = getPropertyInfo(name);
if (propertyInfo) {
return (
return !(
propertyInfo.hasBooleanValue ||
propertyInfo.hasStringBooleanValue ||
propertyInfo.hasOverloadedBooleanValue
);
}
const prefix = name.toLowerCase().slice(0, 5);
return prefix === 'data-' || prefix === 'aria-';
return prefix !== 'data-' && prefix !== 'aria-';
}
export function shouldTreatAttributeValueAsNull(
name: string,
value: mixed,
isCustomComponentTag: boolean,
): boolean {
if (value === null || typeof value === 'undefined') {
return true;
}
const propertyInfo = getPropertyInfo(name);
if (propertyInfo) {
if (propertyInfo.hasBooleanValue) {
return !value;
} else if (propertyInfo.hasOverloadedBooleanValue) {
return value === false;
} else if (propertyInfo.hasNumericValue && isNaN(value)) {
return true;
} else if (propertyInfo.hasPositiveNumericValue && (value: any) < 1) {
return true;
}
}
return isBadlyTypedAttributeValue(name, value, isCustomComponentTag);
}
export function getPropertyInfo(name: string): PropertyInfo | null {
return properties.hasOwnProperty(name) ? properties[name] : null;
}
/**
@ -224,7 +266,7 @@ export function shouldAttributeAcceptBooleanValue(name) {
* @param {string} name
* @return {boolean} If the name is within reserved props
*/
export function isReservedProp(name) {
export function isReservedProp(name: string): boolean {
return RESERVED_PROPS.hasOwnProperty(name);
}

View File

@ -15,8 +15,7 @@ import warning from 'fbjs/lib/warning';
import {
ATTRIBUTE_NAME_CHAR,
isReservedProp,
shouldAttributeAcceptBooleanValue,
shouldSetAttribute,
isBadlyTypedAttributeValue,
} from './DOMProperty';
import isCustomComponent from './isCustomComponent';
import possibleStandardNames from './possibleStandardNames';
@ -191,7 +190,7 @@ if (__DEV__) {
if (
typeof value === 'boolean' &&
!shouldAttributeAcceptBooleanValue(name)
isBadlyTypedAttributeValue(name, value, false)
) {
if (value) {
warning(
@ -235,7 +234,7 @@ if (__DEV__) {
}
// Warn when a known attribute is a bad type
if (!shouldSetAttribute(name, value)) {
if (isBadlyTypedAttributeValue(name, value, false)) {
warnedProperties[name] = true;
return false;
}