How to use multiple models with a single route in EmberJS / Ember Data?

前端 未结 8 1979
一整个雨季
一整个雨季 2020-11-27 09:16

From reading the docs, it looks like you have to (or should) assign a model to a route like so:

App.PostRoute = Ember.Route.extend({
    model: function() {
         


        
相关标签:
8条回答
  • 2020-11-27 09:43

    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.

    0 讨论(0)
  • 2020-11-27 09:43

    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

    0 讨论(0)
  • 2020-11-27 09:43

    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'));
    }
    
    0 讨论(0)
  • 2020-11-27 09:50

    Last update forever: I can't keep updating this. So this is deprecated and will likely be this way. here's a better, and more up-to-date thread EmberJS: How to load multiple models on the same route?

    Update: In my original answer I said to use embedded: true in the model definition. That's incorrect. In revision 12, Ember-Data expects foreign keys to be defined with a suffix (link) _id for single record or _ids for collection. Something similar to the following:

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

    I have updated the fiddle and will do so again if anything changes or if I find more issues with the code provided in this answer.


    Answer with related models:

    For the scenario you are describing, I would rely on associations between models (setting embedded: true) and only load the Post model in that route, considering I can define a DS.hasMany association for the Comment model and DS.belongsTo association for the User in both the Comment and Post models. Something like this:

    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)

    0 讨论(0)
  • 2020-11-27 09:53

    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);
      }
    });
    
    0 讨论(0)
  • 2020-11-27 09:54

    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 讨论(0)
提交回复
热议问题