Skip to content

Refactor DOM attribute code (take two) #11815

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

Merged
merged 31 commits into from
Dec 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
14ac736
Harden tests around init/addition/update/removal of aliased attributes
gaearon Nov 26, 2017
0791bf4
Call setValueForProperty() for null and undefined
gaearon Nov 26, 2017
790929b
Inline deleteValueForProperty() into setValueForProperty()
gaearon Nov 26, 2017
94a6e1a
Inline deleteValueForAttribute() into setValueForAttribute()
gaearon Nov 26, 2017
f49a762
Delete some dead code
gaearon Nov 26, 2017
1601afb
Make setValueForAttribute() a branch of setValueForProperty()
gaearon Nov 26, 2017
38ee12f
Make it more obvious where we skip and when we reset attributes
gaearon Nov 27, 2017
61a6a41
Rewrite setValueForProperty() with early exits
gaearon Nov 27, 2017
48e761f
Move shouldIgnoreValue() into DOMProperty
gaearon Nov 27, 2017
8a7a78f
Use more specific methods for testing validity
gaearon Nov 27, 2017
1459c21
Unify shouldTreatAttributeValueAsNull() and shouldIgnoreValue()
gaearon Nov 27, 2017
0e0d2b9
Remove shouldSetAttribute()
gaearon Nov 27, 2017
fabf589
Remove unnecessary condition
gaearon Dec 8, 2017
e27ab8f
Remove another unnecessary condition
gaearon Dec 8, 2017
87d625f
Add Flow coverage
gaearon Dec 8, 2017
a23de10
Oops
gaearon Dec 8, 2017
2e0f554
Fix lint (ESLint complains about Flow suppression)
gaearon Dec 8, 2017
1dfa5d3
Fix treatment of Symbol/Function values on boolean attributes
gaearon Dec 9, 2017
9177be3
Avoid getPropertyInfo() calls
gaearon Dec 9, 2017
6f371c0
Pass propertyInfo as argument to getValueForProperty()
gaearon Dec 9, 2017
fd706c4
Make it clearer this branch is boolean-specific
gaearon Dec 9, 2017
6864ee6
Memoize whether propertyInfo accepts boolean value
gaearon Dec 9, 2017
4aabb7c
Fix a crash when numeric property is given a Symbol
gaearon Dec 9, 2017
b984eaf
Record attribute table
gaearon Dec 10, 2017
bc17e49
Refactor attribute initialization
gaearon Dec 10, 2017
3d0b66c
Optimization: we know built-in attributes are never invalid
gaearon Dec 10, 2017
6cbea40
Use strict comparison
gaearon Dec 10, 2017
3e0090b
Rename methods for clarity
gaearon Dec 10, 2017
dd0cd63
Lint nit
gaearon Dec 10, 2017
12a1bac
Minor tweaks
gaearon Dec 10, 2017
17f7ac9
Document all the different attribute types
gaearon Dec 10, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
486 changes: 243 additions & 243 deletions fixtures/attribute-behavior/AttributeTableSnapshot.md

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 => {
Expand All @@ -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);
});
});

Expand Down
Loading