web-dev-qa-db-fra.com

Comment utiliser plusieurs modèles avec un seul itinéraire dans EmberJS / Ember Data?

En lisant les documents, il semble que vous devez (ou devriez) affecter un modèle à un itinéraire comme ceci:

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

Que faire si j'ai besoin d'utiliser plusieurs objets sur un certain itinéraire? à savoir les publications, les commentaires et les utilisateurs? Comment dire à l'itinéraire de les charger?

85
Anonymous

Dernière mise à jour pour toujours : je ne peux pas continuer à mettre à jour ceci. C'est donc obsolète et il en sera probablement ainsi. voici un thread meilleur et plus à jour EmberJS: Comment charger plusieurs modèles sur le même itinéraire?

Mise à jour: Dans ma réponse d'origine, j'ai dit d'utiliser embedded: true dans la définition du modèle. C'est faux. Dans la révision 12, Ember-Data s'attend à ce que les clés étrangères soient définies avec un suffixe ( link ) _id pour un seul enregistrement ou _ids pour la collecte. Quelque chose de semblable au suivant:

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

J'ai mis à jour le violon et le ferai à nouveau si quelque chose change ou si je trouve plus de problèmes avec le code fourni dans cette réponse.


Réponse avec des modèles connexes:

Pour le scénario que vous décrivez, je compterais sur associations entre les modèles (réglage embedded: true) et charger uniquement le modèle Post dans cette route, étant donné que je peux définir un DS.hasMany association pour le modèle Comment et DS.belongsTo association pour User dans les modèles Comment et Post. Quelque chose comme ça:

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')
});

Cette définition produirait quelque chose comme ceci:

Associations between models

Avec cette définition, chaque fois que je find un article, j'aurai accès à une collection de commentaires associés à cet article, ainsi qu'à l'auteur du commentaire et à l'utilisateur qui est l'auteur de l'article, car ils sont tous intégrés. L'itinéraire reste simple:

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

Ainsi, dans le PostRoute (ou PostsPostRoute si vous utilisez resource), mes modèles auront accès au content du contrôleur, qui est le Post model, donc je peux me référer à l'auteur, simplement comme 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>

(voir violon )


Réponse avec des modèles non liés:

Cependant, si votre scénario est un peu plus complexe que ce que vous avez décrit et/ou ont pour utiliser (ou interroger) différents modèles pour un itinéraire particulier, je recommanderais de le faire dans - Route#setupController . Par exemple:

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));
    }
});

Et maintenant, lorsque je suis dans la route Post, mes modèles auront accès à la propriété user dans le contrôleur telle qu'elle a été configurée dans le crochet setupController:

<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>

(voir violon )

116
MilkyWayJoe

En utilisant Em.Object pour encapsuler plusieurs modèles est un bon moyen d'obtenir toutes les données dans le crochet model. Mais il ne peut pas garantir que toutes les données sont préparées après le rendu de la vue.

Un autre choix consiste à utiliser Em.RSVP.hash. Il combine plusieurs promesses ensemble et renvoie une nouvelle promesse. La nouvelle promesse est résolue une fois toutes les promesses résolues. Et setupController n'est pas appelé tant que la promesse n'est pas résolue ou rejetée.

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);
  }
});

De cette façon, vous obtenez tous les modèles avant de visualiser le rendu. Et en utilisant Em.Object n'a pas cet avantage.

Un autre avantage est que vous pouvez combiner la promesse et la non-promesse. Comme ça:

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

Cochez cette option pour en savoir plus sur Em.RSVP: https://github.com/tildeio/rsvp.js


Mais n'utilisez pas Em.Object ou Em.RSVP solution si votre itinéraire comporte des segments dynamiques

Le principal problème est link-to. Si vous modifiez l'URL en cliquant sur le lien généré par link-to avec les modèles, le modèle est transmis directement à cette route. Dans ce cas, le hook model n'est pas appelé et dans setupController vous obtenez le modèle link-to te donner.

Un exemple est comme ceci:

Le code d'itinéraire:

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);
  }
});

Et link-to code, la publication est un modèle:

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

Les choses deviennent intéressantes ici. Lorsque vous utilisez url /post/1 pour visiter la page, le hook model est appelé et setupController obtient l'objet simple lorsque la promesse est résolue.

Mais si vous visitez la page en cliquant sur link-to link, il passe post model à PostRoute et la route ignorera model hook. Dans ce cas, setupController obtiendra le modèle post, bien sûr, vous ne pouvez pas obtenir d'utilisateur.

Assurez-vous donc de ne pas les utiliser dans les itinéraires avec des segments dynamiques.

49
darkbaby123

Pendant un moment, j'utilisais Em.RSVP.hash, mais le problème que j'ai rencontré est que je ne voulais pas que ma vue attende que tous les modèles soient chargés avant le rendu. Cependant, j'ai trouvé une excellente solution (mais relativement inconnue) grâce aux gens de Novelys qui implique l'utilisation de Ember.PromiseProxyMixin :

Supposons que vous ayez une vue qui comporte trois sections visuelles distinctes. Chacune de ces sections doit être appuyée par son propre modèle. Le modèle qui sauvegarde le contenu "splash" en haut de la vue est petit et se chargera rapidement, vous pouvez donc le charger normalement:

Créer un itinéraire main-page.js:

import Ember from 'ember';

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

Ensuite, vous pouvez créer un modèle de guidon correspondant 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>

Disons donc que dans votre modèle, vous souhaitez avoir des sections distinctes sur votre relation amour/haine avec le fromage, chacune (pour une raison quelconque) soutenue par son propre modèle. Vous avez de nombreux enregistrements dans chaque modèle avec des détails détaillés relatifs à chaque raison, mais vous souhaitez que le contenu en haut s'affiche rapidement. C'est là que le {{render}} helper entre en jeu. Vous pouvez mettre à jour votre modèle comme suit:

<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>

Vous devez maintenant créer des contrôleurs et des modèles pour chacun. Puisqu'ils sont effectivement identiques pour cet exemple, je n'en utiliserai qu'un.

Créez un contrôleur appelé 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);
        }
    }
});

Vous remarquerez que nous utilisons ici le PromiseProxyMixin, ce qui rend le contrôleur conscient des promesses. Lorsque le contrôleur est initialisé, nous indiquons que la promesse devrait charger le love-cheese model via Ember Data. Vous devrez définir cette propriété sur la propriété promise du contrôleur.

Maintenant, créez un modèle appelé love-cheese.hbs:

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

Dans votre modèle, vous pourrez afficher un contenu différent en fonction de l'état de la promesse. Lorsque votre page se charge initialement, votre section "Raisons pour lesquelles j'aime le fromage" affiche Loading.... Lorsque la promesse est chargée, elle rendra toutes les raisons associées à chaque enregistrement de votre modèle.

Chaque section se chargera indépendamment et n'empêchera pas le contenu principal de s'afficher immédiatement.

C'est un exemple simpliste, mais j'espère que tout le monde le trouvera aussi utile que moi.

Si vous cherchez à faire quelque chose de similaire pour de nombreuses lignes de contenu, vous pouvez trouver l'exemple Novelys ci-dessus encore plus pertinent. Sinon, ce qui précède devrait bien fonctionner pour vous.

13
SwarmIntelligence

Ce n'est peut-être pas la meilleure pratique et une approche naïve, mais cela montre conceptuellement comment vous pourriez avoir plusieurs modèles disponibles sur une route centrale:

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);
  }
});

j'espère que ça aide

8
intuitivepixel

Grâce à toutes les autres excellentes réponses, j'ai créé un mixin qui combine les meilleures solutions ici dans une interface simple et réutilisable. Il exécute un Ember.RSVP.hash in afterModel pour les modèles que vous spécifiez, puis injecte les propriétés dans le contrôleur dans setupController. Cela n'interfère pas avec le hook model standard, vous devez donc toujours le définir comme normal.

Exemple d'utilisation:

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')
    }
  }
});

Voici le 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);
  }
});
4

https://stackoverflow.com/a/16466427/263757 est bien pour les modèles connexes. Cependant, avec la version récente de Ember CLI et Ember Data), il existe une approche plus simple pour les modèles indépendants:

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)
    });
  }
});

Si vous souhaitez uniquement récupérer la propriété d'un objet pour model2, utilisez DS.PromiseObject au lieu de 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'))
    });
  }
});
2
AWM

Pour compléter la réponse de MilkyWayJoe, merci btw:

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

résultats

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

l'auteur serait

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

commentaires

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

... Je crois que les liens de retour sont importants pour que la relation complète soit établie. J'espère que cela aide quelqu'un.

1
philn5d

Vous pouvez utiliser les crochets beforeModel ou afterModel car ils sont toujours appelés, même si model n'est pas appelé car vous utilisez des segments dynamiques.

Selon les documents routage asynchrone :

Le modèle hook couvre de nombreux cas d'utilisation pour les transitions de pause sur promesse, mais parfois vous aurez besoin de l'aide des hooks associés avantModèle et aprèsModèle. La raison la plus courante en est que si vous effectuez une transition vers un itinéraire avec un segment d'URL dynamique via {{link-to}} ou transitionTo (par opposition à une transition provoquée par un changement d'URL), le modèle de l'itinéraire que vous La transition vers sera déjà spécifiée (par exemple {{# link-to 'article' article}} ou this.transitionTo ('article', article)), auquel cas le hook du modèle ne sera pas appelé. Dans ces cas, vous devrez utiliser le hook beforeModel ou afterModel pour héberger toute logique pendant que le routeur rassemble toujours tous les modèles de l'itinéraire pour effectuer une transition.

Supposons donc que vous ayez une propriété themes sur votre SiteController, vous pourriez avoir quelque chose comme ceci:

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
Ryan D