Netflix / x-element

A dead simple starting point for custom elements.
Apache License 2.0
28 stars 12 forks source link

Fix edge case where computed property is used between construction and initialization. #143

Closed theengineear closed 1 year ago

theengineear commented 1 year ago

Through manual manipulation via document.createElement — it’s possible to get into a bizarre scenario where computed properties appear to be inexplicably wrong. Take the following:

import XElement from 'https://deno.land/x/element/x-element.js';

class TestElement extends XElement {
  static get properties() {
    return {
      a: { type: Number },
      b: { type: Number },
      c: { type: Number, internal: true, input: ['a', 'b'], compute: (a, b) => a + b },
    };
  }
}
customElements.define('test-element', TestElement);

// Hint: This element is constructed, but not initialized.
const element = document.createElement('test-element');
element.setAttribute('a', '1');
element.setAttribute('b', '2');
console.log({ a: element.a, b: element.b, c: element.internal.c });
/* 
  This is reasonable — it's not expected that attributes sync before DOM connection.
  {
    a: undefined,
    b: undefined,
    c: NaN
  }
*/

// By attaching it to the DOM, we finally initialize.
document.body.append(element);
console.log({ a: element.a, b: element.b, c: element.internal.c });
/*
  This is NOT reasonable — everything should _just work_ after initialization.
  {
    a: 1,
    b: 2,
    c: NaN
  }
*/

To summarize — this edge case is caused by manual manipulation of attributes and introspection of downstream, internal, computed properties thereof.

In theory, this might even be caused by race conditions due to timing issues between attribute setting, property introspection, and forcible upgrade paths via customElements.upgrade. However, the simple target is to fix the easily-reproducible issue.

Proposal

Explicitly set the memoized compute state to valid = false on initialization. I.e., expect that the element could have been manipulated / interrogated between construction and initialization.