Transitive synchronized inputs

Hello hello,

First time poster here. I have the following question and a notebook that shows an unexpected behavior I’m trying to understand:

Setup

  • Create three inputs source, middle, target
  • Bind source to middle Inputs.bind(viewof middle, viewof source)
  • Bind middle to target Inputs.bind(viewof target, viewof middle)

Expected

  • typing in any input updates the two other inputs.

Actual

  • typing in source updates only middle
  • typing in middle or target updates the two other inputs.

It’s unexpected that binding inputs is not transitive… Maybe I’m doing something wrong.

I understand I could bind target to source also but I thought this would work so I’m trying to figure out why it’s not :slight_smile:

Cheers,

Jun

Think of Inputs.bind() as adding remote controls for your primary input. To quote the docs:

The relationship between target and source is not symmetric: the target input should be considered a dependant of the source input, and if desired, only the source should be declared an Observable view.

Hi @mootari

Thanks for your answer. I understand the relationships are not symmetric, my question is why are they not transitive?

In the notebook example the primary input and dependency relationships set up are:

  • Inputs.bind(viewof middle, viewof source)
    • Primary input is source, i.e. midde is a dependent of source
    • So events flow source -> middle
  • Inputs.bind(viewof target, viewof middle)
    • Primary input is middle: i.e. target is a dependent of middle
    • So events fow middle -> target

If that’s the case, my question is, why don’t event flow from source -> target. i.e. why is the bind relationship not transitive?

Hope this clarify my question!

Jun

I realise I can ask my question with a more practical angle:

  • How can I make it so, that when typing in source I see both middle and target update their content?
  • How come binding source -> middle and middle -> target doesn’t work for that purpose?

Use Inputs.input() as a fake source.

Bind the real UI inputs to that one.

there is a diagram here Scaling User Interface Development / Tom Larkworthy | Observable

As mentioned in the excerpt I shared above, you should only have one viewof cell:

As for why it doesn’t work transitively in both directions: Inputs.bind only dispatches input events on the source (see implementation). The input event is required to let Observable’s Runtime know that a viewof cell’s value has changed and that dependent cells should be invalidated and rerun with the updated value. Programmatically setting a form element’s value however does not automatically dispatch an event.

So while typing in target will dispatch on middle which in turn will dispatch on source, the same isn’t true for the other direction because Inputs.bind() relies on these events to know when to update bound inputs.

@tomlarkworthy Great seeing you here, and thanks for sharing your write up! It really goes in depth and helps a lot. Composability is what indeed I was trying to achieve, and it so happens that my intended use case and what I’m trying to troubleshoot involves your localStorageView component! What happened is that I was trying to use @cmudig/editor but was having trouble wiring it with localStorageView and another button input in this notebook @jmatsushita/try-purescript (sorry as a new user I only have a budget for 2 links :confused: )

I suggested the following changes to @cmudig/editor to try and make the editor reusable (which have been merged since)

// support sending and receiving updates as per https://observablehq.com/@tophtucker/custom-input-example
  Object.defineProperty(container, "value", {
    get() {
      // console.log("editor.get called:", ed.textContent)
      return ed.textContent;
    },
    set(v) {
      // console.log("editor.set called with: ",v)
      ed.textContent = v;
      jar.updateCode(v);
    }
  });

And added a couple of use cases that were not supported before:

  • Setting the value programmatically
  • Supporting persistence with localStorageView

However when trying to wire both of these use cases in @jmatsushita/try-purescript I was scratching my head and started this thread.

I tried your suggestion to create a fake source with Inputs.input(), but It didn’t work, I think because then the binding direction with the localStorageView wasn’t right. Which led to errors

Uncaught TypeError: e.closest is not a function
    at inputs.min.js:40:1444

I created a miminal reproducer here.

I think having a localStorageView means that I can use that as the “authority source” instead of needing a fake source, as per your diagram.

Is it the intended way to use localStorageView, as the “authority”?


@mootari Thank you so much.

and if desired, only the source should be declared an Observable view.

I suppose the docs weren’t crystal clear for me at least. The if desired doesn’t really carry the same meaning as “you should only have one viewof cell”, which is much clearer!

However, your solution, I think works because it avoids chaining/transitive dependencies, it doesn’t seem to be because of the viewof usage. As you can see in the “Solution” section using viewof everywhere seems to achieve exactly the same result.

I did implement your suggestion, using only viewof on the source, in the section “Follow up question” just below, and it seems to behave the same. Am I missing something important here? :slight_smile:

Programmatically setting a form element’s value however does not automatically dispatch an event.

Oh I see, hence the “set helper” I’ve seen around which both sets the value and fires a bubbling event, pretty much exactly like the bind code you linked. (Although I suppose it should be call the “set and dispatch helper” really).

It helps thank you! Maybe using @tomlarkworthy 's diagram and paragraph I copy pasted above would be a great addition to the docs!

1 Like

normally I have localStorageView as a target. So for a coding system, viewof code = Editor(...) is the authority. localStorageView is bound to that, and if you have buttons injecting examples they should write into the editor viewof. If they write AND tell the editor to emit an event, the localStorageView should pick it up. At least in theory, sometimes there is nuance :confused:

I’ll try and see if I can put an example together later but I am at work right now.

1 Like

Yes, you likely didn’t look at the underlying generator value:

I recommend to have a look at this introduction to viewof which might help you approach these kinds of problems differently:

1 Like

Normally I would use localStorageView like this

viewof editor = Inputs.bind(
  PurescriptEditor(
    ""
    // [myDefaultTheme]
  ),
  localStorageView("code")
)

But this does not work with the PurescriptEditor because it is not setup as a view properly when it returns. There is some stuff going on in the background.

I put a basic setup for what you are trying to acheive here Community Help / Tom Larkworthy | Observable using a text component.

ahh I just need to await it. The other problem is the editor is not emitting an input even every time the document updates, which means the data channel is not tracking the editor content. Note how my variable is not synced to the editor code.

Its that event that localStorageView looks for too. So localStorage is not informed of the editor changes and thus can’t save the content.

I tried to write the rules of views here Design Guidelines and Linter for Reusable UI Components / Tom Larkworthy | Observable

although there is not one for emitting the event when the user interacts. I supect there is an event inside but it has not set the bubbles:true flag on the even so its not leaving the component

Cool PureScript editor! Will be cool

Oh I do have a backwritable code mirror view here CodeMirror 6 View / Tom Larkworthy | Observable

I could not find it when searching :confused:

I upgraded the example of binding localStorageView to it to support persisted code mirror