One technique I often like to do when I am refactoring is move relationship logic outside of an action into the Ember Data model behind a custom method. Here's an example:
import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class PostController extends Controller {
@action
addComment(post, body) {
let comment = this.store.createRecord('comment', { body });
post.get('comments').pushObject(comment);
comment.save().then(() => {
post.save();
});
}
}
Let's say in my app I have a button that is wired up to the addComment
controller action. When the action is fired, the comment
record is created, associated with the post
, and both the comment
and post
are saved.
Instead of interacting with the store and the comments
relationship on the post
model, I like to move this logic into a custom method on the post
model. This way, the controller action can be simplified to this:
import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class PostController extends Controller {
@action
addComment(post, body) {
post.addComment({ body });
}
}
The advantage of this is that the controller action becomes much more expressive because it is using the language of the domain instead of the CRUD language of the store. The data concerns can be left to the model while the UI concerns can be left to the action. If the action doesn't have to deal with too many details related to records and relationships, maintenance can be much easier.
Here is what the post.addComment
implementation might look like:
import Model, { attr, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr('string') body;
@hasMany('comment') comments;
addComment(commentData) {
let comment = this.store.createRecord('comment', commentData);
this.get('comments').pushObject(comment);
return comment.save().then(() => {
return this.save();
});
}
}
Now, the post
model has a custom method addComment
that encapsulates the comment
relationship logic instead of it being handled directly in the controller action.
One thing to note here is that models already have a property on them called store
. However, according to the API docs for DS.Model, it isn't public. Therefore, I've opted to look up the store from the container via the getOwner
API.
Here's another example. Let's say you have an action where a user can buy movie tickets.
import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class MovieController extends Controller {
@action
buyMovieTickets(movie, seats) {
let order = this.store.createRecord('order', { movie });
seats.forEach(seat => {
order.get('seats').pushObject(seat);
});
return order.save();
}
}
First we have to create an order
record and associate the order
with the movie
. Maybe the user had an interface to select seats, so we'll need to add each seat
record to the order
. Finally we'll need to save the order
. This works, but if this action has presentation logic in there as well, the action can get long and maintenance can become more difficult.
Instead, I'd like the action to be simpler and express the intent more clearly in the language of the domain. So maybe something like this:
import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class MovieController extends Controller {
@action
buyMovieTickets(movie, seats) {
return movie.buyTickets(seats);
}
}
Now, the movie
model can handle the specifics of creating an order
, associating the movie
with the order
, associating the selected seats with the order
, and finally saving the order
record. Here is what movie.buyTickets
might look like:
import Model, { hasMany } from '@ember-data/model';
export default class MovieModel extends Model {
@hasMany() orders;
buyTickets(seats) {
let order = this.store.createRecord('order', { movie: this });
seats.forEach(seat => {
order.get('seats').pushObject(seat);
});
return order.save();
}
}
Pushing off relationship logic into a custom method in the model can make actions much more expressive, and helps separate data responsibilities from controller/UI logic.