how to use `mutable` to escape cell re-evaluation for input?

This might be easier to understand if we look at the code that Observable produces:

{
  name: "test_text_mutate",
  inputs: ["viewof test_text"],
  value: (function($0) {
    return($0.value = 'bar')
  })
}

Here, $0 is the viewof test_text cell, which in turn is a DOM node with a “special” value property. Let’s take a look at what makes it special:

  1. Let’s say we have an input <input type=text>. Per the HTML specification, this DOM element already comes with a value property.
  2. When we enter something into the text field, each keystroke will trigger an “input” event.
  3. We can use this input with the viewof macro to have both a cell with our DOM element, and another internal cell that only contains the value:
    viewof mytext = html`<input type=text>`
    // Produces two Runtime Variables:
    // 1. "viewof mytext" (the DOM element)
    // 2. "mytext" (the value of the DOM element's `value` property)
    
  4. Because we used the viewof macro, Observable wraps our cell in some code that listens to these events and automatically updates the internal value Variable (and in turn all dependent cells).
  5. We can modify the DOM element’s value property from the outside, by accessing the viewof cell’s value, i.e., the DOM element:
    (viewof mytext).value = "a new value"
    
  6. We can see the text inside the input change, but no “input” event is triggered, and as a result Observable has no idea that the value was changed.

Luckily Javascript allows us to define a getter and, more importantly, a setter for a property – that is, a function that is called whenever some code accesses the property (reading or changing it):

  1. Internally we still need to access and change the input’s property directly, so we cannot simply define a getter or setter directly for the input. Instead, we wrap it, e.g.:

    viewof mytext = html`<div><input type=text></div>`
    
  2. Observable will now listen to input events on the div instead of the input, and also check for a value property on the div. The input events “bubble up” and thus can still be registered on the div, but div’s don’t have a value property!

  3. Let’s define one:

    viewof mytext = Object.defineProperty(
      // The object that we want to add the property to.
      // Note that this is also the return value of defineProperty()!
      html`<div><input type=text></div>`,
      // the name of the property
      "value",
      // The definition:
      {
        get() {
          // "this" is the div, .firstChild the input.
          return this.firstChild.value;
        },
        set(value) {
          // Assign the new value, like we did earlier.
          this.firstChild.value = value;
          // Let Observable know about the change.
          // (Note that manually dispatched events, by default, do *not* bubble.)
          this.dispatchEvent(new Event("input"));
        },
      }
    )
    

With that modification in place, let’s again assign a value and watch what happens:

(viewof mytext).value = "a new value"
  1. The setter is invoked and receives the string “a new value”.
  2. The setter modifies the value on the input element.
  3. The setter dispatches an “input” event.
  4. Observable notices the input event and updates the “mytext” variable.
1 Like