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

Yesterday, Fabian helped guide me to a solution for modifying and re-saving an array between Observable and S3.

Taking this one step further, my desire is to create input fields for my template array, so that I can point at a given array object, expose and edit the values in that object, and the re-save.

The trouble that I am currently facing is that each time I type something into an input field, each key stroke triggers my ‘write’ function (which references my data), with the rest being numerous incomplete arrays being written.

I have read
Introduction to Mutable State as well as the clear and helpful conceptual descriptions of mutables in A bit confused about ‘mutable’, etc . I also found and read Mutable Forms. Sadly, I am not yet understanding how to implement it.

Please help me verify my understanding:

If I am reading the examples correctly, then essentially the cell assigned a mutable operator should be linked to a reference operator, such that when both values are identical, the reference operator performs an action.

If this is correct, then I somehow need to determine how to construct a reference operator? I am at a bit of a loss for what this might look like for the purposes of my notebook :confused:

Also potentially related to understanding when and how to use mutables and determining places to put them: Whenever I reload my notebook, my ‘update’ cell runs on page load – appending a new record and saving it to S3 automatically. This isn’t intentional or wanted. Any suggestions for how to get around this?

Here’s a notebook with a test input:

The submit option here might be useful: https://observablehq.com/@jashkenas/inputs#textDemo

1 Like

Thanks @yurvish! Certainly this allows me to avoid creating new records before an input value is defined. Regrettably, it only takes me part way:

My idea is to create multiple inputs for each ‘record’ [which has a large number of input fields]. I would like to submit the record as a whole with a ‘save’ button (which is largely working) rather than inadvertently saving each time that a new value is written to one particular input. So I reckon I have to somehow de-couple my ‘update’ button from specific changes to the data object that is referenced by the update button. Observable’s reactive cell evaluation, however, makes this difficult. Reading about mutable states, this seems like it’s the intention of the mutable operator. I just can’t (yet) conceptualize how it could look in this context.

1 Like

What is a “reference operator”?

If you want to edit existing entries, you’ll have to identify them by some unchanging value, e.g. an incrementing ID, a generated UUID, or simply the row index. This means that Jeremy’s inputs alone won’t cut it.

As a sort of Poor Man’s row editor you could add a select which allows you to choose the row that you want to edit (with the ID as its value), and have the text input depend on that select.
When storing the updated value, you can refer to your select value to know which row should be updated.

Thanks @mootari! Your suggestions are exactly where I was heading with this: using the row index value.

Still, I am struggling with unintentional writing.

My term “reference operator” was my best way to describe this (from your notebook)

test_text_mutate = viewof test_text.value = 'bar'

or this (from Jeremy’s notebook):

counter = {
  let i = 0;
  while (true) {
    if (i % 2 === 0) mutable counterEven = i;
    if (i % 5 === 0) mutable counterFives = i;
    yield Promises.delay(1000, ++i);
  }
}

… what I am seeing is that if a mutable cell value equals the “reference operator” then the mutable action will be triggered.

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

Wow @mootari! Thank you. This is an amazing explanation of how inputs are constructed and how they interact with Observable. With some time and thought, reading (and re-reading) this, I expect that I’ll be able to move forward in constructing my application. [You might have surmised it, but I intend to create (and share) a ‘poor man’s bibliography and citation manager’ (thanks for the naming idea :wink: ) using Observable and S3).]

I will be sure to report back here after working through this on my desire to both escape and trigger update events. :heart:

To close out this discussion: Escaping cell re-evaluation when typing into an input was a simple matter of declaring a mutable JSON object, and then assigning to it my desired value:

mutable bibliography_update_template = JSON
mutable bibliography_update_template.value = [{..JSON content, w/ input values..}]

And then referencing the mutable value here:

var new_array = Array.from(data_object_formatted).concat(mutable bibliography_update_template.value);

… I haven’t yet managed to work my way around the creation of a new record on page load.

@mootari’s given me separately quite a bit more food for thought (in addition to the fantastic walk-through above) and I am still working through it. At least now I can edit and write records at my discretion :slight_smile:

I’ve updated the notebook: input data into S3 array / categori.se / Observable

Thank you @mootari @yurivish and everyone here for your generous patience, teaching, and support!