ractivejs / ractive

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

Adaptors on edge - Discussion #2512

Open evs-chris opened 8 years ago

evs-chris commented 8 years ago

Description of the issue: I've been looking at fixing some of the adaptor-related bugs on edge, and I've come to the conclusion that there's not really a way to get magic adaptors and array adaptors working together in a clean way. They currently mostly work by getting a reference to their model to side-step the public API, which isn't really ideal. They also don't deal well with being shuffled (see #2005) and can't be used to control models more than one key deep, which is problematic for more advanced adaptors like ractive-ractive.

I think I may be able to eventually mangle things into working, but it feels like I'm fighting the API pretty hard at this point. Of course, I may be completely missing something, and the fix may actually be super simple :smile:.

I know I've mentioned before that I think the array and magic adaptors should be pulled into separate plugins, but I'll say it here to to keep everything together. That would also pave the way for an observer-based magic adaptor to be swapped in for those not requiring legacy browser support.

Link #2288, #2500

Versions affected: edge/0.8

Potential fix/brain dump: The model currently has a handle to its adaptor, but it can't really do much with it beyond setup, reset, and teardown. I was thinking that an adaptor handle object might be a decent solution to communication between the model and adaptor:

JonDum commented 8 years ago

Personally, I think array/magic adaptors are very counterintuitive to Ractive's set/get that's taught in docs from the get go. It's also a serious wtf moment before you come across modifyArrays and realize what that does (same could be said for magic, but at least that is off by default).

martypdx commented 8 years ago

Posted this in wrong place on adaptor PR. Reposting here...

I think there are three things that adaptors get used for, it might be worth separating them out:

  1. Mediating between a data source and the ractive instance model. Most classic use and what people generally think of as adaptors. Your spot on that we need to have dedicated array modification methods. One of the other needed additions to delineate requests made for child properties for the purpose of access a further child property versus for a value to be used in the template. For me, this came up with working with Firebase as getting a childen reference has a much different effect than getting a child value (the former is a reference, the later fetches data from the server).
  2. Mediating between ractive model and the template. For the most part, this should be handled by computed properties, but I've seen it crop up as an adaptor when devs try to apply to a testable conditions, for example show any boolean value as Y or N. If it's between the model and template, IMO it shouldn't be an adaptor.
  3. Extending the ractive API. The data needs a way to tell ractive about changes, the inverse of item 1. Usually this would be in conjunction with first point, but interestingly both the array and magic adaptors fall in this category, but not the first.

Also, I still think for most cases only the ractive instance introducing the data should adapt it.

Jeff17Robbins commented 8 years ago

Whoops -- I too posted on the PR and am reposting here.

@martypdx thank you for the three use cases. I thought it might be worth sharing what we have been doing with the Ractive-Ractive adaptor.

We have been using it to create a template-less Ractive instance that acts as a "store" to several other Ractive instances that act as "views". We use it one-way only, meaning that we only need data flowing one-way from the store to the views.

While there is nothing mandating that we use a Ractive instance as our store, it seems handy because we then also get computeds in our store, for free, so to speak. Since many views can share one store, it seems clean to us.

If there is another way to manage a single external model, and somehow map it into models of our views, we could use that other way. The basic requirements are unsurprising:

  1. A single location to deposit incoming JSON data.
  2. A way to then update efficiently several different Ractive instances with that data.

Our views use mustaches like {{store.somevalue}} or {{store.someobj.someprop}} in their templates. The initial part of the path 'store' is meant to pick out the store Ractive instance.

In some ways, our use is like your use-case 1, except we want a staging area (aka "store") to first deposit incoming data so that we can then have many Ractive instances ("views") sharing this data. But in another sense, it seems like your use-case 3, in that when our store gets new data, we need it to tell ractive (the ractive instances acting as "views") about the changes. If we can continue to use the Ractive-Ractive adaptor, that would be easiest for us. But if there is a better (or equivalent) way to model this in Ractive.js v0.8.0, we are totally open to a different approach.

Any suggestions?

martypdx commented 8 years ago

@Jeff17Robbins It seems the underlying issue has little to do with adaptors, and more about how to wire together separately instantiated Ractive instances into a common parent.

It my apps, I just use a common app parent that is the root of the component tree. Sharing data in a common tree is already something Ractive does very well. So then I use the onrender event to move components out of the actual dom tree and place them elsewhere on the page (a modal popup dialog for example) and all of the data bindings stay intact (yeah Ractive).

So option 1 is to use a single view hierarchy and move around the components on render. (Let me know if this sounds interesting and I and can post some examples).

It's been requested elsewhere to:

  1. allow an el to be specified on an in-tree rendered component
  2. allow components to be attached to an instance (versus requiring a component tag in the template) and thus establish the logical view hierarchy without an actual dom hierarchy.

I think 2 would give the most flexibility (I think 1 is something of a hack to get to 2 ), and from what I remember of the code, Ractive is well architected to allow this sort of thing.

Option 2 then would be to add a method like addChild that would allow you to do:

var app = new Ractive({ ... });
var r1 = new Ractive({ ... });
var r2 = new Ractive({ ... });
app.addChild(r1).addChild(r2);

Open question would be whether implicit data lookup is enough, or should "mappings" be specifiable via name/value pairs identical to attribute bindings:

var app = new Ractive({ ... });
var r1 = new Ractive({ ... });
app.addChild(r1, { items: 'library.books', expanded: 'some.state'; );
evs-chris commented 8 years ago

@martypdx I was just about to open another discussion ticket for your option 2 :smile:. I've been pondering for a while about how to easily add components and mappings to a host ractive instance. I have a few components where things are dynamic enough in their use that setting them up through the template is more than a little cumbersome. It would be nice to be able to just new up a component in an event handler, attach it to your instance somehow, and throw a few mappings at it. I think that would nicely solve most of the issues that ractive-ractive attempts to address, especially if the parent doesn't have to care about the child DOM. I was also thinking about a slight extension of that that would supply an anchor/content virtual DOM node where child components could be attached from the JS side.

Addressing your first post:

For the firebase case, would having the adaptor registered with the model as "the adaptor controls all child keypaths" and calling get with the path from the adaptor point to the target keypath suffice? Or would having a flag for branching work? I've not really done anything with firebase, so I'm not entirely sure how it works. I was trying to get it sorted in my head how ractive could indicate whether the model was meant to be an intermediary or an endpoint (or both). Perhaps just passing along whether or not the model has deps would work?

For item 2, I'm with you in that I think that's not the best idea, but it would still be covered by an adaptor that manages a model with a depth of 0.

I'm planning to do an adaptor branch based on my opening post and some of the feedback here when I get a little more free time.

Jeff17Robbins commented 8 years ago

@martypdx Option 2 is intriguing, as it mirrors what we currently do with the Ractive-Ractive adaptor. If I understand your example code:

app.addChild(r1)

might be equivalent to

view.set('some prefix', store)

where view and store are ractive instances connected via the Ractive-Ractive adaptor. Assuming that your app is, in effect, the store and your r1 is a view?

But suppose I have multiples stores? I want a view to be able to mustache to different stores, each with its own some prefix in the view's data model. Would your Option 2 support this?

martypdx commented 8 years ago

But suppose I have multiples stores? I want a view to be able to mustache to different stores, each with its own some prefix in the view's data model. Would your Option 2 support this?

@Jeff17Robbins if I understand you correctly, you just want to map the data differently for each child component? If so, you could do this:

var app = new Ractive({
    data: { foo: {...} }
});
var v1 = new View1({ template: '{{bar}}' });
var v2 = new View2({ template: '{{qux}}' });
app.addChild(v1, { bar: 'foo' });
app.addChild(v2, { qux: 'foo' });

The object map is the equivalent of the attribute name value pairs: app.addChild(component, { bar: 'foo' }); same as <component bar='{{foo}}'></component>.

There is a slight difference in that if the api takes an instance than the mapped data would be supplied after instantiation. If that's an issue, we could switch to using a constructor and options before mappings:

var Component = Ractive.extend({ template: '{{bar}}' });
var component = app.addChild( Component, { el: '#component' }, { bar: 'foo' } );

I heavier api, but it does allow same process for component instantiation. And would allow token-based resolution as well:

app.components['my-widget'] = Ractive.extend({ template: '{{bar}}' });
var component = app.addChild( 'my-widget', { el: '#modal' }, { bar: 'foo' } );