⚠️ This tutorial uses an old version of Ember. Some things might have changed!
What is Ember Data?
Ember Data is a library that comes bundled with Ember CLI, and helps you manage the data of your app.
When you think of "data", you might instinctively be reminded about databases or ORMs. But remember that Ember is a front-end only framework, and that goes for Ember Data as well. It makes no direct contact with any database whatsoever.
So what does it do? In a nutshell, Ember Data stores your models in the browser's local storage, pulls them out when you need them and sends AJAX requests that are based on a set of conventions for REST APIs.
Don't worry if the words above sound really confusing right now, we're going to take it one bit at a time in this chapter!
Creating our models
Let's first ask ourselves: what data do we actually need in our app? In Chirper, there are really only two things we have to store and retrieve:
Users
Chirps
Seems easy enough. Let's start with the User model:
We'll make it very simple for now and say that a user only consists of four things: a username, the number of chirps (that the user has written), the number of followers, and the number of followings. Open the generated model-file and add the following attributes:
As you can see, we use DS.attr() to specify the data type of the model attributes. You can find all the available types in the Ember Data Documentation.
Installing Ember CLI Mirage
Alright, so we have a model, but we still don't have any actual data. Normally, this data should come from a server-side JSON API, but for now, we want to keep our project front-end only in order to keep experimenting and making quick iterations.
Luckily, we can actually add fake data for now by using a great add-on called ember-cli-mirage. So let's install it!
Note: We're not using the newest version of Mirage, since 0.2.0 introduced a few breaking changes. I'll update this course once I've gone through them all.
Restart your Ember server after the installation is done. Now you should see a new folder: app/mirage
. This is where we will put all of our mock data.
Alright, now let's specify how Ember Data should behave in order to fetch our mock data. First of all, we will specify what kind of requests Ember should make, and how the returned data is formatted. This is specified through an adapter. For this tutorial, we're going to use Ember's simple and predefined RESTAdapter throughout the whole application, so let's generate that:
In the adapter.js
file, we specify that it is a RESTAdapter we want, and also that we want all data request URLs to be prefixed with /api
(otherwise they might collide with our existing route URLs):
This prefix will also need to be specified in our mirage config.js
file:
Okay, so we have a model and we've specified our adapter. Now, we just need to tell Ember when and where in our app we want this data to be loaded.
Fetching mock data
In our case, we want to load the user data as soon as the "home"-screen is launched, and then populate the profile-glance
component with that data. This is what the route.js
-files are for: besides connecting to a URL, they also sets up the initial state of the page they correspond to.
When retrieving a data record for a route, we use this.store.findRecord()
, where the first argument is the name of the model and the second argument is the id of the record that we want to load. So in the code below, we're loading the user with id 1
and assigning it to the user
-key:
If we go back to the browser now you should see a blank page. Uh-oh!
Let's bring up the JavaScript console (View > Developer > JavaScript Console
) and check if we can see what happened:
As you can see, Ember is doing the right thing, trying to fetch some data from our mock server by making a request to /api/users/1
. The problem is that we haven't defined our mock data yet, so let's do that now!
We're going to use fixtures for our mock data, so the first thing you need to do is delete the folder app/mirage/scenarios
. Then we'll create a folder called fixtures
in our new mirage
-folder, and in it we'll create a file called users.js
:
Finally, we specify the URL that we want to make available in the Mirage config-file. Ember CLI Mirage is smart enough to follow the RESTAdapter's conventions and will return the requested user automagically through one simple line of code!
Now go back to the browser, and the page should work as usual again, with a console message informing you that the request was successful!
Although the console log tells us that our model is loaded now, there's no way for us to really see it on the page. Let's open our "home"-template and try to print out the username of the user we loaded:
Passing the data to our component
We now know that our data fetching works, but it's rendering on the wrong place, so let's remove the markup we just added to the template page. We want the data to be rendered inside of the profile-glance
-component instead!
One thing we didn't mention in the last chapter is that you can actually pass data down to your components by adding attributes to it. Let's see how that works!
Adding the chirp-data
Alright, now that you have a rough understanding of how data is passed down, let's also replace our static list of chirps with mock data!
Let's structure the model. We'll say that a chirp contains: a text, a user (the author) and the date it was posted.
Here we stumble on a new kind of attribute: we've added a one-to-one relationship linking a chirp with its user (the author) through DS.belongsTo('user')
. The only data we need then is the id of the user (which in this case is 1
).
This is one of those awesome things with Ember Data; it not only stores your models but also remembers how they're linked together. We're going to take advantage of that in the upcoming chapters.
Let's not forget to add our mock data, so let's create a chirps.js
-file in the fixtures-folder:
Since we want to fetch all these chirps, we'll add the following line to the app/mirage/config.js
-file (right after the one that fetches the user):
Now that we have the structure and data of our chirps ready, let's follow the same process as we did for the user data:
Fetch the data via the `home`-route
Pass the data down to the component that renders the list
Render the data by doing some small edits to the Handlebars-template
If you feel adventurous, you could try passing down the data and render it on your own and then come back to this lesson to see how you did!
Alright, let's get to it. We'll start by fetching the data in the same route (home
) as the one we used before:
Next, we pass the data down to our chirps-list
-component under the key chirps
.
Then, we loop over the data one record at a time using each as
(if you are a Ruby developer you'll probably recognize this syntax). This allows us to get a separate chirp
-variable for every record.
The data of each chirp
-record is then passed down one step further to the chirps-list/chirp-message
-component: that's where we'll actually render the data!
Notice that we have an else
-statement in our each
. This is a nifty Handlebars-feature: the else
-part will only be rendered if the list we're looping over is empty!
Now there's only the easy part left: replace the static data in the chirps-list/chirp-message
-component with the data we're passing down:
Notice how good it is that we defined that one-to-one relationship earlier?
Instead of passing down two separate data-sets of a chirp and a user, we only need to pass down the chirp object. We can still access the username
-attribute from the user model by typing chirp.user.username
. Super simple!
Great! There are just two noticeable things to fix:
That raw timestamp is ugly, the time should be relative (e.g. "5 minutes ago"). Don't worry, this will be fixed in an upcoming chapter with an external library.
The order is wrong! If we want to simulate the way Twitter works, it should show the latest chirp ("Hello again!") before the first one ("Hello world!"). Let's fix that now!
In order to fix the order, we're going to create a computed property called sortedChirps
(right after the classNames
) which will sort out the array in the chirps-list
-component.
Now we can just edit our template so that it uses our new computed array instead of the old one:
This is how you fetch and render your records with Ember Data. In the upcoming chapters we will also show you how to add, remove and edit records!
Our Home-page is starting to get a little lonely, so in the next chapter we'll put together everything we have learned so far by creating the profile page!
Comments
You need a Ludu account in order to ask the instructor a question or post a comment.
<p class="p1">I'm getting a "Mirage: The route handler for /api/users/1 is requesting data from the users collection, but that collection doesn't exist. To create it, create an empty fixture file or factory." despite having this in place:
cat app/mirage/fixtures/users.js </p> <p class="p1">// app/mirage/fixtures/users.js</p> <p class="p2">
</p> <p class="p1">export default [</p> <p class="p1"> {</p> <p class="p1"> id: 1, </p> <p class="p1"> username: 't4t5', // Put your username here</p> <p class="p1"> numberOfChirps: 2,</p> <p class="p1"> numberOfFollowing: 5,</p> <p class="p1"> numberOfFollowers: 5</p> <p class="p1"> }</p> <p class="p1">];</p>
Needs JSON API fixture: Error while processing route: home Assertion Failed: normalizeResponse must return a valid JSON API document:
Sorry, forgot to remove mirage/scenarios/ folder! Might want to emphasize PS how do I change my icon? it didn't grab my FB one...
The data is being logged but I'm still getting an error...Assertion Failed: normalizeResponse must return a valid JSON API document:
@michael: Hey there! Are you sure that you changed your
adapter.js
file so that it uses DS.RESTAdapter instead of the JSONAPIAdapter?I fixed this by following this guide: http://www.ember-cli-mirage.com/docs/v0.1.x/working-with-json-api/ and changing the mock data attributes to use dashes instead of camalcase, IE "number-of-chirps" instead of "numberOfChirps"
Shouldn't it be
mirage/config.js
as it's not inapp
?Yep it's
mirage/config.js
nowadays:Can you explain the pipe syntax?
If I use:
{{#each chirps as chirp}} it doesn't work...
I have an extra chirp with all fields blank... :/
You can teach how to create this model, like this: ember g model user username:string numberOfChirps:number numberOfFollowing:number numberOfFollowers:number
+1
+1
Installing ember-cli-mirage@0.1.13 failed on my machine with SyntaxError: Invalid regular expression: /"predef": [ /: Unterminated character class
A fix can be found here: https://github.com/ember-cli/ember-cli/commit/b64ffaaf913bf25ffa7f56c4c14de24bb2081586
set up
{{chirp.user.username}} is not working for me, all I have access to is: chirp.user which is "1"
From what I noted on mirage page, they are recommending of using factories over fixtures - 0.2.x (yeah I know it still in beta)
I've prepared factory
ember g factory user
and I also need scenario to preload app with some users data - why did you recommend to delete scenarios?Thanks for your reply.
I'm doing RoR development since 2007 - we have already been there, and factories in long run it didn't work for us. This might be the reason why I'm trying to avoid them (although front end apps have it's own rules, so not everything can be map one to one).
@michaczy: Scenarios aren't needed if you use fixtures. Personally, I've found fixtures easier to deal with, but I would say that if you want to mock, say, 100s of data rows, you're better off using factories. It comes down to what the best choice for your particular app is.
I's very helpful to generate it at once
ember g model chirp text:string user:belongsTo createdAt:date
Good tip
Can you point me to the documentation of creating an adapter this way?
@louiesolomon: Check the Ember-CLI website: http://ember-cli.com/extending/#detailed-list-of-blueprints-and-their-use :)
If you get the error
Unknown blueprint: adaptor
check your spelling of adapter. Uses 'e' instead or 'o.'I'm getting model data in the browser console but its not being displayed on the page. my syntax looks correct and I restarted the Ember server. Any ideas?
@chrisr: Hm, not sure what the error could be tbh. If the problem persists and you can't find a solution, you could upload your code to GitHub and I'll try to find what the problem is.
@tristan: I'm not sure what the problem was but I noticed my npms were out of date. I updated everything and started over and now its working.
Thanks for updating to this in this brave new post-fixture world
The build failed after installing ember-cli-mirage because it could not find the config file in the mirage folder it expected. I think this has to do with version differences since this course was published. I finally had to delete any references to it. Is there any update to this command for current Ember versions?
Do you have to be careful of lazy-loading issues and accidentally downloading the universe?
Same question..
@kirkisrael: @manvendrasingh: Yes, if you have many relationships, you want to make sure you don't fire hundreds of unwanted AJAX requests. This is why there's the concept of sideloading in Ember data (which is mentioned in a later chapter).
I think this is actually a many-to-one relationship of "many chirps to 1 user".
I understand your point of view based on the relational databases model, but in Ember theory this is indeed declared as a one-to-one relationship.
http://guides.emberjs.com/v2.0.0/models/defining-models/#toc_defining-relationships
There is also an example of one-to-many relationship, that it can be also seen as a many-to-one, if you look at it from the other end of the relationship. :-)
I would also say that it is a many-to-one relationship because a user can have many comments, but a comment will only belong to one user.
If a comment could belong to more than one user, I'd consider it a many-to-many.
In the documentation you linked to it says that one-to-one has two belongsTo relationships, whereas a one-to-many has a belongsTo and a hasMany.
If you look in the next chapter where the user model is updated there is this line
chirps: DS.hasMany('chirp', { async: true })
So that fits the criteria for a one-to-many
If I have overlooked something, would happy to hear other peoples thoughts!
I was looking at the Ember 2.0 guides and wondering why there is nothing about how to use fixtures this way, but it exists in the 1.10 guides under the models section.
do you mean app/home/template.hbs ?
@alexmolina: Woops, yes of course I do. Fixed it now, thanks!
If this course is about ember 2.0 http://guides.emberjs.com/v2.0.0/models/
@andrsbg: You're right, thanks! Updated the link.
Hi. Can you describe the significance of { async: true } and why it is necessary here? (when using the "belongsTo" relationship)
@jstancox: Hi there! When using a REST backend, { async: true } will make a separate AJAX request in order to fetch the corresponding user, unless it has already been sideloaded. This will actually be the default behaviour in the stable Ember 2.0, so the whole async-thing can soon be removed.
ditto here
get unexpected token with 'chirps: this.store.findAll('chirp')
thks will try that
why are we using ".findAll" vs ".findRecord"? I'm confused. :(
tired other addon, still get error: yntaxError: chirper/home/route.js: Unexpected token (10:6) 8 | // user: this.store.findRecord('user', 1) 9 | user: this.store.find('user', 1) > 10 | chirps: this.store.findAll('chirp') | ^ 11 | }); 12 | } 13 | });
@lynnalexander: Oh, a syntax error? Sounds like there's a problem in your code. Could you email me at tristan@ludu.co?
@lynnalexander: Hmm. It could be that the Ember team has removed the FixtureAdapter from the preinstalled adapters. Try installing this addon: https://github.com/emberjs/ember-data-fixture-adapter I'll update the lesson soon to see what has changed!
@michaelbernal: Ah sorry, this is explained in more detail in the chapter "Consuming the REST API". "FindRecord" will fetch a single chirp (from an id), while "findAll" will fetch ALL the chirps that exist. In this case we want to display multiple chirps and not just one, so we use "findAll".
file name is wrong here should be profile-glance
@billpullen: Thanks! Fixed.
not working for me v0.2.7??
switched to latest version 1.13.8 from 0.2.5 and now working
shgshould this be store.find()
had to use store.filter() in mine v0.2.7
@billpullen: store.find is getting phased out in favor of findRecord + other methods it seems: http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html#toc_new-adapter-hooks-for-better-caching
Unnecessary semicolon at line 9