¿Cómo usar varios modelos con una sola ruta en EmberJS / Ember Data?


85

Al leer los documentos, parece que tiene que (o debería) asignar un modelo a una ruta como esta:

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

¿Qué pasa si necesito utilizar varios objetos en una ruta determinada? es decir, publicaciones, comentarios y usuarios? ¿Cómo le digo a la ruta que los cargue?

Respuestas:


117

Última actualización para siempre : no puedo seguir actualizando esto. Así que esto está desaprobado y probablemente será así. aquí hay un hilo mejor y más actualizado EmberJS: ¿Cómo cargar varios modelos en la misma ruta?

Actualización: en mi respuesta original dije que se usara embedded: trueen la definición del modelo. Eso es incorrecto. En la revisión 12, Ember-Data espera que las claves externas se definan con un sufijo ( enlace ) _idpara un solo registro o _idspara una colección. Algo similar a lo siguiente:

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

He actualizado el violín y lo volveré a hacer si algo cambia o si encuentro más problemas con el código proporcionado en esta respuesta.


Responda con modelos relacionados:

Para el escenario que está describiendo, me gustaría confiar en las asociaciones entre los modelos (de ajuste embedded: true) y sólo cargar el Postmodelo en esa ruta, teniendo en cuenta que puede definir una DS.hasManyasociación para el Commentmodelo y DS.belongsTola asociación para el Usertanto en el Commenty Postmodelos. Algo como esto:

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

Esta definición produciría algo como lo siguiente:

Asociaciones entre modelos

Con esta definición, cada vez que findhaga una publicación, tendré acceso a una colección de comentarios asociados con esa publicación, y también al autor del comentario, y al usuario que es el autor de la publicación, ya que todos están incrustados . La ruta sigue siendo simple:

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

Entonces, en PostRoute(o PostsPostRoutesi está usando resource), mis plantillas tendrán acceso al controlador content, que es el Postmodelo, por lo que puedo referirme al autor, simplemente comoauthor

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

(ver violín )


Responda con modelos no relacionados:

Sin embargo, si su escenario es un poco más complejo de lo que describió y / o tiene que usar (o consultar) diferentes modelos para una ruta en particular, le recomendaría hacerlo en Route#setupController. Por ejemplo:

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

Y ahora, cuando estoy en la ruta de publicación, mis plantillas tendrán acceso a la userpropiedad en el controlador tal como se configuró en el setupControllergancho:

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

(ver violín )


15
Gracias tanto por tomar el tiempo para publicar eso, me pareció muy útil.
Anónimo

1
@MilkyWayJoe, ¡muy buena publicación! Ahora mi enfoque parece realmente ingenuo :)
intuitivopixel

El problema con sus modelos no relacionados es que no acepta promesas como lo hace el modelo hook, ¿verdad? ¿Hay alguna solución para eso?
miguelcobain

1
Si entiendo tu problema correctamente, puedes adaptarlo de forma sencilla. Solo espere a que se cumpla la promesa y solo entonces configure el modelo (s) como una variable del controlador
Fabio

Además de iterar y mostrar comentarios, sería maravilloso si pudiera mostrar un ejemplo de cómo alguien podría agregar un nuevo comentario post.comments.
Damon Aw

49

Usar Em.Objectpara encapsular múltiples modelos es una buena manera de modelconectar todos los datos . Pero no puede garantizar que todos los datos estén preparados después de la visualización de la vista.

Otra opción es utilizar Em.RSVP.hash. Combina varias promesas juntas y devuelve una nueva promesa. La nueva promesa si se resuelve después de que se resuelvan todas las promesas. Y setupControllerno se llama hasta que la promesa se resuelve o se rechaza.

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 esta manera, obtiene todos los modelos antes de ver el renderizado. Y usar Em.Objectno tiene esta ventaja.

Otra ventaja es que puede combinar promesas y no promesas. Me gusta esto:

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

Consulte esto para obtener más información sobre Em.RSVP: https://github.com/tildeio/rsvp.js


Pero no use Em.Objecto Em.RSVPsolución si la ruta tiene segmentos dinámicos

El principal problema es link-to. Si cambia la URL haciendo clic en el enlace generado por link-tocon modelos, el modelo se pasa directamente a esa ruta. En este caso, el modelgancho no se llama y setupControllerobtienes el modelo.link-to .

Un ejemplo es como este:

El código de ruta:

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

Y el link-tocódigo, la publicación es un modelo:

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

Las cosas se ponen interesantes aquí. Cuando usa url /post/1para visitar la página, modelse llama al gancho y setupControllerobtiene el objeto simple cuando se resuelve la promesa.

Pero si visita la página haciendo clic en el link-toenlace, pasa el postmodelo a PostRoutey la ruta ignorará el modelgancho. En este caso setupControllerobtendrá el postmodelo, por supuesto que no puede obtener usuario.

Así que asegúrese de no usarlos en rutas con segmentos dinámicos.


2
Mi respuesta se aplica a una versión anterior de Ember & Ember-Data. Este es un enfoque realmente bueno +1
MilkyWayJoe

11
De hecho la hay. si desea pasar la identificación del modelo en lugar del modelo en sí al asistente de enlace, el enlace del modelo siempre se activa.
darkbaby123

2
Esto debe estar documentado en alguna parte de las guías de Ember (mejores prácticas o algo así). Es un caso de uso importante que estoy seguro de que mucha gente encuentra.
yorbro

1
Si está utilizando controller.setProperties(model);, no olvide agregar estas propiedades con el valor predeterminado al controlador. De lo contrario, arrojará una excepciónCannot delegate set...
Mateusz Nowak

13

Por un tiempo estuve usando Em.RSVP.hash , sin embargo, el problema que encontré fue que no quería que mi vista esperara hasta que todos los modelos estuvieran cargados antes de renderizar. Sin embargo, encontré una gran solución (pero relativamente desconocida) gracias a la gente de Novelys que implica hacer uso de Ember.PromiseProxyMixin :

Digamos que tiene una vista que tiene tres secciones visuales distintas. Cada una de estas secciones debe estar respaldada por su propio modelo. El modelo que respalda el contenido "splash" en la parte superior de la vista es pequeño y se cargará rápidamente, por lo que puede cargarlo normalmente:

Crea una ruta main-page.js :

import Ember from 'ember';

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

Luego puede crear una plantilla de manillares correspondiente 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>

Digamos que en su plantilla desea tener secciones separadas sobre su relación de amor / odio con el queso, cada una (por alguna razón) respaldada por su propio modelo. Tiene muchos registros en cada modelo con detalles extensos relacionados con cada motivo, sin embargo, le gustaría que el contenido en la parte superior se procesara rápidamente. Aquí es donde el{{render}} entra ayudante. Puede actualizar su plantilla de la siguiente manera:

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

Ahora necesitará crear controladores y plantillas para cada uno. Dado que son efectivamente idénticos para este ejemplo, solo usaré uno.

Crea un controlador llamado 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);
        }
    }
});

Notarás que estamos usando PromiseProxyMixinaquí, lo que hace que el controlador sea compatible con las promesas. Cuando se inicializa el controlador, indicamos que la promesa debería ser cargar el love-cheesemodelo a través de Ember Data. Deberá establecer esta propiedad en la propiedad del controlador promise.

Ahora, cree una plantilla llamada love-cheese.hbs:

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

En su plantilla, podrá renderizar contenido diferente según el estado de la promesa. Cuando su página se cargue inicialmente, aparecerá la sección "Razones por las que amo el queso"Loading... . Cuando se carga la promesa, presentará todas las razones asociadas para cada registro de su modelo.

Cada sección se cargará de forma independiente y no bloqueará el contenido principal para que no se procese inmediatamente.

Este es un ejemplo simplista, pero espero que todos los demás lo encuentren tan útil como yo.

Si está buscando hacer algo similar para muchas filas de contenido, puede encontrar el ejemplo de Novelys anterior aún más relevante. Si no, lo anterior debería funcionar bien para usted.


8

Puede que esta no sea la mejor práctica y un enfoque ingenuo, pero muestra conceptualmente cómo haría para tener varios modelos disponibles en una ruta 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);
  }
});

Espero eso ayude


1
Este enfoque está bien, pero no aprovecha demasiado Ember Data. Para algunos escenarios donde los modelos no están relacionados, obtuve algo similar a esto.
MilkyWayJoe

4

Gracias a todas las otras excelentes respuestas, creé un mixin que combina las mejores soluciones aquí en una interfaz simple y reutilizable. Se ejecuta una Ember.RSVP.hashde afterModellas modelos que especifique, a continuación, las propiedades inyecta en el controlador de setupController. No interfiere con el modelgancho estándar , por lo que aún lo definiría como normal.

Ejemplo de uso:

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

Aquí está el 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 está bien para modelos relacionados. Sin embargo, con la versión reciente de Ember CLI y Ember Data, existe un enfoque más simple para modelos no relacionados:

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 solo desea recuperar la propiedad de un objeto para model2, use DS.PromiseObject en lugar 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'))
    });
  }
});

Tengo una postruta / vista de edición donde quiero representar todas las etiquetas existentes en la base de datos para que el uso pueda hacer clic para agregarlas a la publicación que se está editando. Quiero definir una variable que represente una matriz / colección de estas etiquetas. ¿El enfoque que utilizó anteriormente funcionaría para esto?
Joe

Seguro, crearía un PromiseArray(por ejemplo, "etiquetas"). Luego, en tu plantilla lo pasarías al elemento de selección del formulario correspondiente.
AWM

1

Agregando a la respuesta de MilkyWayJoe, gracias por cierto:

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

devoluciones

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

el autor sería

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

comentarios

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

... Creo que los vínculos de retroceso son importantes para que se establezca la relación completa. Espero que ayude a alguien.


0

Puede usar los ganchos beforeModelo afterModelya que siempre se llaman, incluso si modelno se llaman porque está usando segmentos dinámicos.

Según los documentos de enrutamiento asincrónico :

El gancho de modelo cubre muchos casos de uso para las transiciones de pausa en promesa, pero a veces necesitará la ayuda de los ganchos relacionados beforeModel y afterModel. La razón más común de esto es que si está haciendo la transición a una ruta con un segmento de URL dinámico a través de {{link-to}} o transitTo (a diferencia de una transición causada por un cambio de URL), el modelo de la ruta que ya se habrá especificado la transición a will (por ejemplo, {{# link-to 'article' article}} o this.transitionTo ('artículo', artículo)), en cuyo caso no se llamará al gancho modelo. En estos casos, deberá utilizar el gancho beforeModel o afterModel para albergar cualquier lógica mientras el enrutador aún está recopilando todos los modelos de la ruta para realizar una transición.

Entonces, digamos que tiene una themespropiedad en su SiteController, podría tener algo como esto:

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
Creo que usar thisdentro de la promesa daría un mensaje de error. Puede establecer var _this = thisantes del regreso y luego hacerlo _this.set(dentro del then(método para obtener el resultado deseado
Duncan Walker
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.