Noob question about REST and models


#1

I’ve been playing with Ember, Ember Data, and Handlebars for a few days now, and it has been extremely frustrating. It seems there’s a lot of magic going on, and not a lot of info or examples that I can wrap my head around, so I’m turning to these forums.

The first question I have is in regards to understanding how data is loaded. I have a route like this:

Neutrinoapp.Router.map(function() {
    this.resource('sensorgroups', { path: '/' });
});

The documentation led me to believe that this means loading the web root will trigger the handlebars template named ‘sensorgroups’:

<script type="text/x-handlebars" data-template-name="sensorgroups">
    {{#each sensorgroup in model}}
        ...
    {{/each}}
    {{#each sensor in model}}
        ...
    {{/each}}
</script>

This is true, however, if this data-template-name doesn’t also match the pluralized name of a model, no REST api call is made to fetch data. In the above example, the ‘each’ on sensorgroup works, but no data is in the second ‘each’. I’ve extended routes for all of my models, and if I change data-template-name to ‘sensors’, it loads those and the second ‘each’ works but not the first. It seems there is some magic going on here with naming, and after playing awhile I can’t figure out how to get it to request anything from my API other than the one matching the data-template-name, so I stick to the hb template name of ‘sensorgroups’, even though I’d prefer to name it ‘home’ or something similar.

The documentation also indiciates that it will auto-load any necessary model data upon request, but this doesn’t seem to be happening when I call ‘#each sensor in model’, for example.

This has led me down a path where I’m sideloading all of my API data with the /api/sensorgroup resource, essentially making my API a single URL. Related to this, the ‘sensorgroup’ model refers to multiple other models via a hasMany relationship, and the dependencies aren’t resolved unless I sideload them, again making my API return everything from just one URL. I would hope Ember would be smart enough to realize that this model requires other models and fetch that data if necessary, but I’m not seeing it.

I really kind of detest the sideloading paradigm, I understand that it is a bit more efficient, but I feel like it clutters the API by returning things that weren’t asked for, so if I can get away from it I’d prefer that.

A second question, it seems like you should be able to do an arbitrary each loop with handlebars, something like:

{{#each thing in ["a","b","c"]}}

However, I’ve tried the above, I tried formally defining a javascript variable and using that, and can’t get anything of the sort to work. I keep seeing references to ‘using this context’ in the documentation, with no examples about how it applies the context to the helper. It appears with Ember that the context is coming from model, meaning that I have to go to the trouble of storing something in the model in order to do a simple loop like this, even though this array is not really api data but layout-specific details that are easily handled in a loop. I did get it to work finally, but I had to write a transform for ‘array of strings’, and then put that in a model. That’s a little heavy for what should be something simple. I looked at writing my own helper, but I’d still have to have context to work on.

Thanks in advance for any tips.


#2

Quick comment on #each-

You can’t directly iterate over (or use conditionals with) Javascript variables. Handlebars helpers are looking for controller properties. Example:

// Controller
tempArray: ["a", "b", "c"]

// Template
{{#each thing in tempArray}}
    <p>{{thing}}</p>
{{/each}}

Templates always read directly from controllers- they don’t know or care if they came from the model, were native controller properties, computed properties- whatever.


#3

Ok, thanks a lot for that. With this info I noticed via the chrome ember plugin that we were autogenerating a ‘sensorgroups’ array controller for the template. I manually created this:

Myapp.SensorgroupsController = Ember.ArrayController.extend({
    things: ["a","b","c"]
});

And I can fetch ‘things’ in my template now.

I’m still confused as to why this controller has its own ‘model’ containing sensorgroups pulled from the REST API, and how to get the other items into the model. Since this is an ArrayController I assume it’s configured to key off of the name and try to fetch an array for the template’s model.


#4

Ok, I think I have figured it out. Here’s what I did.

Myapp.Router.map(function() {
    this.resource('home', { path: '/' });
});

Myapp.HomeRoute = Ember.Route.extend({
setupController: function(controller) {
        controller.set('hControllers', this.store.find('hcontroller'));
        controller.set('sGroups', this.store.find('sgroup'));
        controller.set('statsStrings', ["a","b","c"]);
    }
});

Now I can pull any item I want from the controller, and this.store.find will fetch the required models from the API. This has helped me cement how things are put together. I had read through the docs, but there’s a lot there and most of it didn’t make a whole lot of sense in just reading. I did the Todos example, but it wasn’t very helpful in understanding how things are linked.


#5

That will work, but I think you still may be making this more complicated than you need to.

The primary functions of a Route object are to apply a model to a controller and render a view. Once the model is applied to the controller, you can think of it as just being part of the controller for the most part (until 2.0 at least, then you’ll need to be a little more explicit while using model properties in a controller).

In your example, you’re applying multiple models to the same controller. While this is legal, there are better patterns for it. For example, you can use RSVP to manage the async a little more explicitly:

// Route
model: function(){
    return Ember.RSVP.hash({
        "hControllers": this.store.find("hcontroller"),
        "sGroups": this.store.find("sgroup")
    });
},
setupController: function(controller, model){
    controller.set("model", model) // Access each thing in the controller with model.hControllers, etc
    //Or...
    // controller.set("hControllers", model.hControllers);
    // controller.set("sGroups", model.sGroups);
}

What I actually prefer is to use a separate model object to abstract out the combinations for a particular route.

// Abstraction model
hControllersINeedForThisPage: DS.hasMany("hcontroller"),
sGroupsINeedForThisPage: DS.hasMany("sgroup")

// Data
{
    abstraction: {
        id: 1,
        hControllersINeedForThisPage: [2,5,8,9],
        sGroupsINeedForThisPage: [2,3,6,7],
    }
}

// Route
model: function(){
    return this.store.find("abstraction", 1);
}

If you have control over your API, you can get a ton of bang for your buck out of sideloading with this strategy. Sideload all of the IDs you’re referencing, and you get all the data you need with one request. Just a word of warning to save you the troubleshooting time down the road if you do this- all relational data is always async. Use promises (.then) to resolve any properties that refer to other models before you try to do anything with the data.

Good hunting!


#6

Wow, thanks for a detailed response. I might need to have a go at one of those. I’m having a different problem now, that the data from controller.set is not coming through in time for one of the views I have to use it. The didInsertElement fires on the view, and the models are still undefined when I try to console.log them. If I wrap that in a setTimeout it all works. I thought I might get around this by ignoring didInsertElement and setting an observable instead, but it didn’t like the undefined object, either.


#7

Rearranging it to use RSVP seems to have fixed the async issue. I’m trying to find more info about what RSVP is and does, but thanks.


#8

RSVP is a library for wrapping async behavior. Anything that could theoretically require a request to an external source is async, and Ember expects you treat it that way. Use .then() to handle any dependent behavior in callbacks.

Heads up, didInsertElement is a view event- there is about a 0.01% chance that you should be handling any data operation there. If you have a timing issue in your view, one might ask you’re having an async handling issue in your controller or route.