9

rivets.js newbie here. I want to bind to an item that will be changing dynamically (store.ActiveItem). I tried the following approach, but although store.ActiveItem is set, store.ActiveItem.(any property) is always undefined. Is there a standard way to bind deeper than one level?

<div id="editItemDialog" data-modal="store.ActiveItem < .ActiveItem">
    <a data-on-click="store:ClearActiveItem" href="#">close - works</a>
    <div>
        <div>
        <label>name:</label><input data-value="store.ActiveItem.Name < .ActiveItem"/>
        </div>
        <div>
        <label>price:</label><input data-value="store.ActiveItem.Price < .ActiveItem"/>
        </div>
        <div>
        <label>description:</label><textarea data-value="store.ActiveItem.Description < .ActiveItem"></textarea>
        </div>
    </div>
</div>
Daniel
  • 920
  • 1
  • 11
  • 22

1 Answers1

23

How the binding works is largely determined by the Rivets adapter you are using, although your model could also do the heavy lifting.

Option 1: Smart Model

If you're using Backbone.js, you could take a look at backbone-deep-model, which supports path syntax for nested attributes (Eg. store.get('ActiveItem.Price')), though it is still under development. If that doesn't quite meet your needs, there are other nested model-type options on the Backbone plugins and extensions wiki.

Option 2: Smart Adapter

If that doesn't work for you, you can extend your Rivets adapter to handle path syntax. I've put together a simple example on how to do this at http://jsfiddle.net/zKHYz/2/ with the following naive adapter:

rivets.configure({
    adapter: {
        subscribe: function(obj, keypath, callback) { /* Subscribe here */ },
        unsubscribe: function(obj, keypath, callback) { /* Unsubscribe here */ },
        read: function(obj, keypath) {
            var index = keypath.indexOf('.');
            if (index > -1) {
                var pathA = keypath.slice(0, index);
                var pathB = keypath.slice(index + 1);
                return obj[pathA][pathB];
            } else {
                return obj[keypath];
            }
        },
        publish: function(obj, keypath, value) {
            var index = keypath.indexOf('.');
            if (index > -1) {
                var pathA = keypath.slice(0, index);
                var pathB = keypath.slice(index + 1);
                return obj[pathA][pathB] = value;
            } else {
                return obj[keypath] = value;
            }
        }
    }
});

Option 3: Dirty Hacks

As of version 0.3.2, Rivets supports iteration binding. By creating a Rivets formatter that returns an array, you can "iterate" over your property. Take a look at http://jsfiddle.net/mhsXG/3/ for a working example of this:

rivets.formatters.toArray = function(value) {
    return [value];
};

<div data-each-item="store.ActiveItem | toArray < store.ActiveItem"">
    <label>name:</label><input data-value="item.Name < store.ActiveItem"/>
    ...
</div>

I'm not sure if the computed property syntax is required here; you will have to test this with your model to see what works.

Option 4: Don't bind deeper than one level (Recommended)

The need to bind deeper than one level may be an indication that your design can be improved.

In your example, you have a list of Items in an ItemCollection for a Store. You go about assigning a single Item to the Store's ActiveItem property, setting up events everywhere to try link things together, and then need to be able to bind to the properties of the ActiveItem under the Store, yet have things update whenever the ActiveItem itself changes, etc..

A better way of doing this is by using a view-per-model approach. In your example, you're trying to handle the Store Model, the ItemCollection and the Item Model with a single view. Instead, you could have a parent Store view, a subview for the ItemCollection, and then generate Item views as necessary below that. This way, the views are easier to build and debug, less tightly coupled to your overall Model design, and are more readily reusable throughout your application. In this example, it also simplifies your Model design, as you no longer need the ActiveItem property on the Store to try maintain state; you simply bind the Item View to the selected Item Model, and everything is released with the Item View.

If you're using Backbone.js, take a look at Backbone.View as a starting point; there are many examples online, although I'll be the first to admit that things can get somewhat complex, especially when you have nested views. I have heard good things about Backbone.LayoutManager and how it reduces this complexity, but have not yet had the chance to use it myself.

I've modified your most recent example to use generated Item views at http://jsfiddle.net/EAvXT/8/, and done away with the ActiveItem property accordingly. While I haven't split the Store view from the ItemCollection view, note that I pass their Models into Rivets separately to avoid needing to bind to store.Items.models. Again, it is a fairly naive example, and does not handle the full View lifecycle, such as unbinding Rivets when the View is removed.

Ashley Ross
  • 2,345
  • 2
  • 23
  • 43
  • Thanks for the great leads. I lost the hard drive where I was working on my rivets demo, and I'm just barely building up my demo again. Looking forward to using your suggestions. – Daniel Oct 23 '12 at 22:50
  • Is there a good way to initialize with the ActiveItem as null and then set it dynamically? Your example shows how it could bind statically. – Daniel Oct 26 '12 at 23:16
  • My simple examples just use basic POJSO models, so they don't support change notifications. Try using Backbone models and updating the adapter to subscribe to Backbone's events (Rivets has an example on how to do this), and see how that goes. If you need to hide fields if ActiveItem is null, you could create an *isNull* Rivets formatter, and then using it like `
    ...
    `
    – Ashley Ross Oct 27 '12 at 12:21
  • Could you possibly give me the link to the Rivets example? This is my key issue: supporting change notifications on a dynamically changing model. Thanks. – Daniel Nov 02 '12 at 22:41
  • The [Rivets example adapter](http://rivetsjs.com/#configure) is aimed at Backbone, and will work with the backbone-deep-model mentioned in Option 1. You may be able to get away with a normal Backbone model and Option 3, so long as the iterated-over property is another Backbone model. If it isn't, you won't get change notification support and you may also have to add POJSO support to the Rivets adapter's `read` and `publish` functions (Although I could be wrong - I haven't tested this). – Ashley Ross Nov 03 '12 at 09:34
  • I played around with this some more but wasn't able to get it working. The tough part (for me) is having rivets recognize the dynamic item has changed. Here is the (mostly broken) project as of now - http://jsfiddle.net/EAvXT/4/ – Daniel Nov 26 '12 at 22:41
  • Ah, your example gives a clearer picture of your use case - I'll update my answer accordingly. DeepModel was updated to require an Underscore mixin, which is maybe why your code wasn't working. With the mixin and the latest version, however, DeepModel causes a stack overflow when you attach event handlers to a Model and later add it to a DeepModel (As far as I can tell). I've rewritten your example without DeepModel, and implemented the Option 4 approach instead: http://jsfiddle.net/EAvXT/8/ – Ashley Ross Dec 29 '12 at 23:54
  • Thanks for your help: I agree with your conclusion and recommended design. This has been a good learning experience! I noticed the Description isn't working, but that should be simple to fix. Thanks again... – Daniel Jan 02 '13 at 22:38