mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Refactor DOM attribute code (take two) (#11815)
* 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) * Fix treatment of Symbol/Function values on boolean attributes They weren't being properly skipped because of the early return. I added tests for this case. * Avoid getPropertyInfo() calls I think this PR looks worse on benchmarks because we have to read propertyInfo in different places. Originally I tried to get rid of propertyInfo, but looks like it's important for performance after all. So now I'm going into the opposite direction, and precompute propertyInfo as early as possible, and then just pass it around. This way we can avoid extra lookups but keep functions nice and modular. * Pass propertyInfo as argument to getValueForProperty() It always exists because this function is only called for known properties. * Make it clearer this branch is boolean-specific I wrote this and then got confused myself. * Memoize whether propertyInfo accepts boolean value Since we run these checks for all booleans, might as well remember it. * Fix a crash when numeric property is given a Symbol * Record attribute table The changes reflect that SSR doesn't crash with symbols anymore (and just warns, consistently with the client). * Refactor attribute initialization Instead of using flags, explicitly group similar attributes/properties. * Optimization: we know built-in attributes are never invalid * Use strict comparison * Rename methods for clarity * Lint nit * Minor tweaks * Document all the different attribute types
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
|
||||
+56
-3
@@ -62,6 +62,16 @@ describe('ReactDOMServerIntegration', () => {
|
||||
const e = await render(<div width={null} />);
|
||||
expect(e.hasAttribute('width')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no string prop with function value', async render => {
|
||||
const e = await render(<div width={function() {}} />, 1);
|
||||
expect(e.hasAttribute('width')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no string prop with symbol value', async render => {
|
||||
const e = await render(<div width={Symbol('foo')} />, 1);
|
||||
expect(e.hasAttribute('width')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean properties', function() {
|
||||
@@ -122,6 +132,16 @@ describe('ReactDOMServerIntegration', () => {
|
||||
const e = await render(<div hidden={null} />);
|
||||
expect(e.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no boolean prop with function value', async render => {
|
||||
const e = await render(<div hidden={function() {}} />, 1);
|
||||
expect(e.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no boolean prop with symbol value', async render => {
|
||||
const e = await render(<div hidden={Symbol('foo')} />, 1);
|
||||
expect(e.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('download property (combined boolean/string attribute)', function() {
|
||||
@@ -164,6 +184,16 @@ describe('ReactDOMServerIntegration', () => {
|
||||
const e = await render(<div download={undefined} />);
|
||||
expect(e.hasAttribute('download')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no download prop with function value', async render => {
|
||||
const e = await render(<div download={function() {}} />, 1);
|
||||
expect(e.hasAttribute('download')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no download prop with symbol value', async render => {
|
||||
const e = await render(<div download={Symbol('foo')} />, 1);
|
||||
expect(e.hasAttribute('download')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('className property', function() {
|
||||
@@ -257,6 +287,11 @@ describe('ReactDOMServerIntegration', () => {
|
||||
},
|
||||
);
|
||||
|
||||
itRenders('numeric property with zero value', async render => {
|
||||
const e = await render(<ol start={0} />);
|
||||
expect(e.getAttribute('start')).toBe('0');
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'no positive numeric property with zero value',
|
||||
async render => {
|
||||
@@ -265,9 +300,27 @@ describe('ReactDOMServerIntegration', () => {
|
||||
},
|
||||
);
|
||||
|
||||
itRenders('numeric property with zero value', async render => {
|
||||
const e = await render(<ol start={0} />);
|
||||
expect(e.getAttribute('start')).toBe('0');
|
||||
itRenders('no numeric prop with function value', async render => {
|
||||
const e = await render(<ol start={function() {}} />, 1);
|
||||
expect(e.hasAttribute('start')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders('no numeric prop with symbol value', async render => {
|
||||
const e = await render(<ol start={Symbol('foo')} />, 1);
|
||||
expect(e.hasAttribute('start')).toBe(false);
|
||||
});
|
||||
|
||||
itRenders(
|
||||
'no positive numeric prop with function value',
|
||||
async render => {
|
||||
const e = await render(<input size={function() {}} />, 1);
|
||||
expect(e.hasAttribute('size')).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
itRenders('no positive numeric prop with symbol value', async render => {
|
||||
const e = await render(<input size={Symbol('foo')} />, 1);
|
||||
expect(e.hasAttribute('size')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+104
-138
@@ -3,95 +3,79 @@
|
||||
*
|
||||
* 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,
|
||||
shouldIgnoreAttribute,
|
||||
shouldRemoveAttribute,
|
||||
isAttributeNameSafe,
|
||||
BOOLEAN,
|
||||
OVERLOADED_BOOLEAN,
|
||||
} 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, '');
|
||||
}
|
||||
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, name, expected) {
|
||||
export function getValueForProperty(
|
||||
node: Element,
|
||||
name: string,
|
||||
expected: mixed,
|
||||
propertyInfo: PropertyInfo,
|
||||
): mixed {
|
||||
if (__DEV__) {
|
||||
const propertyInfo = getPropertyInfo(name);
|
||||
if (propertyInfo) {
|
||||
if (propertyInfo.mustUseProperty) {
|
||||
return node[propertyInfo.propertyName];
|
||||
} else {
|
||||
const attributeName = propertyInfo.attributeName;
|
||||
if (propertyInfo.mustUseProperty) {
|
||||
const {propertyName} = propertyInfo;
|
||||
return (node: any)[propertyName];
|
||||
} else {
|
||||
const attributeName = propertyInfo.attributeName;
|
||||
|
||||
let stringValue = null;
|
||||
let stringValue = null;
|
||||
|
||||
if (propertyInfo.hasOverloadedBooleanValue) {
|
||||
if (node.hasAttribute(attributeName)) {
|
||||
const value = node.getAttribute(attributeName);
|
||||
if (value === '') {
|
||||
return true;
|
||||
}
|
||||
if (shouldIgnoreValue(propertyInfo, expected)) {
|
||||
return value;
|
||||
}
|
||||
if (value === '' + expected) {
|
||||
return expected;
|
||||
}
|
||||
if (propertyInfo.type === OVERLOADED_BOOLEAN) {
|
||||
if (node.hasAttribute(attributeName)) {
|
||||
const value = node.getAttribute(attributeName);
|
||||
if (value === '') {
|
||||
return true;
|
||||
}
|
||||
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
|
||||
return value;
|
||||
}
|
||||
} else if (node.hasAttribute(attributeName)) {
|
||||
if (shouldIgnoreValue(propertyInfo, expected)) {
|
||||
// We had an attribute but shouldn't have had one, so read it
|
||||
// for the error message.
|
||||
return node.getAttribute(attributeName);
|
||||
}
|
||||
if (propertyInfo.hasBooleanValue) {
|
||||
// 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.
|
||||
if (value === '' + (expected: any)) {
|
||||
return expected;
|
||||
}
|
||||
// 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.
|
||||
stringValue = node.getAttribute(attributeName);
|
||||
return value;
|
||||
}
|
||||
|
||||
if (shouldIgnoreValue(propertyInfo, expected)) {
|
||||
return stringValue === null ? expected : stringValue;
|
||||
} else if (stringValue === '' + expected) {
|
||||
} else if (node.hasAttribute(attributeName)) {
|
||||
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
|
||||
// We had an attribute but shouldn't have had one, so read it
|
||||
// for the error message.
|
||||
return node.getAttribute(attributeName);
|
||||
}
|
||||
if (propertyInfo.type === BOOLEAN) {
|
||||
// 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.
|
||||
return expected;
|
||||
} else {
|
||||
return stringValue;
|
||||
}
|
||||
// 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.
|
||||
stringValue = node.getAttribute(attributeName);
|
||||
}
|
||||
|
||||
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
|
||||
return stringValue === null ? expected : stringValue;
|
||||
} else if (stringValue === '' + (expected: any)) {
|
||||
return expected;
|
||||
} else {
|
||||
return stringValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +86,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 +99,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 +113,62 @@ export function getValueForAttribute(node, name, expected) {
|
||||
* @param {string} name
|
||||
* @param {*} value
|
||||
*/
|
||||
export function setValueForProperty(node, name, value) {
|
||||
export function setValueForProperty(
|
||||
node: Element,
|
||||
name: string,
|
||||
value: mixed,
|
||||
isCustomComponentTag: boolean,
|
||||
) {
|
||||
const propertyInfo = getPropertyInfo(name);
|
||||
|
||||
if (propertyInfo && shouldSetAttribute(name, value)) {
|
||||
if (shouldIgnoreValue(propertyInfo, value)) {
|
||||
deleteValueForProperty(node, name);
|
||||
return;
|
||||
} else if (propertyInfo.mustUseProperty) {
|
||||
if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
|
||||
return;
|
||||
}
|
||||
if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
|
||||
value = null;
|
||||
}
|
||||
// If the prop isn't in the special list, treat it as a simple attribute.
|
||||
if (isCustomComponentTag || propertyInfo === null) {
|
||||
if (isAttributeNameSafe(name)) {
|
||||
const attributeName = name;
|
||||
if (value === null) {
|
||||
node.removeAttribute(attributeName);
|
||||
} else {
|
||||
node.setAttribute(attributeName, '' + (value: any));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const {mustUseProperty} = propertyInfo;
|
||||
if (mustUseProperty) {
|
||||
const {propertyName} = propertyInfo;
|
||||
if (value === null) {
|
||||
const {type} = propertyInfo;
|
||||
(node: any)[propertyName] = type === BOOLEAN ? 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 {
|
||||
const {type} = propertyInfo;
|
||||
let attributeValue;
|
||||
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && 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);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-27
@@ -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,
|
||||
shouldIgnoreAttribute,
|
||||
shouldRemoveAttribute,
|
||||
} 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,10 +959,14 @@ 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;
|
||||
const propertyInfo = getPropertyInfo(propKey);
|
||||
if (suppressHydrationWarning) {
|
||||
// Don't bother comparing. We're ignoring all these warnings.
|
||||
} else if (
|
||||
@@ -1010,14 +1008,23 @@ export function diffHydratedProperties(
|
||||
if (nextProp !== serverValue) {
|
||||
warnForPropDifference(propKey, serverValue, nextProp);
|
||||
}
|
||||
} else if (shouldSetAttribute(propKey, nextProp)) {
|
||||
if ((propertyInfo = getPropertyInfo(propKey))) {
|
||||
} else if (
|
||||
!shouldIgnoreAttribute(propKey, propertyInfo, isCustomComponentTag) &&
|
||||
!shouldRemoveAttribute(
|
||||
propKey,
|
||||
nextProp,
|
||||
propertyInfo,
|
||||
isCustomComponentTag,
|
||||
)
|
||||
) {
|
||||
if (propertyInfo !== null) {
|
||||
// $FlowFixMe - Should be inferred as not undefined.
|
||||
extraAttributeNames.delete(propertyInfo.attributeName);
|
||||
serverValue = DOMPropertyOperations.getValueForProperty(
|
||||
domElement,
|
||||
propKey,
|
||||
nextProp,
|
||||
propertyInfo,
|
||||
);
|
||||
} else {
|
||||
let ownNamespace = parentNamespace;
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-35
@@ -3,30 +3,22 @@
|
||||
*
|
||||
* 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,
|
||||
BOOLEAN,
|
||||
OVERLOADED_BOOLEAN,
|
||||
getPropertyInfo,
|
||||
shouldAttributeAcceptBooleanValue,
|
||||
shouldSetAttribute,
|
||||
isAttributeNameSafe,
|
||||
shouldIgnoreAttribute,
|
||||
shouldRemoveAttribute,
|
||||
} 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 +29,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 +44,25 @@ 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 {
|
||||
const propertyInfo = getPropertyInfo(name);
|
||||
if (propertyInfo) {
|
||||
if (shouldIgnoreValue(propertyInfo, value)) {
|
||||
return '';
|
||||
}
|
||||
if (name !== 'style' && shouldIgnoreAttribute(name, propertyInfo, false)) {
|
||||
return '';
|
||||
}
|
||||
if (shouldRemoveAttribute(name, value, propertyInfo, false)) {
|
||||
return '';
|
||||
}
|
||||
if (propertyInfo !== null) {
|
||||
const attributeName = propertyInfo.attributeName;
|
||||
if (
|
||||
propertyInfo.hasBooleanValue ||
|
||||
(propertyInfo.hasOverloadedBooleanValue && value === true)
|
||||
) {
|
||||
const {type} = propertyInfo;
|
||||
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && 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 '';
|
||||
}
|
||||
|
||||
+335
-285
@@ -3,104 +3,55 @@
|
||||
*
|
||||
* 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';
|
||||
|
||||
// These attributes should be all lowercase to allow for
|
||||
// case insensitive checks
|
||||
const RESERVED_PROPS = {
|
||||
children: true,
|
||||
dangerouslySetInnerHTML: true,
|
||||
// TODO: This prevents the assignment of defaultValue to regular
|
||||
// elements (not just inputs). Now that ReactDOMInput assigns to the
|
||||
// defaultValue property -- do we need this?
|
||||
defaultValue: true,
|
||||
defaultChecked: true,
|
||||
innerHTML: true,
|
||||
suppressContentEditableWarning: true,
|
||||
suppressHydrationWarning: true,
|
||||
style: true,
|
||||
};
|
||||
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
function checkMask(value, bitmask) {
|
||||
return (value & bitmask) === bitmask;
|
||||
}
|
||||
// A reserved attribute.
|
||||
// It is handled by React separately and shouldn't be written to the DOM.
|
||||
export const RESERVED = 0;
|
||||
|
||||
const MUST_USE_PROPERTY = 0x1;
|
||||
const HAS_BOOLEAN_VALUE = 0x4;
|
||||
const HAS_NUMERIC_VALUE = 0x8;
|
||||
const HAS_POSITIVE_NUMERIC_VALUE = 0x10 | 0x8;
|
||||
const HAS_OVERLOADED_BOOLEAN_VALUE = 0x20;
|
||||
const HAS_STRING_BOOLEAN_VALUE = 0x40;
|
||||
// A simple string attribute.
|
||||
// Attributes that aren't in the whitelist are presumed to have this type.
|
||||
export const STRING = 1;
|
||||
|
||||
function injectDOMPropertyConfig(domPropertyConfig) {
|
||||
const Properties = domPropertyConfig.Properties || {};
|
||||
const DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {};
|
||||
const DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
|
||||
// A string attribute that accepts booleans in React. In HTML, these are called
|
||||
// "enumerated" attributes with "true" and "false" as possible values.
|
||||
// When true, it should be set to a "true" string.
|
||||
// When false, it should be set to a "false" string.
|
||||
export const BOOLEANISH_STRING = 2;
|
||||
|
||||
for (const propName in Properties) {
|
||||
if (__DEV__) {
|
||||
warning(
|
||||
!properties.hasOwnProperty(propName),
|
||||
"injectDOMPropertyConfig(...): You're trying to inject DOM property " +
|
||||
"'%s' which has already been injected. You may be accidentally " +
|
||||
'injecting the same DOM property config twice, or you may be ' +
|
||||
'injecting two configs that have conflicting property names.',
|
||||
propName,
|
||||
);
|
||||
}
|
||||
// A real boolean attribute.
|
||||
// When true, it should be present (set either to an empty string or its name).
|
||||
// When false, it should be omitted.
|
||||
export const BOOLEAN = 3;
|
||||
|
||||
const lowerCased = propName.toLowerCase();
|
||||
const propConfig = Properties[propName];
|
||||
// An attribute that can be used as a flag as well as with a value.
|
||||
// When true, it should be present (set either to an empty string or its name).
|
||||
// When false, it should be omitted.
|
||||
// For any other value, should be present with that value.
|
||||
export const OVERLOADED_BOOLEAN = 4;
|
||||
|
||||
const propertyInfo = {
|
||||
attributeName: lowerCased,
|
||||
attributeNamespace: null,
|
||||
propertyName: propName,
|
||||
// An attribute that must be numeric or parse as a numeric.
|
||||
// When falsy, it should be removed.
|
||||
export const NUMERIC = 5;
|
||||
|
||||
mustUseProperty: checkMask(propConfig, MUST_USE_PROPERTY),
|
||||
hasBooleanValue: checkMask(propConfig, HAS_BOOLEAN_VALUE),
|
||||
hasNumericValue: checkMask(propConfig, HAS_NUMERIC_VALUE),
|
||||
hasPositiveNumericValue: checkMask(
|
||||
propConfig,
|
||||
HAS_POSITIVE_NUMERIC_VALUE,
|
||||
),
|
||||
hasOverloadedBooleanValue: checkMask(
|
||||
propConfig,
|
||||
HAS_OVERLOADED_BOOLEAN_VALUE,
|
||||
),
|
||||
hasStringBooleanValue: checkMask(propConfig, HAS_STRING_BOOLEAN_VALUE),
|
||||
};
|
||||
if (__DEV__) {
|
||||
warning(
|
||||
propertyInfo.hasBooleanValue +
|
||||
propertyInfo.hasNumericValue +
|
||||
propertyInfo.hasOverloadedBooleanValue <=
|
||||
1,
|
||||
'DOMProperty: Value can be one of boolean, overloaded boolean, or ' +
|
||||
'numeric value, but not a combination: %s',
|
||||
propName,
|
||||
);
|
||||
}
|
||||
// An attribute that must be positive numeric or parse as a positive numeric.
|
||||
// When falsy, it should be removed.
|
||||
export const POSITIVE_NUMERIC = 6;
|
||||
|
||||
if (DOMAttributeNames.hasOwnProperty(propName)) {
|
||||
const attributeName = DOMAttributeNames[propName];
|
||||
|
||||
propertyInfo.attributeName = attributeName;
|
||||
}
|
||||
|
||||
if (DOMAttributeNamespaces.hasOwnProperty(propName)) {
|
||||
propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName];
|
||||
}
|
||||
|
||||
// Downcase references to whitelist properties to check for membership
|
||||
// without case-sensitivity. This allows the whitelist to pick up
|
||||
// `allowfullscreen`, which should be written using the property configuration
|
||||
// for `allowFullscreen`
|
||||
properties[propName] = propertyInfo;
|
||||
}
|
||||
}
|
||||
export type PropertyInfo = {|
|
||||
+acceptsBooleans: boolean,
|
||||
+attributeName: string,
|
||||
+attributeNamespace: string | null,
|
||||
+mustUseProperty: boolean,
|
||||
+propertyName: string,
|
||||
+type: PropertyType,
|
||||
|};
|
||||
|
||||
/* eslint-disable max-len */
|
||||
export const ATTRIBUTE_NAME_START_CHAR =
|
||||
@@ -118,7 +69,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;
|
||||
}
|
||||
@@ -136,39 +87,15 @@ export function isAttributeNameSafe(attributeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map from property "standard name" to an object with info about how to set
|
||||
* the property in the DOM. Each object contains:
|
||||
*
|
||||
* attributeName:
|
||||
* Used when rendering markup or with `*Attribute()`.
|
||||
* attributeNamespace
|
||||
* propertyName:
|
||||
* Used on DOM node instances. (This includes properties that mutate due to
|
||||
* external factors.)
|
||||
* mustUseProperty:
|
||||
* Whether the property must be accessed and mutated as an object property.
|
||||
* hasBooleanValue:
|
||||
* Whether the property should be removed when set to a falsey value.
|
||||
* hasNumericValue:
|
||||
* Whether the property must be numeric or parse as a numeric and should be
|
||||
* removed when set to a falsey value.
|
||||
* hasPositiveNumericValue:
|
||||
* Whether the property must be positive numeric or parse as a positive
|
||||
* numeric and should be removed when set to a falsey value.
|
||||
* hasOverloadedBooleanValue:
|
||||
* Whether the property can be used as a flag as well as with a value.
|
||||
* Removed when strictly equal to false; present without a value when
|
||||
* strictly equal to true; present with a value otherwise.
|
||||
*/
|
||||
export const properties = {};
|
||||
|
||||
/**
|
||||
* Checks whether a property name is a writeable attribute.
|
||||
* @method
|
||||
*/
|
||||
export function shouldSetAttribute(name, value) {
|
||||
if (isReservedProp(name)) {
|
||||
export function shouldIgnoreAttribute(
|
||||
name: string,
|
||||
propertyInfo: PropertyInfo | null,
|
||||
isCustomComponentTag: boolean,
|
||||
): boolean {
|
||||
if (propertyInfo !== null) {
|
||||
return propertyInfo.type === RESERVED;
|
||||
}
|
||||
if (isCustomComponentTag) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
@@ -176,147 +103,266 @@ export function shouldSetAttribute(name, value) {
|
||||
(name[0] === 'o' || name[0] === 'O') &&
|
||||
(name[1] === 'n' || name[1] === 'N')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldRemoveAttributeWithWarning(
|
||||
name: string,
|
||||
value: mixed,
|
||||
propertyInfo: PropertyInfo | null,
|
||||
isCustomComponentTag: boolean,
|
||||
): boolean {
|
||||
if (propertyInfo !== null && propertyInfo.type === RESERVED) {
|
||||
return false;
|
||||
}
|
||||
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': {
|
||||
if (isCustomComponentTag) {
|
||||
return false;
|
||||
}
|
||||
if (propertyInfo !== null) {
|
||||
return !propertyInfo.acceptsBooleans;
|
||||
} else {
|
||||
const prefix = name.toLowerCase().slice(0, 5);
|
||||
return prefix !== 'data-' && prefix !== 'aria-';
|
||||
}
|
||||
}
|
||||
default:
|
||||
// function, symbol
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPropertyInfo(name) {
|
||||
export function shouldRemoveAttribute(
|
||||
name: string,
|
||||
value: mixed,
|
||||
propertyInfo: PropertyInfo | null,
|
||||
isCustomComponentTag: boolean,
|
||||
): boolean {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
shouldRemoveAttributeWithWarning(
|
||||
name,
|
||||
value,
|
||||
propertyInfo,
|
||||
isCustomComponentTag,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (propertyInfo !== null) {
|
||||
switch (propertyInfo.type) {
|
||||
case BOOLEAN:
|
||||
return !value;
|
||||
case OVERLOADED_BOOLEAN:
|
||||
return value === false;
|
||||
case NUMERIC:
|
||||
return isNaN(value);
|
||||
case POSITIVE_NUMERIC:
|
||||
return isNaN(value) || (value: any) < 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPropertyInfo(name: string): PropertyInfo | null {
|
||||
return properties.hasOwnProperty(name) ? properties[name] : null;
|
||||
}
|
||||
|
||||
export function shouldAttributeAcceptBooleanValue(name) {
|
||||
if (isReservedProp(name)) {
|
||||
return true;
|
||||
}
|
||||
let propertyInfo = getPropertyInfo(name);
|
||||
if (propertyInfo) {
|
||||
return (
|
||||
propertyInfo.hasBooleanValue ||
|
||||
propertyInfo.hasStringBooleanValue ||
|
||||
propertyInfo.hasOverloadedBooleanValue
|
||||
);
|
||||
}
|
||||
const prefix = name.toLowerCase().slice(0, 5);
|
||||
return prefix === 'data-' || prefix === 'aria-';
|
||||
function PropertyInfoRecord(
|
||||
name: string,
|
||||
type: PropertyType,
|
||||
mustUseProperty: boolean,
|
||||
attributeName: string,
|
||||
attributeNamespace: string | null,
|
||||
) {
|
||||
this.acceptsBooleans =
|
||||
type === BOOLEANISH_STRING ||
|
||||
type === BOOLEAN ||
|
||||
type === OVERLOADED_BOOLEAN;
|
||||
this.attributeName = attributeName;
|
||||
this.attributeNamespace = attributeNamespace;
|
||||
this.mustUseProperty = mustUseProperty;
|
||||
this.propertyName = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a property name is within the list of properties
|
||||
* reserved for internal React operations. These properties should
|
||||
* not be set on an HTML element.
|
||||
*
|
||||
* @private
|
||||
* @param {string} name
|
||||
* @return {boolean} If the name is within reserved props
|
||||
*/
|
||||
export function isReservedProp(name) {
|
||||
return RESERVED_PROPS.hasOwnProperty(name);
|
||||
}
|
||||
// When adding attributes to this list, be sure to also add them to
|
||||
// the `possibleStandardNames` module to ensure casing and incorrect
|
||||
// name warnings.
|
||||
const properties = {};
|
||||
|
||||
const HTMLDOMPropertyConfig = {
|
||||
// When adding attributes to this list, be sure to also add them to
|
||||
// the `possibleStandardNames` module to ensure casing and incorrect
|
||||
// name warnings.
|
||||
Properties: {
|
||||
allowFullScreen: HAS_BOOLEAN_VALUE,
|
||||
// specifies target context for links with `preload` type
|
||||
async: HAS_BOOLEAN_VALUE,
|
||||
// Note: there is a special case that prevents it from being written to the DOM
|
||||
// on the client side because the browsers are inconsistent. Instead we call focus().
|
||||
autoFocus: HAS_BOOLEAN_VALUE,
|
||||
autoPlay: HAS_BOOLEAN_VALUE,
|
||||
capture: HAS_OVERLOADED_BOOLEAN_VALUE,
|
||||
checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
cols: HAS_POSITIVE_NUMERIC_VALUE,
|
||||
contentEditable: HAS_STRING_BOOLEAN_VALUE,
|
||||
controls: HAS_BOOLEAN_VALUE,
|
||||
default: HAS_BOOLEAN_VALUE,
|
||||
defer: HAS_BOOLEAN_VALUE,
|
||||
disabled: HAS_BOOLEAN_VALUE,
|
||||
download: HAS_OVERLOADED_BOOLEAN_VALUE,
|
||||
draggable: HAS_STRING_BOOLEAN_VALUE,
|
||||
formNoValidate: HAS_BOOLEAN_VALUE,
|
||||
hidden: HAS_BOOLEAN_VALUE,
|
||||
loop: HAS_BOOLEAN_VALUE,
|
||||
// Caution; `option.selected` is not updated if `select.multiple` is
|
||||
// disabled with `removeAttribute`.
|
||||
multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
noValidate: HAS_BOOLEAN_VALUE,
|
||||
open: HAS_BOOLEAN_VALUE,
|
||||
playsInline: HAS_BOOLEAN_VALUE,
|
||||
readOnly: HAS_BOOLEAN_VALUE,
|
||||
required: HAS_BOOLEAN_VALUE,
|
||||
reversed: HAS_BOOLEAN_VALUE,
|
||||
rows: HAS_POSITIVE_NUMERIC_VALUE,
|
||||
rowSpan: HAS_NUMERIC_VALUE,
|
||||
scoped: HAS_BOOLEAN_VALUE,
|
||||
seamless: HAS_BOOLEAN_VALUE,
|
||||
selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
size: HAS_POSITIVE_NUMERIC_VALUE,
|
||||
start: HAS_NUMERIC_VALUE,
|
||||
// support for projecting regular DOM Elements via V1 named slots ( shadow dom )
|
||||
span: HAS_POSITIVE_NUMERIC_VALUE,
|
||||
spellCheck: HAS_STRING_BOOLEAN_VALUE,
|
||||
// Style must be explicitly set in the attribute list. React components
|
||||
// expect a style object
|
||||
style: 0,
|
||||
// Keep it in the whitelist because it is case-sensitive for SVG.
|
||||
tabIndex: 0,
|
||||
// itemScope is for for Microdata support.
|
||||
// See http://schema.org/docs/gs.html
|
||||
itemScope: HAS_BOOLEAN_VALUE,
|
||||
// These attributes must stay in the white-list because they have
|
||||
// different attribute names (see DOMAttributeNames below)
|
||||
acceptCharset: 0,
|
||||
className: 0,
|
||||
htmlFor: 0,
|
||||
httpEquiv: 0,
|
||||
// Set the string boolean flag to allow the behavior
|
||||
value: HAS_STRING_BOOLEAN_VALUE,
|
||||
},
|
||||
DOMAttributeNames: {
|
||||
acceptCharset: 'accept-charset',
|
||||
className: 'class',
|
||||
htmlFor: 'for',
|
||||
httpEquiv: 'http-equiv',
|
||||
},
|
||||
};
|
||||
// These props are reserved by React. They shouldn't be written to the DOM.
|
||||
[
|
||||
'children',
|
||||
'dangerouslySetInnerHTML',
|
||||
// TODO: This prevents the assignment of defaultValue to regular
|
||||
// elements (not just inputs). Now that ReactDOMInput assigns to the
|
||||
// defaultValue property -- do we need this?
|
||||
'defaultValue',
|
||||
'defaultChecked',
|
||||
'innerHTML',
|
||||
'suppressContentEditableWarning',
|
||||
'suppressHydrationWarning',
|
||||
'style',
|
||||
].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
RESERVED,
|
||||
false, // mustUseProperty
|
||||
name, // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
const NS = {
|
||||
xlink: 'http://www.w3.org/1999/xlink',
|
||||
xml: 'http://www.w3.org/XML/1998/namespace',
|
||||
};
|
||||
// A few React string attributes have a different name.
|
||||
// This is a mapping from React prop names to the attribute names.
|
||||
new Map([
|
||||
['acceptCharset', 'accept-charset'],
|
||||
['className', 'class'],
|
||||
['htmlFor', 'for'],
|
||||
['httpEquiv', 'http-equiv'],
|
||||
]).forEach((attributeName, name) => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
attributeName, // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a list of all SVG attributes that need special casing,
|
||||
* namespacing, or boolean value assignment.
|
||||
*
|
||||
* When adding attributes to this list, be sure to also add them to
|
||||
* the `possibleStandardNames` module to ensure casing and incorrect
|
||||
* name warnings.
|
||||
*
|
||||
* SVG Attributes List:
|
||||
* https://www.w3.org/TR/SVG/attindex.html
|
||||
* SMIL Spec:
|
||||
* https://www.w3.org/TR/smil
|
||||
*/
|
||||
const SVG_ATTRS = [
|
||||
// These are "enumerated" HTML attributes that accept "true" and "false".
|
||||
// In React, we let users pass `true` and `false` even though technically
|
||||
// these aren't boolean attributes (they are coerced to strings).
|
||||
['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
BOOLEANISH_STRING,
|
||||
false, // mustUseProperty
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// These are "enumerated" SVG attributes that accept "true" and "false".
|
||||
// In React, we let users pass `true` and `false` even though technically
|
||||
// these aren't boolean attributes (they are coerced to strings).
|
||||
// Since these are SVG attributes, their attribute names are case-sensitive.
|
||||
['autoReverse', 'externalResourcesRequired', 'preserveAlpha'].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
BOOLEANISH_STRING,
|
||||
false, // mustUseProperty
|
||||
name, // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// These are HTML boolean attributes.
|
||||
[
|
||||
'allowFullScreen',
|
||||
'async',
|
||||
// Note: there is a special case that prevents it from being written to the DOM
|
||||
// on the client side because the browsers are inconsistent. Instead we call focus().
|
||||
'autoFocus',
|
||||
'autoPlay',
|
||||
'controls',
|
||||
'default',
|
||||
'defer',
|
||||
'disabled',
|
||||
'formNoValidate',
|
||||
'hidden',
|
||||
'loop',
|
||||
'noValidate',
|
||||
'open',
|
||||
'playsInline',
|
||||
'readOnly',
|
||||
'required',
|
||||
'reversed',
|
||||
'scoped',
|
||||
'seamless',
|
||||
// Microdata
|
||||
'itemScope',
|
||||
].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
BOOLEAN,
|
||||
false, // mustUseProperty
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// These are the few React props that we set as DOM properties
|
||||
// rather than attributes. These are all booleans.
|
||||
[
|
||||
'checked',
|
||||
// Note: `option.selected` is not updated if `select.multiple` is
|
||||
// disabled with `removeAttribute`. We have special logic for handling this.
|
||||
'multiple',
|
||||
'muted',
|
||||
'selected',
|
||||
].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
BOOLEAN,
|
||||
true, // mustUseProperty
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// These are HTML attributes that are "overloaded booleans": they behave like
|
||||
// booleans, but can also accept a string value.
|
||||
['capture', 'download'].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
OVERLOADED_BOOLEAN,
|
||||
false, // mustUseProperty
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// These are HTML attributes that must be positive numbers.
|
||||
['cols', 'rows', 'size', 'span'].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
POSITIVE_NUMERIC,
|
||||
false, // mustUseProperty
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// These are HTML attributes that must be numbers.
|
||||
['rowSpan', 'start'].forEach(name => {
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
NUMERIC,
|
||||
false, // mustUseProperty
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
const CAMELIZE = /[\-\:]([a-z])/g;
|
||||
const capitalize = token => token[1].toUpperCase();
|
||||
|
||||
// This is a list of all SVG attributes that need special casing, namespacing,
|
||||
// or boolean value assignment. Regular attributes that just accept strings
|
||||
// and have the same names are omitted, just like in the HTML whitelist.
|
||||
// Some of these attributes can be hard to find. This list was created by
|
||||
// scrapping the MDN documentation.
|
||||
[
|
||||
'accent-height',
|
||||
'alignment-baseline',
|
||||
'arabic-form',
|
||||
@@ -388,7 +434,21 @@ const SVG_ATTRS = [
|
||||
'vert-origin-y',
|
||||
'word-spacing',
|
||||
'writing-mode',
|
||||
'xmlns:xlink',
|
||||
'x-height',
|
||||
].forEach(attributeName => {
|
||||
const name = attributeName.replace(CAMELIZE, capitalize);
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
attributeName,
|
||||
null, // attributeNamespace
|
||||
);
|
||||
});
|
||||
|
||||
// String SVG attributes with the xlink namespace.
|
||||
[
|
||||
'xlink:actuate',
|
||||
'xlink:arcrole',
|
||||
'xlink:href',
|
||||
@@ -396,46 +456,36 @@ const SVG_ATTRS = [
|
||||
'xlink:show',
|
||||
'xlink:title',
|
||||
'xlink:type',
|
||||
'xml:base',
|
||||
'xmlns:xlink',
|
||||
'xml:lang',
|
||||
'xml:space',
|
||||
];
|
||||
|
||||
const SVGDOMPropertyConfig = {
|
||||
Properties: {
|
||||
autoReverse: HAS_STRING_BOOLEAN_VALUE,
|
||||
externalResourcesRequired: HAS_STRING_BOOLEAN_VALUE,
|
||||
preserveAlpha: HAS_STRING_BOOLEAN_VALUE,
|
||||
},
|
||||
DOMAttributeNames: {
|
||||
autoReverse: 'autoReverse',
|
||||
externalResourcesRequired: 'externalResourcesRequired',
|
||||
preserveAlpha: 'preserveAlpha',
|
||||
},
|
||||
DOMAttributeNamespaces: {
|
||||
xlinkActuate: NS.xlink,
|
||||
xlinkArcrole: NS.xlink,
|
||||
xlinkHref: NS.xlink,
|
||||
xlinkRole: NS.xlink,
|
||||
xlinkShow: NS.xlink,
|
||||
xlinkTitle: NS.xlink,
|
||||
xlinkType: NS.xlink,
|
||||
xmlBase: NS.xml,
|
||||
xmlLang: NS.xml,
|
||||
xmlSpace: NS.xml,
|
||||
},
|
||||
};
|
||||
|
||||
const CAMELIZE = /[\-\:]([a-z])/g;
|
||||
const capitalize = token => token[1].toUpperCase();
|
||||
|
||||
SVG_ATTRS.forEach(original => {
|
||||
const reactName = original.replace(CAMELIZE, capitalize);
|
||||
|
||||
SVGDOMPropertyConfig.Properties[reactName] = 0;
|
||||
SVGDOMPropertyConfig.DOMAttributeNames[reactName] = original;
|
||||
].forEach(attributeName => {
|
||||
const name = attributeName.replace(CAMELIZE, capitalize);
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
attributeName,
|
||||
'http://www.w3.org/1999/xlink',
|
||||
);
|
||||
});
|
||||
|
||||
injectDOMPropertyConfig(HTMLDOMPropertyConfig);
|
||||
injectDOMPropertyConfig(SVGDOMPropertyConfig);
|
||||
// String SVG attributes with the xml namespace.
|
||||
['xml:base', 'xml:lang', 'xml:space'].forEach(attributeName => {
|
||||
const name = attributeName.replace(CAMELIZE, capitalize);
|
||||
properties[name] = new PropertyInfoRecord(
|
||||
name,
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
attributeName,
|
||||
'http://www.w3.org/XML/1998/namespace',
|
||||
);
|
||||
});
|
||||
|
||||
// Special case: this attribute exists both in HTML and SVG.
|
||||
// Its "tabindex" attribute name is case-sensitive in SVG so we can't just use
|
||||
// its React `tabIndex` name, like we do for attributes that exist only in HTML.
|
||||
properties.tabIndex = new PropertyInfoRecord(
|
||||
'tabIndex',
|
||||
STRING,
|
||||
false, // mustUseProperty
|
||||
'tabindex', // attributeName
|
||||
null, // attributeNamespace
|
||||
);
|
||||
|
||||
@@ -14,9 +14,9 @@ import warning from 'fbjs/lib/warning';
|
||||
|
||||
import {
|
||||
ATTRIBUTE_NAME_CHAR,
|
||||
isReservedProp,
|
||||
shouldAttributeAcceptBooleanValue,
|
||||
shouldSetAttribute,
|
||||
RESERVED,
|
||||
shouldRemoveAttributeWithWarning,
|
||||
getPropertyInfo,
|
||||
} from './DOMProperty';
|
||||
import isCustomComponent from './isCustomComponent';
|
||||
import possibleStandardNames from './possibleStandardNames';
|
||||
@@ -155,7 +155,8 @@ if (__DEV__) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isReserved = isReservedProp(name);
|
||||
const propertyInfo = getPropertyInfo(name);
|
||||
const isReserved = propertyInfo !== null && propertyInfo.type === RESERVED;
|
||||
|
||||
// Known attributes should match the casing specified in the property config.
|
||||
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
|
||||
@@ -191,7 +192,7 @@ if (__DEV__) {
|
||||
|
||||
if (
|
||||
typeof value === 'boolean' &&
|
||||
!shouldAttributeAcceptBooleanValue(name)
|
||||
shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)
|
||||
) {
|
||||
if (value) {
|
||||
warning(
|
||||
@@ -235,7 +236,7 @@ if (__DEV__) {
|
||||
}
|
||||
|
||||
// Warn when a known attribute is a bad type
|
||||
if (!shouldSetAttribute(name, value)) {
|
||||
if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) {
|
||||
warnedProperties[name] = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user