Jak korzystać z wielu modeli z jedną trasą w EmberJS / Ember Data?

85

Po przeczytaniu dokumentacji wygląda na to, że musisz (lub powinieneś) przypisać model do trasy w następujący sposób:

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

A jeśli potrzebuję użyć kilku obiektów na określonej trasie? tj. posty, komentarze i użytkownicy? Jak określić trasę, aby je załadować?

Anonimowy
źródło

Odpowiedzi:

117

Ostatnia aktualizacja na zawsze : nie mogę tego aktualizować. Więc to jest przestarzałe i prawdopodobnie tak będzie. oto lepszy i bardziej aktualny wątek EmberJS: Jak załadować wiele modeli na tej samej trasie?

Aktualizacja: W mojej pierwotnej odpowiedzi powiedziałem, aby użyć embedded: truew definicji modelu. To jest niepoprawne. W wersji 12 Ember-Data oczekuje, że klucze obce będą definiowane z sufiksem ( łączem ) _iddla pojedynczego rekordu lub _idsdo kolekcji. Coś podobnego do następującego:

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

Zaktualizowałem skrzypce i zrobię to ponownie, jeśli coś się zmieni lub jeśli znajdę więcej problemów z kodem podanym w tej odpowiedzi.


Odpowiedz za pomocą powiązanych modeli:

W przypadku scenariusza, który opisujesz, oparłbym się na powiązaniach między modelami (ustawieniami embedded: true) i załadowałbym Postmodel tylko w tej trasie, biorąc pod uwagę, że mogę zdefiniować DS.hasManyskojarzenie dla Commentmodelu i DS.belongsToskojarzenie dla Userobu modeli Commenti Post. Coś takiego:

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

Ta definicja dałaby coś takiego:

Powiązania między modelami

Dzięki tej definicji za każdym razem, gdy piszę findpost, będę miał dostęp do zbioru komentarzy powiązanych z tym postem, a także autora komentarza oraz użytkownika, który jest autorem posta, ponieważ wszystkie są osadzone . Trasa pozostaje prosta:

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

Więc w PostRoute(lub PostsPostRoutejeśli używasz resource) moje szablony będą miały dostęp do kontrolera content, który jest Postmodelem, więc mogę odwołać się do autora, po prostu jakoauthor

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

(patrz skrzypce )


Odpowiedź z niepowiązanymi modelami:

Jeśli jednak Twój scenariusz jest nieco bardziej złożony niż ten, który opisałeś i / lub musisz używać (lub przeszukiwać) różne modele dla określonej trasy, polecam zrobić to w programie Route#setupController. Na przykład:

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

A teraz, gdy jestem w trasie postu, moje szablony będą miały dostęp do userwłaściwości w kontrolerze, tak jak zostało ustawione w setupControllerhooku:

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

(patrz skrzypce )

MilkyWayJoe
źródło
15
Dziękuję bardzo za poświęcenie czasu na opublikowanie tego, uznałem to za naprawdę przydatne.
Anonimowy
1
@MilkyWayJoe, naprawdę dobry post! Teraz moje podejście wygląda na naprawdę naiwne :)
intuitivepixel
Problem z twoimi niepowiązanymi modelami polega na tym, że nie akceptuje obietnic, jak robi to haczyk do modeli, prawda? Czy jest jakieś obejście tego problemu?
miguelcobain
1
Jeśli dobrze rozumiem Twój problem, możesz go łatwo dostosować. Poczekaj tylko na spełnienie obietnicy i dopiero wtedy ustaw model (y) jako zmienną sterownika
Fabio
Oprócz iterowania i wyświetlania komentarzy, byłoby wspaniale, gdybyś mógł pokazać przykład, jak ktoś może dodać nowy komentarz do post.comments.
Damon Aw,
49

Używanie Em.Objectdo hermetyzacji wielu modeli to dobry sposób na przechwycenie wszystkich danych model. Ale nie może zapewnić przygotowania wszystkich danych po wyrenderowaniu widoku.

Innym wyborem jest użycie Em.RSVP.hash. Łączy kilka obietnic razem i zwraca nową obietnicę. Nowa obietnica, jeśli zostanie rozwiązana po rozwiązaniu wszystkich obietnic. I setupControllernie jest powołany, dopóki obietnica nie zostanie rozwiązana lub odrzucona.

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

W ten sposób otrzymujesz wszystkie modele przed renderowaniem widoku. A używanie Em.Objectnie ma tej przewagi.

Kolejną zaletą jest to, że możesz łączyć obietnicę i brak obietnicy. Lubię to:

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

Sprawdź to, aby dowiedzieć się więcej o Em.RSVP: https://github.com/tildeio/rsvp.js


Ale nie używaj Em.Objectlub Em.RSVProzwiązania, jeśli Twoja trasa ma dynamiczne segmenty

Główny problem to link-to. Jeśli zmienisz adres URL, klikając link generowany przez link-toz modelami, model zostanie przekazany bezpośrednio do tej trasy. W tym przypadku modelhak nie jest wywoływany i setupControllerdostajesz model, link-toktóry ci daje.

Przykład jest taki:

Kod trasy:

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

A link-tokod, post to model:

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

Tutaj robi się ciekawie. Kiedy używasz url /post/1do odwiedzenia strony, modelwywoływany jest haczyk i setupControllerpobiera zwykły obiekt po rozwiązaniu obietnicy.

Ale jeśli odwiedzasz stronę przez kliknięcie link-tolinku, przekazuje postmodel do, PostRoutea trasa zignoruje modelhak. W takim przypadku setupControllerdostaniesz postmodel, oczywiście nie możesz pobrać użytkownika.

Dlatego upewnij się, że nie używasz ich w trasach z dynamicznymi segmentami.

darkbaby123
źródło
2
Moja odpowiedź dotyczy wcześniejszej wersji Ember & Ember-Data. To naprawdę dobre podejście +1
MilkyWayJoe
11
Właściwie jest. jeśli chcesz przekazać identyfikator modelu zamiast samego modelu do linku do pomocnika, zawsze wyzwalany jest model.
darkbaby123
2
Powinno to być udokumentowane gdzieś w przewodnikach Ember (najlepsze praktyki czy coś takiego). Jest to ważny przypadek użycia, z którym z pewnością spotyka się wiele osób.
yorbro,
1
Jeśli używasz controller.setProperties(model);, nie zapomnij dodać tych właściwości z wartością domyślną do kontrolera. Inaczej wyrzuci wyjątekCannot delegate set...
Mateusz Nowak
13

Przez jakiś czas używałem Em.RSVP.hash, jednak problem, na który natknąłem się, polegał na tym, że nie chciałem, aby mój widok czekał, aż wszystkie modele zostaną załadowane przed renderowaniem. Jednak dzięki ludziom z Novelys znalazłem świetne (ale stosunkowo nieznane) rozwiązanie, które polega na wykorzystaniu Ember. PromiseProxyMixin :

Załóżmy, że masz widok, który ma trzy odrębne sekcje wizualne. Każda z tych sekcji powinna być poparta własnym modelem. Model, na którym umieszczona jest zawartość „splash” u góry widoku, jest mały i szybko się wczytuje, więc można go wczytać normalnie:

Utwórz trasę main-page.js:

import Ember from 'ember';

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

Następnie możesz utworzyć odpowiedni szablon Kierownica 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>

Powiedzmy więc, że w swoim szablonie chcesz mieć osobne sekcje dotyczące relacji miłości / nienawiści z serem, z których każda (z jakiegoś powodu) jest poparta własnym modelem. Masz wiele rekordów w każdym modelu z obszernymi szczegółami dotyczącymi każdego powodu, jednak chcesz, aby zawartość na górze była szybko renderowana. Tutaj pojawia się {{render}}pomocnik. Możesz zaktualizować swój szablon w następujący sposób:

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

Teraz musisz utworzyć kontrolery i szablony dla każdego. Ponieważ w tym przykładzie są identyczne, użyję tylko jednego.

Utwórz kontroler o nazwie 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);
        }
    }
});

Zauważysz, że używamy PromiseProxyMixintutaj, co sprawia, że ​​kontroler jest świadomy obietnic. Podczas inicjalizacji kontrolera wskazujemy, że obietnica powinna ładować love-cheesemodel za pośrednictwem Ember Data. Musisz ustawić tę właściwość we właściwości kontrolera promise.

Teraz utwórz szablon o nazwie love-cheese.hbs:

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

W swoim szablonie będziesz mógł renderować różne treści w zależności od stanu obietnicy. Po pierwszym załadowaniu strony zostanie wyświetlona sekcja „Powody, dla których kocham ser” Loading.... Kiedy obietnica zostanie załadowana, wyświetli wszystkie przyczyny związane z każdym rekordem twojego modelu.

Każda sekcja wczytuje się niezależnie i nie blokuje natychmiastowego renderowania głównej treści.

To jest uproszczony przykład, ale mam nadzieję, że wszyscy uznają go za równie przydatny, jak ja.

Jeśli chcesz zrobić coś podobnego dla wielu wierszy treści, powyższy przykład Novelys może okazać się jeszcze bardziej odpowiedni. Jeśli nie, powyższe powinno działać dobrze.

Inteligencja roju
źródło
8

Może to nie jest najlepsza praktyka i naiwne podejście, ale koncepcyjnie pokazuje, w jaki sposób można by mieć dostęp do wielu modeli na jednej centralnej trasie:

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

mam nadzieję, że to pomoże

intuicyjny piksel
źródło
1
Takie podejście jest w porządku, po prostu nie wykorzystując zbytnio możliwości Ember Data. W przypadku niektórych scenariuszy, w których modele nie są powiązane, dostałem coś podobnego do tego.
MilkyWayJoe
4

Dzięki wszystkim innym znakomitym odpowiedziom stworzyłem mixin, który łączy najlepsze rozwiązania tutaj w prosty i wielokrotnego użytku interfejs. Wykonuje Ember.RSVP.hashin afterModeldla określonych modeli, a następnie wstrzykuje właściwości do kontrolera w setupController. Nie koliduje ze standardowym modelzaczepem, więc nadal możesz to zdefiniować jako normalne.

Przykładowe zastosowanie:

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

Oto 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
źródło
2

https://stackoverflow.com/a/16466427/2637573 jest odpowiedni dla powiązanych modeli. Jednak w najnowszej wersji Ember CLI i Ember Data istnieje prostsze podejście do niepowiązanych modeli:

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

Jeśli chcesz pobrać tylko właściwość obiektu model2, użyj DS.PromiseObject zamiast 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
źródło
Mam posttrasę / widok edycji, w którym chcę wyrenderować wszystkie istniejące tagi w bazie danych, aby użytkownik mógł kliknąć, aby dodać je do edytowanego postu. Chcę zdefiniować zmienną, która reprezentuje tablicę / kolekcję tych tagów. Czy podejście, które zastosowałeś powyżej, zadziała w tym przypadku?
Joe
Jasne, utworzyłbyś PromiseArray(np. „Tagi”). Następnie w swoim szablonie przekażesz go do elementu wyboru odpowiedniego formularza.
AWM,
1

Dodając do odpowiedzi MilkyWayJoe, przy okazji:

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

zwroty

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

autor byłby

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

komentarze

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

... Uważam, że powiązania są ważne, aby nawiązać pełną relację. Mam nadzieję, że to komuś pomoże.

philn5d
źródło
0

Możesz użyć haków beforeModellub, afterModelponieważ są one zawsze nazywane, nawet jeśli modelnie są wywoływane, ponieważ używasz segmentów dynamicznych.

Zgodnie z dokumentacją routingu asynchronicznego :

Hak modelu obejmuje wiele przypadków użycia dla przejść z przerwą na obietnicę, ale czasami będziesz potrzebować pomocy powiązanych zaczepów przed modelem i po modelu. Najczęstszą tego przyczyną jest to, że jeśli przechodzisz na trasę z segmentem dynamicznego adresu URL przez {{link-to}} lub przejścieTo (w przeciwieństwie do przejścia spowodowanego zmianą adresu URL), model trasy, 'ponowne przejście do będzie już określone (np. {{# link-to' artykuł 'artykuł}} lub this.transitionTo (' artykuł ', artykuł)), w którym to przypadku hak modelu nie zostanie wywołany. W takich przypadkach musisz użyć zaczepu beforeModel lub afterModel, aby pomieścić dowolną logikę, podczas gdy router nadal gromadzi wszystkie modele tras, aby wykonać przejście.

Więc powiedz, że masz themesnieruchomość na swoim SiteController, możesz mieć coś takiego:

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.
źródło
1
Myślę, że użycie thiswewnątrz obietnicy dałoby komunikat o błędzie. Możesz ustawić var _this = thisprzed powrotem, a następnie _this.set(zastosować then(metodę, aby uzyskać pożądany efekt
Duncan Walker