diff --git a/src/diff/index.js b/src/diff/index.js index 242bf84581..ff03e5f954 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -332,6 +332,8 @@ function diffElementNodes( let oldProps = oldVNode.props; let newProps = newVNode.props; let nodeType = newVNode.type; + let isCustomElement = ('' + newVNode.type).indexOf('-') > -1; + let i = 0; // Tracks entering and exiting SVG namespace when descending through the tree. @@ -420,7 +422,7 @@ function diffElementNodes( } } - diffProps(dom, newProps, oldProps, isSvg, isHydrating); + diffProps(dom, newProps, oldProps, isSvg, isHydrating, isCustomElement); // If the new vnode didn't have dangerouslySetInnerHTML, diff its children if (newHtml) { diff --git a/src/diff/props.js b/src/diff/props.js index df5710925a..ed5b8cb75e 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -9,8 +9,16 @@ import options from '../options'; * @param {object} oldProps The old props * @param {boolean} isSvg Whether or not this node is an SVG node * @param {boolean} hydrate Whether or not we are in hydration mode + * @param {boolean} isCustomElement Whether or not we are diffing a custom element. */ -export function diffProps(dom, newProps, oldProps, isSvg, hydrate) { +export function diffProps( + dom, + newProps, + oldProps, + isSvg, + hydrate, + isCustomElement +) { let i; for (i in oldProps) { @@ -24,8 +32,7 @@ export function diffProps(dom, newProps, oldProps, isSvg, hydrate) { (!hydrate || typeof newProps[i] == 'function') && i !== 'children' && i !== 'key' && - i !== 'value' && - i !== 'checked' && + (isCustomElement || (i !== 'value' && i !== 'checked')) && oldProps[i] !== newProps[i] ) { setProperty(dom, i, newProps[i], oldProps[i], isSvg); diff --git a/test/browser/render.test.js b/test/browser/render.test.js index bd084a40b6..d8f2f5a733 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -489,6 +489,59 @@ describe('render()', () => { expect(scratch.innerHTML).to.equal(''); }); + describe('Custom elements properties', () => { + // Properties set to null/undefined are cleared in the DOM through an empty string. + const clearedPropertyValue = ''; + + class TestComponent extends HTMLElement { + constructor() { + super(); + this.value = {}; + this.checked = undefined; + } + } + customElements.define('test-component', TestComponent); + + it(`should set complex value property`, () => { + const value = { foo: 'bar' }; + render(, scratch); + + expect(scratch.querySelector('test-component').value).to.equal(value); + }); + + it(`should set checked property`, () => { + let initialValue = true; + render(, scratch); + expect(scratch.querySelector('test-component').checked).to.equal(true); + }); + + clearPropertyWith(null); + clearPropertyWith(undefined); + + function clearPropertyWith(clearValue) { + it(`should clear existing value property with ${clearValue}`, () => { + render(, scratch); + + render(, scratch); + + expect(scratch.querySelector('test-component').value).to.equal( + clearedPropertyValue + ); + }); + + it(`should clear existing checked property with ${clearValue}`, () => { + let initialValue = true; + render(, scratch); + + render(, scratch); + + expect(scratch.querySelector('test-component').checked).to.equal( + clearedPropertyValue + ); + }); + } + }); + it('should mask value on password input elements', () => { render(, scratch); expect(scratch.innerHTML).to.equal('');