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('');