Now that we have discussed different aspects of the ember-data library, let's look at a real example for the same.
The source code for the example can be found at https://github.com/suchitpuri/emberjs-essentials/tree/master/chapter-6/example1. In this example, we are aiming to continue with the books model that was defined in the chapter, and work on the following functionality:
Here, to communicate with the backend server, we have used the HTTP-mock generators provided by Ember CLI to generate mock responses. The HTTP-mock generator generates the Express.js (found at http://expressjs.com/)server scaffold for the resource. If you want to generate a REST compliant URL for your resource, you can do that by running the following command at your project's root directory. This is chapter-6/example1
in our case:
ember g http-mock <resource name>
For the current example, we have already generated the HTTP mock calls for our resources. You can see the mock calls at chapter-6/example1/server/mocks
directory.
This is how the mock calls for the book resource look:
module.exports = function(app) { var express = require('express'), var booksRouter = express.Router(); booksRouter.get('/', function(req, res) { res.send({ "books": [{ "id":1, "title": "Ember.js Essentials", "isbn": "ISBN1", "pages": 180, "description": "Ember.js essentials to master", "authors": [1], "publisher": 1, "reviews": [1,2,3] }, { "id":2, "title": "Some Other Book On Ember.js", "isbn": "ISBN2", "pages": 200, "description": "Some Description", "authors": [2], "publisher": 1, "reviews": [4,5,6] }] }); }); booksRouter.post('/', function(req, res) { res.status(201); res.send({"book":{ "id":Math.floor(Math.random()*1000) }}); }); booksRouter.get('/:id', function(req, res) { var reviews,authors, publisher; if(req.params.id == 1){ reviews = [1,2,3] authors = [1] publisher = 1 }else if(req.params.id == 2){ reviews = [4,5,6] authors = [2] publisher = 2 } res.send({ "book": { "id":req.params.id, "title": "Ember.js Essentials", "isbn": "ISBN1", "pages": 180, "description": "Ember.js essentials to master", "authors": authors, "publisher": publisher, "reviews": reviews } }); }); booksRouter.put('/:id', function(req, res) { res.send({ "book": { "id": req.params.id } }); }); booksRouter.delete('/:id', function(req, res) { res.status(204).end(); }); app.use('/api/books', booksRouter); };
The mock calls for the book resource is present at chapter-6/example1/server/mocks/books.js
A full explanation of the above Express.js code is beyond the scope of the book, but as this is just JavaScript code, it is not that difficult to understand. The only thing to note here is that we have mapped different HTTP verbs, with different JSON responses here.
Similar to the above code, we have created mocked responses for authors
, publishers
, and reviews
too.
Let us now see the Ember application code that is present at chapter-6/example1/app/
directory.
The adapter's directory contains the application adapter in application.js
as follows:
import DS from 'ember-data'; export default DS.RESTAdapter.extend({ namespace: 'api' });
The application adapter is present at chapter-6/example1/app/adapters/application.js
Here, we just define the namespace of our server API.
All the model classes are present at chapter-6/example1/app/models/
:
import DS from "ember-data"; export default DS.Model.extend({ title: DS.attr('string'), isbn: DS.attr('string'), pages: DS.attr('number'), description: DS.attr('string'), authors: DS.hasMany('author',{ async: true }), publisher: DS.belongsTo('publisher',{ async: true }), reviews: DS.hasMany("review",{ async: true }) });
The book model is present at chapter-6/example1/app/models/book.js
Let us see the routes that we have defined in our application:
Router.map(function() { this.resource('books', function(){ this.route('book',{path: "/:id"}); this.route('new'), }); });
The router.js is present at chapter-6/example1/app/router.js
As you can see, we have defined a books resource, which has two additional routes: books.book
and books.new
.
The books.book
route should show the information for a specific book and hence we should ask the store about the book with the matching ID to be returned from its model hook:
import Ember from 'ember'; export default Ember.Route.extend({ model: function(params){ return this.store.find('book',params.id); } });
The books.book route is present at chapter-6/example1/app/routes/books/book.js
Similarly, in our books index route, we would want to return all the books in our collection. So, we should return an array of books from its model hook:
import Ember from 'ember'; export default Ember.Route.extend({ model: function(){ return this.store.find('book'), } });
The books.index route is present at chapter-6/example1/app/routes/books/index.js
To create a new book, we need to define the books.new
template and route. The template for creating a new book is a collection for different input fields to collect data for book, publisher, and author. It also has button that calls the action, createBook
.
The template for books.new
, along with books.book
and books.index
, can be found in the chapter-6/example1/app/templates/books/
directory.
The createBook
action defined in book.new
template can either be defined in the respective controller or route. But, as we are dealing with saving a resource on the server, the books.new
route is more natural place to house the action, as follows:
import Ember from 'ember'; export default Ember.Route.extend({ actions:{ createBook: function(){ var that = this; var publisher = that.store.createRecord("publisher",{ "name": that.get("controller.name"), "organizationName": that.get("controller.organizationName"), "address": that.get("controller.address") }); var author = that.store.createRecord("author",{ "firstName": that.get("controller.firstName"), "lastName": that.get("controller.lastName"), "bio": that.get("controller.bio") }); var book = that.store.createRecord("book",{ "title": that.get("controller.title"), "isbn": that.get("controller.isbn"), "pages": that.get("controller.pages"), "description": that.get("description") }); publisher.save().then(function(publisherFromDB){ book.set("publisher",publisherFromDB); author.save().then(function(authorFromServer){ //Set The Author to the books book.get("authors").then(function(authors){ authors.pushObject(authorFromServer); }); //Save the book book.save().then(function(book){ that.transitionTo('books.book',book); }); }); }); } } });
The books.new route is defined in chapter-6/example1/app/routes/books/new.js
As you can see, we have defined one action, createBook
, in the actions object of the route. This action is triggered when someone fills from the UI, the book, authors, and publishers information, and presses the Submit
button.
Inside the createBook
action, we first create the objects for publisher, author, and the book. Then we save the publisher by calling save on the record. This would make a POST call to /api/publishers
, which would return a response that has a unique ID for the object. When the promise for the publisher.save()
resolves successfully, we associate the publisher with the book we are creating.
We do a something similar with the author, and finally, when everything returns successfully for the author resource, we save the book and transition to the book.book
route upon successful completion of the save request.
Here's how the books information page will look when you visit/books
route:
When you navigate to the /books/1
, the book details page will look like this:
And here's how the book creation form looks when you navigate to /books/new route:
18.216.47.169