luwes / sinuous

🧬 Light, fast, reactive UI library
https://sinuous.netlify.app
1.04k stars 34 forks source link

Make `each` work with a plain `list.map` #2

Closed luwes closed 5 years ago

luwes commented 5 years ago

Lighterhtml @WebReflection seems to have cracked this. Not sure if it's possible with this library.

ryansolid commented 5 years ago

Not really. This is the cost of fine grained hitting you here. Lighterhtml is top down diffing. A completely different paradigm. I think it might be possible and I worked at it for a while before changing my approach. Especially since I figured I could use the compilation step to close the gap. There is a real positive benefits to using compiled DSL for fine grained to remove all the wrapping ugliness you are seeing.

At minimum you will probably need to provide your own map function. The problems are 2 fold:

  1. How to hoist out context and handle appropriate disposal logic when the expression itself is the computation.
  2. How to ensure that nested updates ie. list[0].___ don't retrigger the whole thing again. Or get unnecessarily recreated.

The first problem is a lot harder than the 2nd but they are both awkward. See if you execute the list accessort ie. list() that whole computation retriggers but you want to save state, memoize unchanged keys to maintain deeper nested observables. This means that those observables and some amount of disposal logic need to live outside of the bindings own computation.

The first thought is what I nest it in another computation, as the outer one won't refire in that case, but to do so you need to be aware of the end users intention since you can't just don't nest everything. So that creates it's own DSL.. so no good.

Second thought is well if I can accomplish the same by not dereferencing in the binding computation. So if my list signal has it's own map function that could work. The awkwardness there is creating specialized objects, and do you hoist it onto the prototype? Not great for tree shaking. This is the tact that S-Array takes. Having used knockout for years ObservableArray being a thing always was awkward and I was using Proxies which completely alleviates the need for special Observable Types so I shied away.

So then you are left with just making your own method maybe map(observable, mapFn). And this isn't the worst but you are basically back at your own DSL again. At which point you are asking why am I taking the overhead of doing the mapping in the observable layer and again the rendering resolution.

I just went back to option 1, and figured out what I felt was the best way to mitigate its shortcomings. I'm super interested to see if you come up with a different solution to this problem.

luwes commented 5 years ago

Thanks for your great reply @ryansolid. Makes a lot of sense.

Currently it looks a bit like this in Sinuous:

      <ul>
        ${each(list, (item) => html`<li id=${item.id}>${item.text}</li>`)}
      </ul>

I do like the idea of running your own .map function on the observable, it would be "invisible" to the user and it could be an alias to each.

      <ul>
        ${list.map((item) => html`<li id=${item.id}>${item.text}</li>`)}
      </ul>

With a plain array.map it does look quite impossible. I'll have to do some thinking on this one :)

      <ul>
        ${() => list().map((item) => html`<li id=${item.id}>${item.text}</li>`)}
      </ul>
luwes commented 5 years ago

Closing because if it's even possible it would make the code more complex.

The current map function works nicely, not adding it to the observable because of not being able to tree shake.

      <ul>
        ${map(list, (item) => html`<li id=${item.id}>${item.text}</li>`)}
      </ul>