EmberJS / Ember Data में एकल मार्ग के साथ कई मॉडल का उपयोग कैसे करें?


85

डॉक्स पढ़ने से, ऐसा लगता है कि आपको एक मॉडल को इस तरह से रूट करना (या करना) चाहिए:

App.PostRoute = Ember.Route.extend({
    model: function() {
        return App.Post.find();
    }
});

क्या होगा अगर मुझे एक निश्चित मार्ग में कई वस्तुओं का उपयोग करने की आवश्यकता है? यानी पोस्ट, टिप्पणियाँ और उपयोगकर्ता? मैं उन्हें लोड करने का मार्ग कैसे बताऊँ?

जवाबों:


117

अंतिम अपडेट हमेशा के लिए : मैं इसे अपडेट नहीं रख सकता। तो यह पदावनत है और इस तरह से होगा। यहाँ एक बेहतर, और अधिक अप-टू-डेट धागा EmberJS है: एक ही मार्ग पर कई मॉडल कैसे लोड करें?

अद्यतन: अपने मूल उत्तर में मैंने embedded: trueमॉडल परिभाषा में उपयोग करने के लिए कहा । यह गलत है। संशोधन 12 में, एम्बर-डेटा को विदेशी कुंजियों को एकल रिकॉर्ड या संग्रह के लिए एक प्रत्यय ( लिंक ) के साथ परिभाषित करने की उम्मीद है । निम्नलिखित के समान कुछ:_id_ids

{
    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]
}

मैंने फ़िडेल को अपडेट किया है और यदि ऐसा कुछ भी बदलता है या यदि मुझे इस उत्तर में दिए गए कोड के साथ अधिक मुद्दे मिलते हैं तो मैं फिर से ऐसा करूंगा।


संबंधित मॉडल के साथ उत्तर दें:

परिदृश्य आप का वर्णन कर रहे हैं के लिए, मैं पर भरोसा होता संघों मॉडलों के बीच (सेटिंग embedded: true) और केवल लोड Postकि मार्ग में मॉडल, मैं एक परिभाषित कर सकते हैं पर विचार DS.hasManyके लिए संघ Commentमॉडल और DS.belongsToके लिए संघ Userदोनों में Commentऔर Postमॉडल। कुछ इस तरह:

App.User = DS.Model.extend({
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    email: DS.attr('string'),
    posts: DS.hasMany('App.Post'),
    comments: DS.hasMany('App.Comment')
});

App.Post = DS.Model.extend({
    title: DS.attr('string'),
    body: DS.attr('string'),
    author: DS.belongsTo('App.User'),
    comments: DS.hasMany('App.Comment')
});

App.Comment = DS.Model.extend({
    body: DS.attr('string'),
    post: DS.belongsTo('App.Post'),
    author: DS.belongsTo('App.User')
});

This definition would produce something like the following:

Associations between models

With this definition, whenever I find a Post, I will have access to a collection of comments associated with that post, and the comment's author as well, and the user which is the author of the post, since they are all embedded. The route stays simple:

App.PostsPostRoute = Em.Route.extend({
    model: function(params) {
        return App.Post.find(params.post_id);
    }
});

So in the PostRoute (or PostsPostRoute if you're using resource), my templates will have access to the controller's content, which is the Post model, so I can refer to the author, simply as author

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>{{title}}</h3>
    <div>by {{author.fullName}}</div><hr />
    <div>
        {{body}}
    </div>
    {{partial comments}}
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    {{#each content.comments}}
    <hr />
    <span>
        {{this.body}}<br />
        <small>by {{this.author.fullName}}</small>
    </span>
    {{/each}}
</script>

(see fiddle)


Answer with non-related models:

However, if your scenario is a little more complex than what you described, and/or have to use (or query) different models for a particular route, I would recommend to do it in Route#setupController. For example:

App.PostsPostRoute = Em.Route.extend({
    model: function(params) {
        return App.Post.find(params.post_id);
    },
    // in this sample, "model" is an instance of "Post"
    // coming from the model hook above
    setupController: function(controller, model) {
        controller.set('content', model);
        // the "user_id" parameter can come from a global variable for example
        // or you can implement in another way. This is generally where you
        // setup your controller properties and models, or even other models
        // that can be used in your route's template
        controller.set('user', App.User.find(window.user_id));
    }
});

And now when I'm in the Post route, my templates will have access to the user property in the controller as it was set up in setupController hook:

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>{{title}}</h3>
    <div>by {{controller.user.fullName}}</div><hr />
    <div>
        {{body}}
    </div>
    {{partial comments}}
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    {{#each content.comments}}
    <hr />
    <span>
        {{this.body}}<br />
        <small>by {{this.author.fullName}}</small>
    </span>
    {{/each}}
</script>

(see fiddle)


15
Thank you so much for taking the time to post that, I found it really useful.
Anonymous

1
@MilkyWayJoe, really good post! Now my approach looks really naïve :)
intuitivepixel

The problem with your non-related models is that it doesn't accept promises like the model hook does, right? Is there any workaround for that?
miguelcobain

1
If I understand your problem correctly, you can adapt it in an easy way. Just wait for the promise to fulfill and only then set the model(s) as a variable of the controller
Fabio

Other than iterating and displaying comments, it will be lovely if you could show an example of how someone might add an new comment into post.comments.
Damon Aw

49

Using Em.Object to encapsulate multiple models is a good way to get all data in model hook. But it can't ensure all data is prepared after view rendering.

Another choice is to use Em.RSVP.hash. It combines several promises together and return a new promise. The new promise if resolved after all the promises are resolved. And setupController is not called until the promise is resolved or rejected.

App.PostRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      post:     // promise to get post
      comments: // promise to get comments,
      user:     // promise to get user
    });
  },

  setupController: function(controller, model) {
    // You can use model.post to get post, etc
    // Since the model is a plain object you can just use setProperties
    controller.setProperties(model);
  }
});

In this way you get all models before view rendering. And using Em.Object doesn't have this advantage.

Another advantage is you can combine promise and non-promise. Like this:

Em.RSVP.hash({
  post: // non-promise object
  user: // promise object
});

Check this to learn more about Em.RSVP: https://github.com/tildeio/rsvp.js


But don't use Em.Object or Em.RSVP solution if your route has dynamic segments

The main problem is link-to. If you change url by click link generated by link-to with models, the model is passed directly to that route. In this case the model hook is not called and in setupController you get the model link-to give you.

An example is like this:

The route code:

App.Router.map(function() {
  this.route('/post/:post_id');
});

App.PostRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      post: App.Post.find(params.post_id),
      user: // use whatever to get user object
    });
  },

  setupController: function(controller, model) {
    // Guess what the model is in this case?
    console.log(model);
  }
});

And link-to code, the post is a model:

{{#link-to "post" post}}Some post{{/link-to}}

Things become interesting here. When you use url /post/1 to visit the page, the model hook is called, and setupController gets the plain object when promise resolved.

But if you visit the page by click link-to link, it passes post model to PostRoute and the route will ignore model hook. In this case setupController will get the post model, of course you can not get user.

So make sure you don't use them in routes with dynamic segments.


2
My answer applies to an earlier version of Ember & Ember-Data. This is a really good approach +1
MilkyWayJoe

11
Actually there is. if you want to pass model id instead of model itself to link-to helper, the model hook is always triggered.
darkbaby123

2
This should be documented somewhere on the Ember guides (best practices or something). It's an important use case that I'm sure many people encounter.
yorbro

1
If you are using controller.setProperties(model); do not forget to add these properties with default value to controller. Otherwise it will throw exception Cannot delegate set...
Mateusz Nowak

13

For a while I was using Em.RSVP.hash, however the problem I ran into was that I didn't want my view to wait until all models were loaded before rendering. However, I found a great (but relatively unknown) solution thanks to the folks at Novelys that involves making use of the Ember.PromiseProxyMixin:

Let's say you have a view that has three distinct visual sections. Each of these sections should be backed by its own model. The model backing the "splash" content at the top of the view is small and will load quickly, so you can load that one normally:

Create a route main-page.js:

import Ember from 'ember';

export default Ember.Route.extend({
    model: function() {
        return this.store.find('main-stuff');
    }
});

Then you can create a corresponding Handlebars template main-page.hbs:

<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
    <li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
</section>

So let's say in your template you want to have separate sections about your love/hate relationship with cheese, each (for some reason) backed by its own model. You have many records in each model with extensive details relating to each reason, however you'd like the content on top to render quickly. This is where the {{render}} helper comes in. You can update your template as so:

<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
    <li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
    {{render 'love-cheese'}}
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
    {{render 'hate-cheese'}}
</section>

Now you'll need to create controllers and templates for each. Since they're effectively identical for this example, I'll just use one.

Create a controller called love-cheese.js:

import Ember from 'ember';

export default Ember.ObjectController.extend(Ember.PromiseProxyMixin, {
    init: function() {
        this._super();
        var promise = this.store.find('love-cheese');
        if (promise) {
            return this.set('promise', promise);
        }
    }
});

You'll notice that we are using the PromiseProxyMixin here, which makes the controller promise-aware. When the controller is initialized, we indicate that the promise should be loading the love-cheese model via Ember Data. You'll need to set this property on the controller's promise property.

Now, create a template called love-cheese.hbs:

{{#if isPending}}
  <p>Loading...</p>
{{else}}
  {{#each item in promise._result }}
    <p>{{item.reason}}</p>
  {{/each}}
{{/if}}

In your template, you'll be able to render different content depending on the state of promise. When your page initially loads, your "Reasons I Love Cheese" section will display Loading.... When the promise is loaded, it will render all the reasons associated for each record of your model.

Each section will load independently and not block the main content from rendering immediately.

This is a simplistic example, but I hope everyone else finds it as useful as I did.

If you're looking to do something similar for many rows of content, you may find the Novelys example above even more relevant. If not, the above should work fine for you.


8

This might not be best practice and a naïve approach, but it shows conceptually how you would go about having on multiple models available on one central route:

App.PostRoute = Ember.Route.extend({
  model: function() {
    var multimodel = Ember.Object.create(
      {
        posts: App.Post.find(),
        comments: App.Comments.find(),
        whatever: App.WhatEver.find()
      });
    return multiModel;
  },
  setupController: function(controller, model) {
    // now you have here model.posts, model.comments, etc.
    // as promises, so you can do stuff like
    controller.set('contentA', model.posts);
    controller.set('contentB', model.comments);
    // or ...
    this.controllerFor('whatEver').set('content', model.whatever);
  }
});

hope it helps


1
This approach is fine, just not taking too much advantage of Ember Data. For some scenarios where the models are not related, I'd got for something similar to this.
MilkyWayJoe

4

Thanks to all the other excellent answers, I created a mixin that combines the best solutions here into a simple and reusable interface. It executes an Ember.RSVP.hash in afterModel for the models you specify, then injects the properties into the controller in setupController. It does not interfere with the standard model hook, so you would still define that as normal.

Example use:

App.PostRoute = Ember.Route.extend(App.AdditionalRouteModelsMixin, {

  // define your model hook normally
  model: function(params) {
    return this.store.find('post', params.post_id);
  },

  // now define your other models as a hash of property names to inject onto the controller
  additionalModels: function() {
    return {
      users: this.store.find('user'), 
      comments: this.store.find('comment')
    }
  }
});

Here is the mixin:

App.AdditionalRouteModelsMixin = Ember.Mixin.create({

  // the main hook: override to return a hash of models to set on the controller
  additionalModels: function(model, transition, queryParams) {},

  // returns a promise that will resolve once all additional models have resolved
  initializeAdditionalModels: function(model, transition, queryParams) {
    var models, promise;
    models = this.additionalModels(model, transition, queryParams);
    if (models) {
      promise = Ember.RSVP.hash(models);
      this.set('_additionalModelsPromise', promise);
      return promise;
    }
  },

  // copies the resolved properties onto the controller
  setupControllerAdditionalModels: function(controller) {
    var modelsPromise;
    modelsPromise = this.get('_additionalModelsPromise');
    if (modelsPromise) {
      modelsPromise.then(function(hash) {
        controller.setProperties(hash);
      });
    }
  },

  // hook to resolve the additional models -- blocks until resolved
  afterModel: function(model, transition, queryParams) {
    return this.initializeAdditionalModels(model, transition, queryParams);
  },

  // hook to copy the models onto the controller
  setupController: function(controller, model) {
    this._super(controller, model);
    this.setupControllerAdditionalModels(controller);
  }
});

2

https://stackoverflow.com/a/16466427/2637573 is fine for related models. However, with recent version of Ember CLI and Ember Data, there is a simpler approach for unrelated models:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller,model);
    var model2 = DS.PromiseArray.create({
      promise: this.store.find('model2')
    });
    model2.then(function() {
      controller.set('model2', model2)
    });
  }
});

If you only want to retrieve an object's property for model2, use DS.PromiseObject instead of DS.PromiseArray:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller,model);
    var model2 = DS.PromiseObject.create({
      promise: this.store.find('model2')
    });
    model2.then(function() {
      controller.set('model2', model2.get('value'))
    });
  }
});

I have a post edit route/view where I want to render all the existing tags in the DB so that the use can click to add them to the post being edited. I want to define a variable that represents an array/collection of these tags. Would the approach you used above work for this?
Joe

Sure, you would create a PromiseArray (e.g. "tags"). Then, in your template you would pass it to the selection element of the corresponding form.
AWM

1

Adding to MilkyWayJoe's answer, thanks btw:

this.store.find('post',1) 

returns

{
    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]
};

author would be

{
    id: 1,
    firstName: 'Joe',
    lastName: 'Way',
    email: 'MilkyWayJoe@example.com',
    points: 6181,
    post_ids: [1,2,3,...,n],
    comment_ids: [1,2,3,...,n],
}

comments

{
    id:1,
    author_id:1,
    body:'some words and stuff...',
    post_id:1,
}

... I believe the link backs are important so that the full relationship is established. Hope that helps someone.


0

You could use the beforeModel or afterModel hooks as these are always called, even if model is not called because you're using dynamic segments.

As per the asynchronous routing docs:

The model hook covers many use cases for pause-on-promise transitions, but sometimes you'll need the help of the related hooks beforeModel and afterModel. The most common reason for this is that if you're transitioning into a route with a dynamic URL segment via {{link-to}} or transitionTo (as opposed to a transition caused by a URL change), the model for the route you're transitioning into will have already been specified (e.g. {{#link-to 'article' article}} or this.transitionTo('article', article)), in which case the model hook won't get called. In these cases, you'll need to make use of either the beforeModel or afterModel hook to house any logic while the router is still gathering all of the route's models to perform a transition.

So say you have a themes property on your SiteController, you could have something like this:

themes: null,
afterModel: function(site, transition) {
  return this.store.find('themes').then(function(result) {
    this.set('themes', result.content);
  }.bind(this));
},
setupController: function(controller, model) {
  controller.set('model', model);
  controller.set('themes', this.get('themes'));
}

1
I think using this inside the promise would give an error message. You could set var _this = this before the return and then do _this.set( inside the then( method to get the desired outcome
Duncan Walker
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.