Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow binding nested Ractive objects #74

Closed
conradz opened this issue Jul 25, 2013 · 25 comments
Closed

Allow binding nested Ractive objects #74

conradz opened this issue Jul 25, 2013 · 25 comments

Comments

@conradz
Copy link

conradz commented Jul 25, 2013

Is it currently possible to bind nested Ractive objects? I was looking at the source and while it didn't look like it would be hard to support, I couldn't find anything that does it.

What I'm trying to do is have Ractive objects in the "root" Ractive object, and then bind to them from the template. The output would be the rendered (and bound) template of the "child" Ractive object.

Example JS:

var ractive = new Ractive({
  template: "Hello {{recipient}}",
  el: myElement,
  data: {
    recipient: new Ractive({
      template: "{{name}}!",
      data: { name: "World" }
    })
  }
});

The output from this would be "Hello World!".

This would be really useful for reducing template duplication, and would allow breaking up the views into sub views. It also helps a lot when having lists (for example, each row in a table could be its own Ractive object with its own logic and template).

@Rich-Harris
Copy link
Member

It's something I've thought about - it would indeed to great to keep modules separate but update them all simultaneously. I haven't quite figured out an approach I really like yet.

You can use partials however. There's some info about it on the wiki, and a tutorial here - hopefully this will do what you need.

I'll leave this issue open for the time being to remind me to think about it some more!

@conradz
Copy link
Author

conradz commented Jul 25, 2013

I saw that I can use partials and they can help, however, that's not quite as powerful, since it's only the template that is encapsulated in the partials, not things like event handlers and data.

@jpellerin
Copy link

+1 I think the ability to nest Ractive instances will be really useful for building complex applications. If they can be nested then they really become reusable widgets, encapsulating a view and behaviors.

@jonathanmarvens
Copy link

👍 ...what @jpellerin said. I'd actually argue that, for the sake of promoting, encouraging, and allowing reusability, this would be beneficial for any type of application...complex or simple.

@Rich-Harris
Copy link
Member

Thanks for your input everyone, it's very helpful - looks like this is a popular idea. If anyone has any specific use cases they can share, it would help me/us figure out the best design.

@gliese1337
Copy link

I was about half-way through writing my own custom templating & data bindings library when I discovered Ractive and realized it does 90% of what I want already- this is one of the things I'm missing, though.

What I'd find particularly useful is if nested property objects were automatically turned into sub-Ractives. E.g., such that given

var ractive = new Ractive({el: my_element, template: my_template, data: { nested_data: { name: "Bob" } } });

ractive.data.nested_data is itself a ractive containing the binding for "name".
The purpose is to be able to take a small piece of the complete view-model and pass it to another part of the program that doesn't need to know about the rest of it.

For purposes of explicitly nesting Ractives within each other by passing one in as data to another, it would be really nice to have some kind of compiled-but-not-instantiated ractive (combination of template, initial data, and any observers / event listeners) that could be used to instantiate multiple copies of a Ractive widget without having to re-do the behavior setup every time. (It seems to me that creating subclasses with .extend() comes close to this idea, but just falls short of reaching it without the nesting component).

@codler
Copy link
Member

codler commented Jul 31, 2013

+1

@conradz
Copy link
Author

conradz commented Jul 31, 2013

@gliese1337 I suppose when you say "pass [the nested ractive object] to another part of the program", you want to observe the updates to a property in the object? Otherwise passing the plain object would work. This would probably require separating the change event handling and the template/binding of a Ractive, since normally it is expected that a Ractive has an associated template, but it would not know what template to use if it created them automatically.

I personally don't see the need for automatically creating sub-ractive objects, since normally they would need there own (explicitly defined) Ractive class with the associated template, event handlers, etc.

@gliese1337
Copy link

@conradz Either observe updates, or cause updates. Just passing the plain object doesn't work if you want to call .set().
An automatically generated sub-ractive should be able to share the same template as its parent, it would merely only expose properties in some sub-branch of the data tree.

@1N50MN14
Copy link

1N50MN14 commented Aug 6, 2013

+1

@Rich-Harris
Copy link
Member

Hi there early adopter friends,

I've had a go at implementing some of these ideas. This is a first stab, so nothing in here is written in stone - at this stage I'd love to get some feedback on whether you think this is the right direction.

I'm calling it 'components', as the idea is fairly similar to Web Components (also Ember.Component).

Here it is in action (click on the template/javascript tabs above the readme to see how it was put together).

Basically the idea is that you define a component constructor using Ractive.extend()...

MyWidgetConstructor = Ractive.extend({
  template: '<h2>I am a widget!</h2>'
  init: function ( options ) {
    // any widget-specific setup goes here...

    this.specialWidgetMethod();
  },
  specialWidgetMethod: function () {
    // as the design stands, there is no way to get a reference to this
    // widget instance, so all the behaviour needs to be set up during
    // the init sequence - you can't call this method externally
  }
});

... and register it like so:

ractive = new Ractive({
  el: 'main',
  template: '#tpl',
  components: {
    widget: MyWidgetConstructor
  }
});

// or by doing Ractive.components.widget = MyWidgetConstructor, though this isn't implemented yet

Then you can include the component like this:

<h1>Here is my widget</h1>
<rv-widget/>

In this case, the widget is completely isolated from the surrounding context. However we can create an interface like so:

<rv-widget foo='bar' size='5' options='{"some":"json"}' dynamic='{{someData}}'/>

This would be the equivalent of doing the following (in fact this is basically what happens under the hood):

widget = MyWidgetConstructor({
  el: theParentElement,
  append: true,
  data: {
    foo: 'bar',
    size: 5,
    options: {
      some: 'json'
    },
    dynamic: someValue // whatever {{someData}} resolves to
  }
});

Whenever someData changes, the dynamic property of the widget's model will also change. Changes are bi-directional - if the widget changes the value of dynamic (in response to user action, for example), the value of someData in the parent ractive's model will change.

We can also listen to events coming from the widget:

<rv-widget on-foo='bar'/>

Then, whenever the widget fires the foo event, the parent ractive will fire the bar event with the same arguments.

So, that's about the size of it so far. Would love to know if you all think this is the right way to go about solving this (rather tricky!) problem.

@weepy
Copy link

weepy commented Aug 7, 2013

Where does the name of the tag 'rv-widget' get specified ?

On Wed, Aug 7, 2013 at 3:14 PM, Rich-Harris notifications@git.luolix.topwrote:

Hi there early adopter friends,

I've had a go at implementing some of these ideas. This is a first stab,
so nothing in here is written in stone - at this stage I'd love to get some
feedback on whether you think this is the right direction.

I'm calling it 'components', as the idea is fairly similar to Web
Components (also Ember.Component).

Here it is in action http://www.ractivejs.org/examples/components/(click on the template/javascript tabs above the readme to see how it was
put together).

Basically the idea is that you define a component constructor using
Ractive.extend()https://github.com/Rich-Harris/Ractive/wiki/Ractive.extend%28%29
...

MyWidgetConstructor = Ractive.extend({
template: '

I am a widget!

'
init: function ( options ) {
// any widget-specific setup goes here...

this.specialWidgetMethod();

},
specialWidgetMethod: function () {
// as the design stands, there is no way to get a reference to this
// widget instance, so all the behaviour needs to be set up during
// the init sequence - you can't call this method externally
}});

... and register it like so:

ractive = new Ractive({
el: 'main',
template: '#tpl',
components: {
widget: MyWidgetConstructor
}});
// or by doing Ractive.components.widget = MyWidgetConstructor, though this isn't implemented yet

Then you can include the component like this:

Here is my widget

In this case, the widget is completely isolated from the surrounding
context. However we can create an interface like so:

This would be the equivalent of doing the following (in fact this is
basically what happens under the hood):

widget = MyWidgetConstructor({
el: theParentElement,
append: true,
data: {
foo: 'bar',
size: 5,
options: {
some: 'json'
},
dynamic: someValue // whatever {{someData}} resolves to
}});

Whenever someData changes, the dynamic property of the widget's model
will also change. Changes are bi-directional - if the widget changes the
value of dynamic (in response to user action, for example), the value of
someData in the parent ractive's model will change.

We can also listen to events coming from the widget:

Then, whenever the widget fires the foo event, the parent ractive will
fire the bar event with the same arguments.

So, that's about the size of it so far. Would love to know if you all
think this is the right way to go about solving this (rather tricky!)
problem.


Reply to this email directly or view it on GitHubhttps://github.com/Rich-Harris/Ractive/issues/74#issuecomment-22254044
.

@codler
Copy link
Member

codler commented Aug 7, 2013

Fantastic! This is great! But I wonder if it can be in Mustache syntax and maybe replace partials?

@codler
Copy link
Member

codler commented Aug 7, 2013

@weepy , from here

components: {
  widget: MyWidgetConstructor
}

@Rich-Harris
Copy link
Member

@weepy - right now, there's a fairly crude mechanism: at parse time, any elements whose tag names begin rv- are treated as components by the renderer, rather than as elements. Then, as @codler notes, it looks for constructors of the same name (without the rv-) registered in the components map.

Incidentally, this was just a simple hack to get up and running. In the longer term I don't know if prepending all component names like this makes sense - it's certainly useful to know straight away if something is an element or a component (without having to look up every tag name the renderer comes across), but the rv- thing is a little bit ugly. We'll see.

@codler I thought about that - initially I tried something like {{>DonutChart}} (distinguishing between components and normal partials with the capital 'D') instead of <rv-donutchart/>.

The problems came when defining the interface between the parent ractive and the component, I found myself contorting the syntax into all sorts of weird shapes. Fundamentally the problem is that components don't really behave like partials - components are isolated blocks of functionality with their own models, whereas partials are just shorthand - they inherit the model from the surrounding context. For components to be truly reusable I think they need to be isolated in this way, and without normal partials Ractive wouldn't be backwards compatible with normal Mustache templates (which is one of the design goals).

But it's interesting that we both leaned in that direction initially. Can you describe what the benefits would be, as you see them?

@codler
Copy link
Member

codler commented Aug 7, 2013

I suggested Mustache syntax because it would be more consistent but maybe it is not possible.
I havent used Mustache anywhere untill I found Ractive :) So I have now looked on the Mustache syntax/spec more closely and saw lambda exist.

I have been thinking and came up with an illustrative example on how we maybe could use lambda think

js

ractive = new Ractive({
  el: 'main',
  template: '#tpl',
  data: {
    nested: function() {
      return Ractive.extend({
        data: {
          name: 'Hello'
        }
      })
    }
  }
})

tpl

{{#nested}}
  {{name}}
{{/nested}}

output

Hello

@gliese1337
Copy link

I like this addition. I've got several immediate uses for it. It doesn't feel like something I would want to use for a one-off purpose, though. E.g., embedding a sub-ractive as the body content of a modal dialog ractive- I wouldn't want to create a component constructor for the dialog body, because it's never going to be used anywhere else. That's kind of an inverse situation- rather than a re-usable component embedded in a larger custom template, there's a smaller custom template embedded in a larger reusable component. For that kind of use case, it makes a lot more sense to just provide the smaller embedded ractive object as part of the data.

@weepy
Copy link

weepy commented Aug 8, 2013

To what extent can I write a fully reusable Reactive component. E.g. ValidatedInputControl that a 3rd party can just include in their project and use?

@kivikakk
Copy link
Contributor

kivikakk commented Nov 6, 2013

I've added a stub glossary entry for components just so people can be made aware of their existence other than by stumbling upon this issue/the examples page (as I did). Hope this isn't too forward!

@dshimkoski
Copy link

I wasn't aware of this feature, so this happened...

https://github.com/dshimkoski/ractive-tree
https://github.com/dshimkoski/ractive-tree/blob/master/example.html

This approach is a bit different from the one above though. The code basically rips all ractives out of the DOM, children first, sticks them in temporary hidden elements, then inits everything from the root up, creating parent/child links in the ractives along the way.

This isolates each ractive. One might be embedded in another, but that's the only relationship they have by default. I like this because there's no magic involved. You could still have a child ractive look to its parent for data, but this would have to be configured explicitly.

Another benefit of this approach is there would be no need to introduce another concept, i.e., no 'components'. It's just ractives all the way down.

I dislike the code I posted because it establishes parent/child relationships with hacky DOM stuff when you're already figuring this stuff out inside the library.

Instead of this, it would be nice if I could provide a factory that Ractive calls when it encounters a new element. If the factory returns false, it's a normal element, otherwise a Ractive subclass is expected. Parent/child links are updated and that's it.

With a tree of ractives all rigged up, I can hang additional functionality on it without requiring any changes to the library. I can probably do most of that stuff in the factory of course, but if I need to traverse the tree for some reason, it would be available to me.

@monsanto
Copy link
Contributor

@Rich-Harris is something supposed to happen when I hover over the donut charts? Nothing happens on Firefox 25 or Chrome 31 on Ubuntu 13.10

JavaScript Console says http://www.ractivejs.org/examples/components/libs.js is a 404

@kivikakk
Copy link
Contributor

@monsanto see #245 :)

@Rich-Harris
Copy link
Member

Closing this issue as most of the functionality discussed here is now available in components. There are still a few problems to work out, which deserve their own issues eventually, but for now I'll jot a few thoughts down:

What to do with markup inside a component?

It would be really great to be able to do stuff like this:

<leaflet lat='{{lat}}' lon='{{lon}}' zoom='{{zoom}}'>
  <leaflet-tilelayer tiles='http://{s}.tile.cloudmade.com/API-key/997/256/{z}/{x}/{y}.png'/>
  <leaflet-marker lat='51.5' lon='-0.09' popup='<b>Hello world!</b><br>I am a popup.'/> 
</leaflet>

At the moment, markup inside a component is effectively 'swallowed'.

Finding components

We could argue that components should be entirely self-contained and data-driven, but the reality is it is sometimes necessary to interact with a component directly within an app. Right now, there's no way to find a component, so there's no way to interact with it - the closest we can get is

widgets = [];

Ractive.components.widget = Ractive.extend({
  init: function () {
    widgets.push( this );
  },
  foo: function ( bar ) {
    alert( bar );
  }
});

// later...
widgets[4].foo( 'woo-hoo!' );

There's a ractive.find() method which used to delegate to document.querySelector() but now traverses the parallel DOM instead. This could be extended to work with components (e.g. ractive.find('widget')).

Access to parent data context

It's a pain to have to do stuff like this:

<widget foo='{{foo}}' bar='{{bar}}'/>

At the same time, it's good to be able to treat components as isolated sandboxes. There's a useful analogue here which is JavaScript closures - a function can access (and modify) variables from parent scopes, but variables declared inside it are free from external meddling. Would be neat to have the same thing with components.

Arbitrary render methods

Right now, components are assumed to work the same as Ractive as a whole - template and data goes in, DOM comes out. It would be nice to do away with this assumption, so that a Ractive component could describe (for example) a canvas chart or (why not?) a three.js scene. This, and the first point above (the leaflet.js example) would be a win from an app developer's perspective because the interface for working with UI components would be separate from the technologies and libraries used - more power, less to learn.

That's all that springs to mind, though doubtless there are other ideas that will also deserve attention. Obviously feel free to carry on the discussion on this thread.

@monsanto
Copy link
Contributor

What to do with markup inside a component?

Make partials first class (see #236 (comment)). Bind _body to a partial representing the body of the component. Then,

x = Ractive.extend({
    ...
    template: '<div>{{>_body}}</div>'
    ....
})

Or, since partials are first class, we could transform them by rendering an expression:

x = Ractive.extend({
    ...
    template: '<div>{{> foo == 2 ? f(_body) : "Please set foo to 2"  }}</div>'
    ....
})

Finding components

#280 suggests a solution to this, by repurposing the id and class parameters.

@codler
Copy link
Member

codler commented Dec 1, 2013

Finding components
...
There's a ractive.find() method which used to delegate to document.querySelector() but now traverses the parallel DOM instead. This could be extended to work with components (e.g. ractive.find('widget')).

Since ractive.find returns a node and a componant instance is not a node it would be strange if find would return different "datatype".
Maybe we could have something like ractive.getComponantsByName('widget'), not the best name and it feels old but that could be an alternative :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests