Como usar vários modelos com uma única rota no EmberJS / Ember Data?

85

Pela leitura dos documentos, parece que você tem que (ou deveria) atribuir um modelo a uma rota assim:

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

E se eu precisar usar vários objetos em uma determinada rota? ou seja, postagens, comentários e usuários? Como faço para informar a rota para carregá-los?

Anônimo
fonte

Respostas:

117

Última atualização para sempre : não posso continuar atualizando isso. Portanto, ele está obsoleto e provavelmente será assim. aqui está um tópico melhor e mais atualizado EmberJS: Como carregar vários modelos na mesma rota?

Update: Na minha resposta original eu disse para usar embedded: truena definição do modelo. Isso está incorreto. Na revisão 12, Ember-Data espera que as chaves estrangeiras sejam definidas com um sufixo ( link ) _idpara registro único ou _idspara coleta. Algo semelhante ao seguinte:

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

Eu atualizei o violino e o farei novamente se algo mudar ou se eu encontrar mais problemas com o código fornecido nesta resposta.


Responda com modelos relacionados:

Para o cenário que você está descrevendo, eu confiaria em associações entre modelos (configuração embedded: true) e apenas carregaria o Postmodelo nessa rota, considerando que posso definir uma DS.hasManyassociação para o Commentmodelo e uma DS.belongsToassociação para Usernos modelos Commente Post. Algo assim:

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

Essa definição produziria algo como o seguinte:

Associações entre modelos

Com essa definição, sempre que eu findpostar, terei acesso a uma coleção de comentários associados a essa postagem, bem como ao autor do comentário e ao usuário que é o autor da postagem, uma vez que estão todos incorporados . A rota permanece simples:

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

Portanto, no PostRoute(ou PostsPostRoutese você estiver usando resource), meus modelos terão acesso ao controlador content, que é o Postmodelo, então posso me referir ao autor, simplesmente 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 violino )


Responder com modelos não relacionados:

No entanto, se o seu cenário for um pouco mais complexo do que o que você descreveu e / ou tiver que usar (ou consultar) modelos diferentes para uma rota específica, recomendo fazê-lo em Route#setupController. Por exemplo:

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

E agora, quando estou na rota Post, meus modelos terão acesso à userpropriedade no controlador conforme foi configurado no 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 violino )

MilkyWayJoe
fonte
15
Muito obrigado por dedicar seu tempo para postar isso, eu achei muito útil.
Anônimo
1
@MilkyWayJoe, post muito bom! Agora minha abordagem parece realmente ingênua :)
intuitivepixel
O problema com seus modelos não relacionados é que eles não aceitam promessas como o gancho do modelo, certo? Existe alguma solução alternativa para isso?
miguelcobain
1
Se eu entendi seu problema corretamente, você pode adaptá-lo de uma forma fácil. É só aguardar o cumprimento da promessa e só então definir o (s) modelo (s) como variável do controlador
Fábio
Além de iterar e exibir comentários, seria ótimo se você pudesse mostrar um exemplo de como alguém pode adicionar um novo comentário no post.comments.
Damon Aw
49

Usar Em.Objectpara encapsular vários modelos é uma boa maneira de colocar todos os dados no modelgancho. Mas não pode garantir que todos os dados sejam preparados após a renderização da visualização.

Outra opção é usar Em.RSVP.hash. Ele combina várias promessas e retorna uma nova promessa. A nova promessa é resolvida depois que todas as promessas são resolvidas. E setupControllernão é chamado até que a promessa seja resolvida ou rejeitada.

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

Desta forma, você obtém todos os modelos antes da renderização da vista. E usar Em.Objectnão tem essa vantagem.

Outra vantagem é que você pode combinar promessa e não promessa. Como isso:

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

Verifique aqui para saber mais sobre Em.RSVP: https://github.com/tildeio/rsvp.js


Mas não use Em.Objectou Em.RSVPsolução se sua rota tem segmentos dinâmicos

O principal problema é link-to. Se você alterar o url clicando no link gerado por link-tocom modelos, o modelo é passado diretamente para aquela rota. Neste caso, o modelgancho não é chamado e setupControllervocê pega o modelo link-toque lhe dá.

Um exemplo é assim:

O código da rota:

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

E o link-tocódigo, o post é um modelo:

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

As coisas ficam interessantes aqui. Quando você usa url /post/1para visitar a página, o modelgancho é chamado e setupControllerobtém o objeto simples quando a promessa é resolvida.

Mas se você visitar a página clicando no link-tolink, ele passa o postmodelo para PostRoutee a rota irá ignorar o modelgancho. Neste caso setupController, obterá o postmodelo, é claro que você não pode obter o usuário.

Portanto, certifique-se de não usá-los em rotas com segmentos dinâmicos.

darkbaby123
fonte
2
Minha resposta se aplica a uma versão anterior do Ember & Ember-Data. Esta é uma abordagem muito boa +1
MilkyWayJoe
11
Na verdade, existe. se você deseja passar o id do modelo em vez do próprio modelo para o auxiliar de link, o gancho do modelo é sempre acionado.
darkbaby123
2
Isso deve ser documentado em algum lugar nos guias do Ember (melhores práticas ou algo assim). É um caso de uso importante que tenho certeza que muitas pessoas encontram.
yorbro
1
Se estiver usando controller.setProperties(model);não se esqueça de adicionar essas propriedades com valor padrão ao controlador. Caso contrário, lançará exceçãoCannot delegate set...
Mateusz Nowak
13

Por um tempo eu estava usando Em.RSVP.hash, mas o problema que encontrei foi que eu não queria que minha visualização esperasse até que todos os modelos estivessem carregados antes de renderizar. No entanto, encontrei uma ótima solução (mas relativamente desconhecida) graças ao pessoal da Novelys que envolve o uso do Ember.PromiseProxyMixin :

Digamos que você tenha uma visualização com três seções visuais distintas. Cada uma dessas seções deve ser apoiada por seu próprio modelo. O modelo que apoia o conteúdo "inicial" na parte superior da visualização é pequeno e carregará rapidamente, então você pode carregá-lo normalmente:

Crie uma rota main-page.js:

import Ember from 'ember';

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

Em seguida, você pode criar um modelo de guiador correspondente 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>

Então, digamos que em seu modelo você queira ter seções separadas sobre sua relação de amor / ódio com o queijo, cada uma (por algum motivo) apoiada por seu próprio modelo. Você tem muitos registros em cada modelo com muitos detalhes relacionados a cada motivo, mas gostaria que o conteúdo acima fosse renderizado rapidamente. É aqui que {{render}}entra o auxiliar. Você pode atualizar seu modelo da seguinte maneira:

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

Agora você precisará criar controladores e modelos para cada um. Já que eles são efetivamente idênticos para este exemplo, usarei apenas um.

Crie um controlador chamado 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);
        }
    }
});

Você notará que estamos usando o PromiseProxyMixinhere, o que torna o controlador ciente da promessa. Quando o controlador é inicializado, indicamos que a promessa deve ser carregar o love-cheesemodelo via Ember Data. Você precisará definir esta propriedade na propriedade do controlador promise.

Agora, crie um modelo chamado love-cheese.hbs:

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

Em seu modelo, você será capaz de renderizar conteúdo diferente dependendo do estado de promessa. Quando sua página for carregada inicialmente, a seção "Razões para adorar queijo" será exibida Loading.... Quando a promessa for carregada, ela renderizará todos os motivos associados a cada registro de seu modelo.

Cada seção carregará independentemente e não bloqueará a renderização do conteúdo principal imediatamente.

Este é um exemplo simplista, mas espero que todos o considerem tão útil quanto eu.

Se você está procurando fazer algo semelhante para muitas linhas de conteúdo, pode achar o exemplo da Novelys acima ainda mais relevante. Caso contrário, o procedimento acima deve funcionar bem para você.

SwarmIntelligence
fonte
8

Isso pode não ser uma prática recomendada e uma abordagem ingênua, mas mostra conceitualmente como você faria para ter vários modelos disponíveis em uma rota 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 que ajude

pixel intuitivo
fonte
1
Essa abordagem é boa, mas não aproveita muito o Ember Data. Para alguns cenários onde os modelos não estão relacionados, procurei algo semelhante a isso.
MilkyWayJoe
4

Graças a todas as outras excelentes respostas, criei um mixin que combina as melhores soluções aqui em uma interface simples e reutilizável. Ele executa um Ember.RSVP.hashin afterModelpara os modelos que você especifica e, em seguida, injeta as propriedades no controlador em setupController. Ele não interfere com o modelgancho padrão , então você ainda definiria isso como normal.

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

Aqui está o 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);
  }
});
Christopher Sansone
fonte
2

https://stackoverflow.com/a/16466427/2637573 é adequado para modelos relacionados. No entanto, com a versão recente do Ember CLI e Ember Data, há uma abordagem mais simples para modelos não 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)
    });
  }
});

Se você deseja recuperar apenas a propriedade de um objeto para model2, use DS.PromiseObject em vez 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
fonte
Eu tenho uma postrota / visualização de edição onde desejo renderizar todas as tags existentes no banco de dados para que o usuário possa clicar para adicioná-las ao post que está sendo editado. Quero definir uma variável que representa uma matriz / coleção dessas marcas. A abordagem que você usou acima funcionaria para isso?
Joe
Claro, você criaria um PromiseArray(por exemplo, "tags"). Então, em seu modelo, você o passaria para o elemento de seleção do formulário correspondente.
AWM de
1

Acrescentando à resposta do MilkyWayJoe, obrigado a propósito:

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

retorna

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

autor seria

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

comentários

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

... acredito que os links backs são importantes para que o relacionamento pleno seja estabelecido. Espero que ajude alguém.

philn5d
fonte
0

Você poderia usar os ganchos beforeModelou, afterModelpois eles são sempre chamados, mesmo se modelnão forem chamados porque você está usando segmentos dinâmicos.

De acordo com os documentos de roteamento assíncrono :

O gancho do modelo cobre muitos casos de uso para transições de pausa na promessa, mas às vezes você precisará da ajuda dos ganchos relacionados beforeModel e afterModel. O motivo mais comum para isso é que se você estiver fazendo a transição para uma rota com um segmento de URL dinâmico por meio de {{link-to}} ou transiçãoTo (ao contrário de uma transição causada por uma alteração de URL), o modelo para a rota que você 'está em transição para já terá sido especificado (por exemplo, {{# link-to' artigo 'artigo}} ou this.transitionTo (' artigo ', artigo)), caso em que o gancho do modelo não será chamado. Nesses casos, você precisará usar o hook beforeModel ou afterModel para hospedar qualquer lógica enquanto o roteador ainda está reunindo todos os modelos da rota para realizar uma transição.

Então, digamos que você tenha uma themespropriedade em seu SiteController, você poderia ter algo assim:

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
fonte
1
Acho que usar thisdentro da promessa geraria uma mensagem de erro. Você pode definir var _this = thisantes do retorno e, em seguida, fazer _this.set(dentro do then(método para obter o resultado desejado
Duncan Walker,