Як використовувати кілька моделей з одним маршрутом у EmberJS / Ember Data?


85

З прочитання документів здається, що вам потрібно (або слід) призначити модель маршруту приблизно так:

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

Що робити, якщо мені потрібно використовувати кілька об’єктів за певним маршрутом? тобто повідомлення, коментарі та користувачі? Як визначити маршрут для їх завантаження?

Відповіді:


117

Останнє оновлення назавжди : я не можу продовжувати оновлювати це. Тож це застаріло і, швидше за все, буде таким. ось кращий та сучасніший потік EmberJS: Як завантажити кілька моделей за одним маршрутом?

Оновлення: У своїй оригінальній відповіді я сказав використовувати embedded: trueу визначенні моделі. Це неправильно. У редакції 12 Ember-Data очікує, що зовнішні ключі визначатимуться суфіксом ( посиланням ) _idдля одного запису або _idsдля збору. Щось схоже на наступне:

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

Я оновив скрипку і буду робити це знову, якщо щось зміниться або якщо я знайду більше проблем із кодом, наведеним у цій відповіді.


Відповідь із суміжними моделями:

Для сценарію, який ви описуєте, я б покладався на асоціації між моделями (налаштування embedded: true) і завантажував би Postмодель лише за цим маршрутом, враховуючи, що я можу визначити DS.hasManyасоціацію для Commentмоделі та DS.belongsToасоціацію Userяк для моделей, так Commentі для Postмоделей. Щось на зразок цього:

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

Це визначення дасть щось на зразок наступного:

Асоціації між моделями

З цим визначенням, щоразу, коли я findпублікую допис, я матиму доступ до колекції коментарів, пов’язаних із цим дописом, і автора коментаря, а також користувача, який є автором допису, оскільки всі вони вбудовані . Маршрут залишається простим:

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

Отже, у PostRoute(або PostsPostRouteякщо ви використовуєте resource) мої шаблони матимуть доступ до контролера content, який є Postмоделлю, тому я можу звернутися до автора просто як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>

(див. скрипку )


Відповідь із не пов’язаними моделями:

Однак, якщо ваш сценарій трохи складніший, ніж ви описали, та / або вам доводиться використовувати (або запитувати) різні моделі для певного маршруту, я б рекомендував зробити це в Route#setupController. Наприклад:

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

І тепер, коли я перебуваю в маршруті Post, мої шаблони матимуть доступ до userвластивості в контролері, як це було налаштовано на 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>

(див. скрипку )


15
Дякую так багато за час , витрачений на пост , який я знайшов , що це дуже корисно.
Анонім

1
@MilkyWayJoe, справді хороший пост! Тепер мій підхід виглядає справді наївним :)
intuitivepixel

Проблема ваших не пов’язаних моделей полягає в тому, що вони не приймають обіцянок, як приймає модель, правда? Чи є для цього обхідні шляхи?
miguelcobain

1
Якщо я правильно зрозумів вашу проблему, ви можете легко її адаптувати. Просто почекайте, поки обіцянка виконається, і лише тоді встановіть модель (и) як змінну контролера
Fabio

Окрім ітерації та відображення коментарів, буде чудово, якщо ви зможете продемонструвати приклад того, як хтось може додати новий коментар post.comments.
Деймон Ав

49

Використання Em.Objectдля інкапсуляції декількох моделей - це хороший спосіб отримати всі дані в modelгачку. Але це не може забезпечити підготовку всіх даних після рендерінгу подання.

Інший вибір - використання Em.RSVP.hash. Він поєднує кілька обіцянок разом і повертає нову обіцянку. Нова обіцянка буде вирішена після вирішення всіх обіцянок. І setupControllerне викликається, доки обіцянка не буде вирішена або відхилена.

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

Таким чином ви отримуєте всі моделі перед візуалізацією перегляду. І використання Em.Objectне має цієї переваги.

Ще одна перевага полягає в тому, що ви можете поєднувати обіцянку та не обіцянку. Подобається це:

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

Перевірте це, щоб дізнатися більше про це Em.RSVP: https://github.com/tildeio/rsvp.js


Але не використовуйте Em.Objectта не використовуйте Em.RSVPрішення, якщо ваш маршрут має динамічні сегменти

Основна проблема полягає в link-to. Якщо ви зміните URL-адресу, клацнувши посилання, згенероване link-toз моделями, модель передається безпосередньо до цього маршруту. У цьому випадку modelгачок не викликається, і в результаті setupControllerви отримуєте модель, link-toяку вам дадуть.

Приклад такий:

Код маршруту:

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

І link-toкод, пост - це модель:

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

Тут справи стають цікавими. Коли ви використовуєте url /post/1для відвідування сторінки, modelвикликається хук і setupControllerотримує звичайний об'єкт, коли обіцянка вирішена.

Але якщо ви перейдете на сторінку, клацнувши link-toпосилання, вона передає postмодель, PostRouteі маршрут ігноруватиме modelхук. У цьому випадку setupControllerви отримаєте postмодель, звичайно, ви не можете отримати користувача.

Тож переконайтеся, що не використовуєте їх у маршрутах з динамічними сегментами.


2
Моя відповідь стосується попередньої версії Ember & Ember-Data. Це справді хороший підхід +1
MilkyWayJoe

11
Насправді є. якщо ви хочете передати ідентифікатор моделі замість самої моделі в помічник зв’язування, завжди запускається гачок моделі.
darkbaby123

2
Це слід задокументувати десь у путівниках Ember (найкращі практики чи щось інше). Це важливий випадок використання, з яким я впевнений, що багато людей стикаються.
yorbro

1
Якщо ви використовуєте controller.setProperties(model);, не забудьте додати ці властивості зі значенням за замовчуванням до контролера. В іншому випадку це призведе до виняткуCannot delegate set...
Матеуш Новак,

13

Деякий час я користувався Em.RSVP.hash, однак проблема, з якою я зіткнувся, полягала в тому, що я не хотів, щоб моє подання чекало, поки всі моделі будуть завантажені перед візуалізацією. Однак я знайшов чудове (але відносно невідоме) рішення завдяки людям у Novelys, яке передбачає використання Ember.PromiseProxyMixin :

Скажімо, у вас є вид, який має три окремі візуальні розділи. Кожен із цих розділів повинен бути підкріплений власною моделлю. Модель, що підтримує вміст "сплеск" у верхній частині подання, невелика і завантажується швидко, тому ви можете завантажити її зазвичай:

Створити маршрут main-page.js:

import Ember from 'ember';

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

Тоді ви можете створити відповідний шаблон рулів 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>

Тож, скажімо, у своєму шаблоні ви хочете мати окремі розділи про ваші стосунки кохання / ненависті із сиром, кожен (з якихось причин) підкріплений власною моделлю. У вас є багато записів у кожній моделі з великими подробицями, що стосуються кожної причини, однак ви хотіли б, щоб вміст зверху відображався швидко. Тут з’являється {{render}}помічник. Ви можете оновити шаблон так:

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

Тепер вам потрібно буде створити контролери та шаблони для кожного. Оскільки вони фактично ідентичні для цього прикладу, я просто використаю один.

Створіть контролер під назвою 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);
        }
    }
});

Ви помітите, що ми використовуємо PromiseProxyMixinтут, що робить контролер обіцяним. Коли ініціалізується контролер, ми вказуємо, що обіцянка повинна завантажувати love-cheeseмодель через дані Ember. Вам потрібно буде встановити цю властивість у властивості контролера promise.

Тепер створіть шаблон із назвою love-cheese.hbs:

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

У вашому шаблоні ви зможете відображати різний вміст залежно від стану обіцянки. Коли ваша сторінка завантажується спочатку, з’явиться розділ "Причини, якими я люблю сир"Loading... . Коли обіцянка завантажується, вона відображатиме всі причини, пов’язані з кожним записом вашої моделі.

Кожен розділ завантажується самостійно і не блокує відразу основний вміст.

Це спрощений приклад, але я сподіваюся, що всі інші знайдуть його таким же корисним, як і я.

Якщо ви хочете зробити щось подібне для багатьох рядків вмісту, можливо, приклад Novelys вище ще більш актуальним. Якщо ні, то вищезазначене має вам добре підходити.


8

Це може бути не найкращою практикою та наївним підходом, але концептуально показує, як ви хочете мати кілька моделей, доступних на одному центральному маршруті:

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

сподіваюся, це допоможе


1
Такий підхід чудовий, просто не надто користуючись перевагами Ember Data. У деяких сценаріях, коли моделі не пов'язані між собою, я хотів би щось подібне до цього.
MilkyWayJoe

4

Завдяки всім іншим чудовим відповідям я створив комбінацію, яка поєднує найкращі рішення тут у простий та багаторазовий інтерфейс. Він виконує Ember.RSVP.hashв afterModelпротягом моделей ви вказуєте, а потім впорскує властивості в контролер в setupController. Це не заважає стандартному modelгачку, тому ви все одно визначите це як звичайне.

Приклад використання:

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

Ось міксин:

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 чудово підходить для пов’язаних моделей. Однак з останньою версією Ember CLI та Ember Data існує простіший підхід до непов’язаних моделей:

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

Якщо ви хочете отримати лише властивість об’єкта для model2, використовуйте DS.PromiseObject замість 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'))
    });
  }
});

У мене є postмаршрут редагування / подання, де я хочу відтворити всі існуючі теги в БД, щоб користувач міг натиснути, щоб додати їх до публікації, що редагується. Я хочу визначити змінну, яка представляє масив / колекцію цих тегів. Чи підійде для цього підхід, який ви використали вище?
Джо

Звичайно, ви б створили PromiseArray(наприклад, "теги"). Потім у своєму шаблоні ви передасте його елементу виділення відповідної форми.
AWM

1

Додаючи до відповіді MilkyWayJoe, дякую btw:

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

повертається

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

автором буде

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

коментарі

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

... Я вважаю, що підтримка посилань важлива для того, щоб встановити повноцінні стосунки. Надія, що комусь допомагає.


0

Ви можете використовувати хуки beforeModelабо, afterModelяк їх завжди викликають, навіть якщо modelвони не викликаються, оскільки ви використовуєте динамічні сегменти.

Відповідно до асинхронних документів маршрутизації :

Модель гачка охоплює багато випадків використання для переходів на паузу за обіцянкою, але іноді вам знадобиться допомога відповідних гачків beforeModel і afterModel. Найпоширенішою причиною цього є те, що якщо ви переходите на маршрут із динамічним сегментом URL-адреси за допомогою {{link-to}} або переходуTo (на відміну від переходу, спричиненого зміною URL-адреси), модель маршруту, який ви перехід до буде вже вказано (наприклад, {{# link-to 'article' article}}) або this.transitionTo ('article', article)), і в цьому випадку гачок моделі не буде викликаний. У цих випадках вам потрібно буде використовувати гачок beforeModel або afterModel для розміщення будь-якої логіки, поки маршрутизатор все ще збирає всі моделі маршруту для виконання переходу.

Отже, скажімо, у вас є themesвласність SiteController, у вас може бути щось подібне:

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
Я думаю, що використання thisвсередині обіцянки дасть повідомлення про помилку. Ви можете встановити var _this = thisперед поверненням, а потім зробити _this.set(всередині then(методу, щоб отримати бажаний результат
Duncan Walker
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.