ractivejs / ractive

Next-generation DOM manipulation
http://ractive.js.org
MIT License
5.94k stars 396 forks source link

Linked values are not properly data-bound in components #3235

Open dagnelies opened 6 years ago

dagnelies commented 6 years ago

Hi,

the example is here:

https://dagnelies.github.io/ractive-examples/editor4panes.html?url=widgets/simple-grid.html

There are two tables. The first is plainly in the template. With an on-click="@.link(@keypath, 'selectedBis')" and the other basically the same but wrapped inside a component. You can see that clicking on the first table propagates the binding to the second table (it's the same data), but if you click on the second table wrapped in a component, the on-click="@.link(@keypath, 'selectedBis')" appears to break the "link".

evs-chris commented 6 years ago

This is an interesting edge case for links. What's happening is the outer context is creating a link, which the inner context is then linking to - a link to a link to some keypath. When the outer context changes the link destination, the inner context can observe the change, but as soon as you link the inner context, the link to the outer link is broken. I'm not sure that it makes sense to be able to change the outer context link from within the inner context.

Since fs symlinks are the go to analogous construct here, is there a way to link to a file, link to that link, and then change the middle link's target by changing the outer link? There could be an option added to link to do so, but given that this is in a nested context where @keypath doesn't exactly line up with the source path that may lead to more issues. You can kinda skirt that with a @rootpath, but that falls apart as soon as there's data mapped in that's not on the root. You can also kinda skirt it by resolving the keypath from a context and giving it an anchor instance (@context.resolve('.', @.parent)), but at that point you've still locked yourself into a tightly coupled template structure, which may or may not be acceptable.

My solution to this is usually to use a yield or fire an instance event and let the controlling context handle what happens.

dagnelies commented 6 years ago

Well, that the two grids link to the same selectedBis item was to show that it works in the outside -> inside direction. I agree it's an edge case.

However, the much more general case doesn't work either. If you pick a completely different name like <grid selected="{{~/selectedFoo}}" ... /> and want to show it outside the component {{JSON.stringify({{~/selectedFoo}}), it will not be linked/displayed.

dagnelies commented 6 years ago

I can work on a smaller / more tailored example tomorrow if you want.

evs-chris commented 6 years ago

So you're looking for a sort of a reverse link? I'm gonna lean on the symlink example for clarification:

cd /
mkdir list
cd list
touch 0 1 2 3
cd ..
ln -s list /mnt/component/list
# let the component selected point to selectedFoo, which doesn't actually exist at this point
ln -s selectedFoo /mnt/component/selected
# change to the component context
cd /mnt/component
# from component, selected is a symlink pointing at /selectedFoo
ln -s list/0 selected
# now selected is a symlink to /mnt/component/list/0, which is a symlink to /list/0

What you're after is a way to say ln -s /mnt/component/selected /selectedFoo, which currently doesn't exist as a template construct, however, you could set it up during init e.g. <grid on-init="@.parent.link('selected', 'selectedBis', { instance: @this })">, which would create the link from the parent into the child (the syntax may not be exactly right, as I haven't tried it, but the gist is there).

dagnelies commented 6 years ago

Well, you're deeper in this than I. I only have the black box view and can only speak for myself about the behavior I would intuitively expect.

From reading <grid selected="{{selectedFoo}}", I would intuitively assume that the selectedFoo outside the component is the same as the selected inside the component. As such, I would indeed expect that the link inside the component is propagated outside.

However, links are a bit tricky, because what happens when you set('mylink', ...) is conceptually unclear. (actually, I have no idea if it overrides the link or updates the linked data).

evs-chris commented 6 years ago

<grid selected="{{selectedFoo}}" creates a link in the grid that points selected to selectedFoo in the parent instance, so that when selectedFoo is updated (set, add, etc), it notifies selected and when selected is updated, it passes the change upstream to its source (selectedFoo) and lets the change flow back through to the links for the actual update. Calling get on a link also passes the request to the link's source.

Links are not the same data, they're shadow modesl that manage their own deps, but pass all data access requests through to the source (like a symlink). The reason it's not the same data directly is if the link source changes, there would be no way to determine which deps were supposed to be on the link and which were supposed to be on the source when moving them around. As such, relinking a link just points it at a new source, but doesn't modify the source in any way.

dagnelies commented 6 years ago

I assumed linked data worked like normal data with some kind of two-way observers ...which they do not. I have to get used to how they "tick" ...but still, it's slightly misleading. A warning would be appropriate when you try to attempt to use them the "wrong" way, like data-binding a link through a component attribute.

evs-chris commented 6 years ago

There are legitimate uses for linking to a link, but it is something you have to be careful with if you're doing anything with further manual linking rather than just data manipulation. I can see a pretty good cause to add support for creating a reverse link (from parent into child data, as opposed to the current child into parent data), but I haven't come up with a good way to express it yet. I think that would solve both of the cases you presented here. You can accomplish it manually with inline lifecycle hooks on the component, but it's not nearly as pretty as automatic mapping lifecycle management.