Save viewof input values when forking a notebook

I have a notebook that accepts lots of inputs, collected through views. The user experience I’d like to get to is:

  • User opens my notebook
  • User changes input values in the notebook until they’re happy with them
  • User then forks notebook, and a copy of the notebook is made that retains the inputs values at their current state

But at the moment, this doesn’t happen: after the notebook is forked, the input values are reverted to their default.

What is the best way to approach this: allowing users of my notebook to “save” the values that they have selected for the inputs?

1 Like

I imagine the data should be saved to https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage.
And the input initial value should be set looking first to localStorage, or else to a default value.

I don’t think that’s going to work, as the worker domain changes when the notebook is forked by another user.

hmmm you’re right!

Regardless of the solution that you pick, it will always require the user to actively save their settings before forking. If you’re good with that, some options would be:

  • Permanent:
    1. Store the default values in a mutable cell that you update when the configuration is changed.
    2. Users can download the data as JSON and paste it into the cell in their fork.
    3. A variation of this would be a button that lets users copy the current configuration to the clipboard.
      Note that you can add toJSON() methods to your objects in order to control how they’ll be serialized.
  • Temporary:
    1. Encode the configuration in URL parameters and offer a perma link.
    2. Make sure that users click the link before creating a fork, so that they can navigate back to the original notebook with that config applied.
    3. They’ll have to copy the URL parameters over to their own notebook (or you provide a widget where they enter the notebook ID and you generate the link).
    4. Users can then decide how they want to persist the data outside of the URL.
1 Like

In case you need to put parameters in the URL, @Fil wrote a useful notebook https://observablehq.com/@severo/bookmark (and I published it)

Alternatively the UrlParam class in this notebook base64-encodes a config and assigns it to a single query parameter:

2 Likes

Thanks @mootari and @severo! I’m going to use the URL method I think.

Ideally I’d like the URL to dynamically update as soon as an input is updated. Do you know if that’s possible?

Sadly it’s not. You could however highlight your link if the new URL no longer matches the current one.

Ah well, no worries, thanks for your help!

I’ve almost got it working, but I haven’t been able to get the values from the URL to actually update the inputs when the page loads.

If you can see what I’m doing wrong, it would be much appreciated!

Check out the second cell in my notebook. Its output is hidden until you change some of the sliders. Looking at the cell’s source might give you some pointers.

Edit: I misremembered. The output becomes visible when a preset is applied, eg. by clicking the link “current settings”.

Yeah I can replicate it for a single input, but I’m trying to use this with a grid of inputs, and I can’t seem to get it to work.

I’ve created a very simple example notebook using this approach with inputsGroup and it doesn’t work, I guess there’s something about this approach I’m just not seeing.

Alright, let’s dive in.

First things first: You can import UrlParam just like any other cell:

import {UrlParam} from '@mootari/triangles-and-circles'

If you copy over code, please remember to add a comment for the proper attribution.

Next, we’ll discard inputGroup because it adds another layer of abstraction that will be difficult to deal with. However, we’ll import a new helper that enables us to change values of @jashkenas/inputs widgets from the outside:

import {mutableForm} from '@mootari/mutable-forms'

Time to build our options form. A little more overhead since we’ll go fully custom, but that gives us a high degree of flexibility.

viewof options = {
  const widgets = {
    sides: mutableForm(slider({
      title: 'Number of sides (grid)', min:3, max: 20, step: 1, value: 8,
    })),
    radius: mutableForm(slider({
      title: 'Size', min:3, max: 20, step: 1, value: 8,
    })),
  };
  // The outermost div is what we present to Observable's Runtime as our view.
  // It wraps both widgets, nicely lining them up horizontally.
  const view = html`<div style="display:flex;flex-wrap:wrap">
    <div style="margin:0 10px 10px">${widgets.sides}</div>
    <div style="margin:0 10px 10px">${widgets.radius}</div>
  `;
  // Empty value object for our view which we'll populate below.
  view.value = {};
  // For each widget, add a property to the value.
  for(const [name, widget] of Object.entries(widgets)) {
    Object.defineProperty(view.value, name, {
      // Getter. Accessing options[name] returns widgets[name].value
      get: () => widget.value,
      // Setter. Assigning a value to options[name] sets it for widgets[name].value
      set: value => { widget.value = value },
      // Makes the property visible in the Inspector (and when iterating over the properties).
      enumerable: true,
    });
  }
  
  return view;
}

All we need to do now is to set the options from the URL. Removing the fluff texts, we’re left with:

{
  const preset = new UrlParam('preset').getData();
  // A shorthand to the value object.
  const value = viewof options.value;
  for(const name in preset) {
    // Simply assign all values. mutableForm() takes care of
    // setting the value on a widget's input and dispatching
    // the required event.
    if(name in value) value[name] = preset[name];
  }
  // Let's see what was in that preset data.
  return preset;
}

Awesome thanks, yeah this is almost working now, but the form values aren’t updating with the preset values.

You’ve added a comment that says the mutableForm() function should handle dispatching the event, but it doesn’t seem to be working?

Add the following line to your notebook to inspect the preset data:

new UrlParam('preset').getData()

You’ll notice that the data has only a single key, options. Then have a closer look at getPresetUrl(), specifically how you pass in your options:

.getUrl({options})

should read

.getUrl(options)

Awesome, got it now. Thanks so much for your help!