Comment utiliser plusieurs modèles avec une seule route dans EmberJS / Ember Data?

85

À la lecture de la documentation, il semble que vous deviez (ou devriez) affecter un modèle à une route comme ceci:

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

Que faire si j'ai besoin d'utiliser plusieurs objets dans un certain itinéraire? c'est-à-dire les messages, les commentaires et les utilisateurs? Comment indiquer l'itinéraire pour les charger?

Anonyme
la source

Réponses:

117

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

Mise à jour: Dans ma réponse initiale, j'ai dit d'utiliser embedded: truedans la définition du modèle. C'est incorrect. Dans la révision 12, Ember-Data s'attend à ce que les clés étrangères soient définies avec un suffixe ( lien ) _idpour un seul enregistrement ou _idspour la collecte. Quelque chose de similaire à ce qui suit:

{
    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 je 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 associés:

Pour le scénario que vous décrivez, je compter sur les associations entre les modèles (réglage embedded: true) et seulement charger le Postmodèle dans cette voie, étant donné que je peux définir une DS.hasManyassociation pour le Commentmodèle et l' DS.belongsToassociation pour le Userdans les deux Commentet Postmodèles. 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 ce qui suit:

Associations entre modèles

Avec cette définition, chaque fois que findje publierai un message, j'aurai accès à une collection de commentaires associés à ce message, ainsi qu'à l'auteur du commentaire et à l'utilisateur qui est l'auteur du message, 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);
    }
});

Donc, dans le PostRoute(ou PostsPostRoutesi vous utilisez resource), mes modèles auront accès au contrôleur content, qui est le Postmodèle, afin que je puisse me référer à l'auteur, simplement commeauthor

<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 si vous devez utiliser (ou interroger) différents modèles pour un itinéraire particulier, je vous recommande 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 userpropriété dans le contrôleur tel qu'il a été configuré dans setupControllerhook:

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

MilkyWayJoe
la source
15
Merci beaucoup d' avoir pris le temps de poster ça, je l'ai trouvé vraiment utile.
Anonyme
1
@MilkyWayJoe, très bon post! Maintenant, mon approche semble vraiment naïve :)
intuitifpixel
Le problème avec vos modèles non liés est qu'ils n'acceptent pas les promesses comme le fait le crochet de modèle, non? Y a-t-il une solution de contournement pour cela?
miguelcobain
1
Si je comprends bien votre problème, vous pouvez l'adapter facilement. Attendez simplement que la promesse se réalise et définissez ensuite le (s) modèle (s) comme variable du contrôleur
Fabio
En plus d'itérer et d'afficher des commentaires, ce serait bien si vous pouviez montrer un exemple de la façon dont quelqu'un pourrait ajouter un nouveau commentaire dans post.comments.
Damon Aw
49

Utiliser Em.Objectpour encapsuler plusieurs modèles est un bon moyen d'obtenir toutes les données en modelhook. 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 retourne une nouvelle promesse. La nouvelle promesse est résolue une fois que toutes les promesses sont résolues. Et setupControllern'est pas appelée 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 le rendu de la vue. Et l'utilisation Em.Objectn'a pas cet avantage.

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

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

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


Mais ne pas utiliser Em.Objectou Em.RSVPsolution si votre itinéraire comporte des segments dynamiques

Le problème principal est link-to. Si vous modifiez l'URL en cliquant sur le lien généré par link-towith models, le modèle est transmis directement à cette route. Dans ce cas, le modelcrochet n'est pas appelé et setupControllervous obtenez le modèle qui link-tovous est donné.

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 le link-tocode, le post est un modèle:

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

Les choses deviennent intéressantes ici. Lorsque vous utilisez url /post/1pour visiter la page, le modelhook est appelé et setupControllerobtient l'objet brut lorsque la promesse est résolue.

Mais si vous visitez la page en cliquant sur le link-tolien, il passe le postmodèle à PostRouteet l'itinéraire ignorera le modelhook. Dans ce cas setupController, vous obtiendrez le postmodèle, 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.

darkbaby123
la source
2
Ma réponse s'applique à une version antérieure d'Ember & Ember-Data. C'est une très bonne approche +1
MilkyWayJoe
11
En fait, il y en a. si vous souhaitez transmettre l'ID du modèle au lieu du modèle lui-même à l'assistant de liaison, le hook de modèle est toujours déclenché.
darkbaby123
2
Cela devrait être documenté quelque part dans les guides Ember (meilleures pratiques ou autre). C'est un cas d'utilisation important que je suis sûr que beaucoup de gens rencontrent.
yorbro
1
Si vous utilisez, controller.setProperties(model);n'oubliez pas d'ajouter ces propriétés avec la valeur par défaut au contrôleur. Sinon, il lancera une exceptionCannot delegate set...
Mateusz Nowak
13

Pendant un certain temps, j'utilisais Em.RSVP.hash, mais le problème que j'ai rencontré était 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 consiste à utiliser Ember.PromiseProxyMixin :

Supposons que vous ayez une vue comportant trois sections visuelles distinctes. Chacune de ces sections doit être soutenue par son propre modèle. Le modèle sauvegardant le contenu "splash" en haut de la vue est petit et se chargera rapidement, vous pouvez donc charger celui-là normalement:

Créez 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 que dans votre modèle, vous souhaitez avoir des sections séparées sur votre relation d'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 soit rendu rapidement. C'est là que l' {{render}}assistant 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 devrez 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 PromiseProxyMixinici, ce qui rend le contrôleur conscient des promesses. Lorsque le contrôleur est initialisé, nous indiquons que la promesse devrait charger le love-cheesemodèle via Ember Data. Vous devrez définir cette propriété sur la propriété du contrôleur promise.

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 rendre un contenu différent en fonction de l'état de la promesse. Lors du chargement initial de votre page, votre section «Raisons pour lesquelles j'aime le fromage» s'affichera 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 fonctionner correctement pour vous.

SwarmIntelligence
la source
8

Ce n'est peut-être pas la meilleure pratique et une approche naïve, mais cela montre conceptuellement comment vous feriez pour avoir plusieurs modèles disponibles sur un itinéraire central:

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

pixel intuitif
la source
1
Cette approche est bonne, mais ne profite pas trop d'Ember Data. Pour certains scénarios où les modèles ne sont pas liés, j'aurais obtenu quelque chose de similaire.
MilkyWayJoe
4

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 une Ember.RSVP.hashdans afterModelles modèles que vous spécifiez, injectent ensuite les propriétés dans le contrôleur setupController. Cela n'interfère pas avec le modelhook standard , donc vous définiriez toujours cela 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);
  }
});
Christophe Sansone
la source
2

https://stackoverflow.com/a/16466427/2637573 convient aux modèles associés. Cependant, avec la version récente d'Ember CLI et d'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'))
    });
  }
});
AWM
la source
J'ai une postroute / vue d'édition où je veux rendre toutes les balises existantes dans la base de données afin que l'utilisation puisse cliquer pour les ajouter au message en cours d'édition. Je souhaite définir une variable qui représente un tableau / une collection de ces balises. L'approche que vous avez utilisée ci-dessus fonctionnerait-elle pour cela?
Joe le
Bien sûr, vous créeriez un PromiseArray(par exemple, "tags"). Ensuite, dans votre modèle, vous le passerez à l'élément de sélection du formulaire correspondant.
AWM
1

Ajout à la réponse de MilkyWayJoe, merci btw:

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

Retour

{
    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 sont importants pour que la relation complète soit établie. J'espère que cela aide quelqu'un.

philn5d
la source
0

Vous pouvez utiliser les hooks beforeModelou afterModelcar ils sont toujours appelés, même s'ils modelne sont pas appelés car vous utilisez des segments dynamiques.

Selon la documentation de routage asynchrone :

Le hook de modèle couvre de nombreux cas d'utilisation des transitions de pause sur promesse, mais vous aurez parfois besoin de l'aide des hooks associés beforeModel et afterModel. La raison la plus courante en est que si vous effectuez une transition vers une route 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 're transitioning into aura déjà été spécifié (par exemple {{# link-to' article 'article}} ou this.transitionTo (' article ', article)), auquel cas le hook de 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 route pour effectuer une transition.

Alors disons que vous avez une themespropriété 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'));
}
Ryan D
la source
1
Je pense que l'utilisation thisà l'intérieur de la promesse donnerait un message d'erreur. Vous pouvez définir var _this = thisavant le retour, puis faire _this.set(à l'intérieur de la then(méthode pour obtenir le résultat souhaité
Duncan Walker