Working with traits

Learn how to use Mirage's traits to simplify data creation

Transcript

In this video I want to show you how to use Mirage's traits to clean up the code you write to seed your application's dummy data.

I'm working on this CMS app that helps users manage their blog posts. If we click on blog posts right here, we see that there's some charts and a table showing all of our blog posts. We can click on individual posts and view their detail pages. Some of these posts have comments, and if we click on the detail page we can see them down here.

Now, all this data is being powered by mirage. Let's come to the code and take a quick look at the models in this app. Right now we have two: a post model and a comment model, and there's a one-to-many relationship between them.

Our app is running in development, so the data we see is coming from our default scenario file. So let's open mirage/scenarios/default.js and take a look at the code. Now up here we're using server.createList and making 3 posts. And then next we're making a long post, which we can see from the comment and also because the text property is 30 paragraphs long. And finally, we make a post that has some comments associated with it.

So you can see here that we've kind of split up our data creation into these different variations, and this happens a lot when you want to create a different shapes of your data to help you design and develop your app, or especially when you're writing tests that call for specific configurations of your data. What I want to show you is a feature of Mirage called traits that can help us clean this code up, and make it easier to understand and more reusable for both our development seed file and for when we're writing tests.

So, let's start with this long post here. We can see we're creating a post with the title "A very long post", and if we come back to the app, we can see it here in the index. And if we click it, we can see it is indeed a very long post. So what we're going to do is create a new trait that will make it easy for us make long posts just like this one.

We'll start by opening up the posts factory file, and here can we see all the different attributes on our post. Now to make a trait, first we'll come up to the top and import it from ember-cli-mirage, and then we'll come down here and make a new property, and we'll call it long since this trait represents a long post. And then we'll use the trait helper, which takes a POJO that we can use to pass in attribute overrides, just like we do out here in server.create. So let's come over here, grab these and paste them into our trait:

long: trait({
  author: 'Jane Doe',
  title: 'A very long post',
  text: faker.lorem.paragraphs(30).split('\n').join('\n<br /><br />')
})

And I'll delete the author for now, since this trait is just concerned with the post length. And now back in our scenario file we can delete this, and let's just save these files and look at our app.

Now we don't see our Very long blog post anymore, since we're just making a plain post here. To use our new trait, we just need to pass it in as the next argument to server.create. Let's do that, save and look at our app. And there we see our dummy record, just like before. So this is how we actually invoke our traits in server.create. And now, if we ever need a long post, we have this simple, expressive way to get one.

Now with traits, I like to use functions for things like this title, since they're likely to be used multiple times. So let's update this to be a function, and we'll call it long post #1, long post #2 and so on.

title(i) {
  return `Long post #${i}`;
},

And now we can come back to our scenario, and let's use createList to make 3 long posts

server.createList('post', 3, 'long');

And now we have these three long posts in our app here. Cool.

Now, in our original scenario file we had Jane Doe specified as the author of this post, but we didn't add that to the trait. And that's because, as their name suggests, traits should really be focused on one distinguishing characteristic of your model - in this case, the post length. So, what do we do if we wanted a long post written by Jane Doe?

Well, even though we're passing in a trait to this server.create call, we can pass in a POJO of attribute overrides as the last parameter, just like always. So we can add back in a POJO here, and write author Jane Doe

server.createList('post', 3, 'long', {
  author: 'Jane Doe'
});

And now if we look at our app, we see the three long posts are all written by Jane Doe. So, traits plus attribute overrides ends up being a very powerful combination.

Now, let's move on to our last block of code here, where we create a post associated with three comments. We want to use a trait to clean this up, but first I want to show you another API on Mirage's factories that's called afterCreate.

afterCreate() {
}

AfterCreate is a hook that gets called every time a model is created. It gets two parameters, the model from this factory that was just created, and the Mirage server instance. In this case we're in the post factory, so let's name this first parameter post. What this hook does is give us an opportunity to change or augment our newly created model with additional attributes, or even new relationships. For instance I could call

post.update('category', 'Literature');

and now every time a post is created, it will get assigned this category. And we can see in our app that all of our posts are now LIterature posts.

Also, since we have the server available, we can call server.create here to make related data. Let's grab this code that creates 3 comments and paste it in afterCreate. Now, every time a post is created, we then create three associated comments for it. If we save this and check our app, now all of our posts have 3 comments. So afterCreate is a very handy way to create our dummy data.

But we don't want every post to start out with 3 comments - we want an easy way to make posts that have comments. Sounds like a perfect use case for a trait! We really want to be able to say, server, create me a post with comments. So let's make this withComments trait.

So withComments is a trait, and we can actually just move this afterCreate to within our new trait

withComments: trait({
  afterCreate(post, server) {
    server.createList('comment', 3, {
      post
    });
  }
}),

We could add other attributes here too like author, but this trait will only be concerned with making associated comments for the post that was just created.

Now back in our scenario we can delete this, save, and there we see our post with three comments!

We're not using faker anymore so we can delete it from here, and looking at our refactored code, it's much clearer than before - in fact it's so clear that we can even delete these comments! This tells us that we're creating a long post, and this tells us that we're creating a post with comments! So this is one of the main benefits of traits - it encodes these higher-level concepts into our creation logic, which keeps our code expressive and relevant for anyone else reading our work. All that noisy code we had before was really just a distraction from what we were trying to convey. Traits help us pull that meaning out of our code and make it front-and-center.

One last thing - multiple traits can be used together! If we wanted a post that was long and withComments, we're ready to go!

server.create('post', 'long', 'withComments');

If we save this, there we see it in our app.

So, if you keep your traits small and focused, what you'll end up with is a very powerful and flexible data-creation story for your application.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.