Giter Club home page Giter Club logo

ember-resources's Introduction

General Assembly Logo

Ember Resources

One of the chief advantages of having a front-end framework is being able to store and manipulate data entirely on the front-end, without needing to explicitly make AJAX requests. This is accomplished through a data layer, which for Ember is a library called ember-data. In this session, we'll look at how to use ember-data to set up front-end models and perform CRUD actions on them.

Prerequisites

By now, you have already learned how to:

  • Create nested view states and route to them appropriately.
  • Set up resource routes.
  • Model the user interface using components.
  • Represent visual hierarchies with nested components.
  • Register actions and click handlers on component objects.
  • Pass data from routes to components, and from components to components.

Objectives

By the end of this session, you should be able to:

  • Generate a Model to represent a resource on the front-end.
  • Extend an Adapter to connect your Model(s) to an API.
  • Make Models accessible in templates by loading them through Routes.
  • Create CRUD actions on a Route, and trigger them from Components.
  • Add behavior to Route actions to perform CRUD on the Route's model.

Setup

  1. Fork and clone this repo.
  2. Run npm install and bower install.
  3. Clone listr-api into a subdirectory of ~/wdi/tmp and follow the instructions to setup the API.
  4. Start the api with bin/rails server
  5. Start the client with ember server

ember-data and CRUD

In the past few days, you've seen a whole lot of Ember's 'view' layer - the system governing how Ember responds to user behavior and controls what HTML gets rendered in the browser.

While this is all very nice, it's meaningless without an API. That's where Ember's 'data' layer comes in, as implemented by an add-on library to Ember called ember-data.

ember-data provides several Ember Classes for handling the exchange of information between the client and the API, most notably Models (which represent back-end resources as Ember Objects) and Adapters (which manage the actual interactions with the underlying API(s)).

Refactor ListR

We'll start with the solution from ember-components.

Move ListR to the lists route

  1. Generate a lists route
  2. Move ListR specifics from index route to lists route
  3. Link to lists route from index route
ember generate route lists

Get and display list data from the listr-api

  1. Generate a list model (for now, we'll leave items off)
  2. Generate a shopping-list/card component as the top-level interface to lists
  3. Copy the list title display to shopping-list/card (without any action)
  4. Refactor the lists route template to use shopping-list/card
  5. Refactor the lists route model method to use the ActiveModelAdapter
ember generate model list title:string hidden:boolean

This will create a new model.js file inside app/list. The README for the API shows us the data we can expect at GET /lists. Note that the items returned are just ids. We specified the properties that we want the ember-data model to have. We could all of the properties from the API, but we're leaving off items because we haven't created and item model, yet.

DS.attr is how we define attributes for our models. The default types are 'number', 'string', 'boolean', and 'date', but we can define our own if we really need to. We can also use DS.attr to specify a default value for a given attribute by passing in an optional object as a second argument.

As we saw in the material on routing, each Route has a model method that exposes data to the template. Each Route also has a store property which refers to whatever data store our application is using (in this case, ember-data), so to make the List model available in the lists route, we reference the store and query it for all instances.

export default Ember.Route.extend({
  model () {
    return this.get('store').findAll('list');
  }
});

Get and display item data

  1. Generate an item model
  2. Add a hasMany to the list model
  3. Generate a route for a single list
  4. Update app/router.js for the single list route
  5. Add the model method to the list route
  6. Invoke the shopping-list component from the list route template
  7. Link to the list route from the shopping-list/card template
ember generate model item content:string done:boolean list:belongs-to:list
ember generate route list
 export default DS.Model.extend({
   title: DS.attr('string'),
   hidden: DS.attr('boolean'),
+  items: DS.hasMany('item'),
 });
 Router.map(function () {
   this.route('lists');
-  this.route('list');
+  this.route('list', { path: '/lists/:list_id' });
 });
 export default Ember.Route.extend({
+  model (params) {
+    return this.get('store').findRecord('list', params.list_id);
+  },
 });

Now that we've refactored ListR to use data from the API, we'll move on to persisting changes.

Ember CRUD - Data Down, Actions Up (DDAU)

Now that we have models loaded in our Routes, it's finally time to tie all of this together.

Before talking about CRUD, though, we should start by talking about something you touched on in the material on Components: 'actions'. 'Actions' are a special class of trigger-able events that are handled by the Ember.ActionHandler Ember Class. Like normal events, actions 'bubble up', moving from the leaf (i.e. Template) to the root (i.e. the 'application' Route) until they are met by a matching handler.

In Ember 1, action handlers inside the Controller were used to perform CRUD on the model. This made sense, since the Controller was responsible for managing all of the business data in our application, and since it mirrored how responsibilities were broken out in Rails. An action could be triggered in a Template and bubble up to a Controller, where it would cause that Controller to manipulate the given Model.

However, with the shift towards Components in Ember 2, a lot of the functionality of Controllers has been made redundant and moved into other entities within the app. In this case, Components and Routes both incorporate Ember.ActionHandler, so we can instead set our action handlers there. For simplicity's sake, we'll put all handlers related to Model CRUD into the Route; any other action handlers can be placed in either place.

Defining Action handlers in a Route is very easy. Simply open up the route.js file and make the following addition:

import Ember from 'ember';

export default Ember.Route.extend({
  model: function(...){
    ...
  },
  actions: {
    create () { ... },
    update () { ... },
    destroy () { ... },
    // ... etc
  }
});

To trigger an action, you can add an {{action ... }} helper to an element (usually a button) - this will cause that element to launch the action whenever it executes its defaults behavior (in the case of a button, being clicked).

In Ember applications that use Components (which will soon be all of them) the generally recommended strategy is to follow a 'data down, actions up' design pattern, which essentially means two things:

  1. All Components look to their parent element as a source of data to bind to; as a result, data changes propagate 'downwards' from parent to child.
  2. Implicit in the first point is that all changes to data take place in the parent. In order to effect changes to the data in a parent element, Components trigger their parents' actions; in this fashion, action invocations propagate 'upwards' from child to parent.

Persist item changes to the API

  1. In the shopping-list/item component
    1. Make listItemCompleted a computed property alias of the item component
    2. Change toggleDone to send that action up
  2. In the shopping-list component
    1. Add toggleDone='toggleItemDone' to invoking shopping-list/item
    2. Add the toggleItemDone action handler to send the action up
  3. In the list route
    1. Add toggleItemDone='toggleItemDone' to invoking shopping-list
    2. Add the toggleItemDone action to the route
 export default Ember.Component.extend({
   tagName: 'li',
   classNameBindings: ['listItemCompleted'],
-  listItemCompleted: false,
+  listItemCompleted: Ember.computed.alias('item.done'),
   actions: {
     toggleDone () {
-      return this.toggleProperty('listItemCompleted');
+      return this.sendAction('toggleDone', this.get('item'));
     },
   },
 });
   classNameBindings: ['listDetailHidden'],
   listDetailHidden: false,
   actions: {
+    toggleItemDone (item) {
+      return this.sendAction('toggleItemDone', item);
+    },
+
   toggleListDetail () {
     return this.toggleProperty('listDetailHidden');
   },
   model (params) {
     return this.get('store').findRecord('list', params.list_id);
   },
+
+  actions: {
+    toggleItemDone (item) {
+      item.toggleProperty('done');
+      item.save();
+    },
+  },
 });

Lab: Delete items on the API

  1. In the shopping-list/item component
    1. Add a button with text "Delete" and {{action 'delete'}}
    2. Add a delete action to send that action up
  2. In the shopping-list component
    1. Add delete='deleteItem' to invoking shopping-list/item
    2. Add the deleteItem action to send the action up
  3. In the list route
    1. Add deleteItem='deleteItem' to invoking shopping-list
    2. Add the deleteItem action to the route

Create items on the API

  1. In the shopping-list component
    1. Add a form after each with {{action "createItem" on="submit"}}
    2. Add an input to the form with value=newItem.content
    3. Add a newItem property
    4. Add the createItem action to send the action up
  2. In the list route
    1. Add createItem='createItem' to invoking shopping-list
    2. Add the createItem action to the route

Does it work?

Unfortunately, no. The API uses a nested route for creating new list items. This doesn't fit directly with ember-data's modeling of APIs, so we have to do some extra work.

We'll extend the default application adapter, included in ember-template to handle this case.

Additional Resources

  1. All content is licensed under a CC­BY­NC­SA 4.0 license.
  2. All software code is licensed under GNU GPLv3. For commercial use or alternative licensing, please contact [email protected].

ember-resources's People

Contributors

ember-tomster avatar ga-meb avatar payne-chris-r avatar raq929 avatar

Watchers

 avatar  avatar

ember-resources's Issues

Front end data caches with browser back and forward buttons

When you go to the 'create lists' and click back in browser, and then click forward, and then click back, etc - the delete and edit buttons are created for each instance. They don't persist to the backend and on refresh they go away. I thought of the 'transitionTo' but since the browser nav buttons aren't controlled by you you can't attach methods to them(you probably could but..eww) You could certainly 'refresh' the page on back somehow, but doesn't that defeat the purpose of Ember and Ember's data caching to speed things up? I didn't attach any code because it is browser related - the code is all fine. Googling the issue seems to suggest that this is kind of a common problem, but I didn't see any real solutions. I think the problem with hardwiring anything in would be the side effects of 'page jumping' - back and forward in the browser may not follow what I as a developer expect the user to navigate to. Any thoughts on fixes for this?

Remove Mirage

It ended up being a headache last time. Just take it out and use a real api instead.

Deprecation Issue

Jeff mentioned the browser is giving him an deprecation error. I'm trying to catch up to see it myself...

Typo

Add the toggleItemDone action to send the action up
Add the toggleItemDone action handler to send the action up

Edit instructions for Mirage setup.

Tell students to not overwrite the provided config.js file and to delete the scenarios directory.
(Is there a way to do that from the console?)

Error after deleting a Pokemon

Maybe trying to reload the Pokemon after it's been destroyed?

Error: Attempted to handle event `notFound` on <ember-repo-template@model:pokemon::ember474:1> while in state root.deleted.saved. 
    at new Error (native)
    at Error.EmberError (http://localhost:4200/assets/vendor.js:25269:21)
    at ember$data$lib$system$model$internal$model$$InternalModel._unhandledEvent (http://localhost:4200/assets/vendor.js:126790:15)
    at ember$data$lib$system$model$internal$model$$InternalModel.send (http://localhost:4200/assets/vendor.js:126668:16)
    at ember$data$lib$system$model$internal$model$$InternalModel.notFound (http://localhost:4200/assets/vendor.js:126578:14)
    at http://localhost:4200/assets/vendor.js:123331:23
    at tryCatch (http://localhost:4200/assets/vendor.js:61667:14)
    at invokeCallback (http://localhost:4200/assets/vendor.js:61682:15)
    at publish (http://localhost:4200/assets/vendor.js:61650:9)
    at publishRejection (http://localhost:4200/assets/vendor.js:61585:5)onerrorDefault @ ember.debug.js:30877exports.default.trigger @ ember.debug.js:52928(anonymous function) @ ember.debug.js:54177Queue.invoke @ ember.debug.js:320Queue.flush @ ember.debug.js:384DeferredActionQueues.flush @ ember.debug.js:185Backburner.end @ ember.debug.js:563Backburner.run @ ember.debug.js:685run @ ember.debug.js:20105hash.error @ rest-adapter.js:761fire @ jquery.js:3099self.fireWith @ jquery.js:3211done @ jquery.js:8266(anonymous function) @ jquery.js:8605(anonymous function) @ fake_xml_http_request.js:137dispatchEvent @ fake_xml_http_request.js:181_readyStateChange @ fake_xml_http_request.js:371_setResponseBody @ fake_xml_http_request.js:431respond @ fake_xml_http_request.js:452(anonymous function) @ pretender.js:303resolve @ pretender.js:340(anonymous function) @ pretender.js:331

3/22/17 "Events bubble up" phrasing is confusing

My source of confusion for this point that we covered in class today is that the repo says that events bubble up in Ember. As a result, I went straight to the route and attempted to handle the event. I was under the impression that since "events bubble up" that the components were somehow linked and that the app knew the structure of the tree enough to send the event through the various layers to the route at the top. From my questions and Jeff's explanation, I learned that this is not true.

What I think is a better explanation is that events MUST bubble up in order to update the API, but WE must control the bubbling every step of the way. Events do not just bubble up by themselves. Maybe the phrasing should be changed to distance it somehow from the bubbling that we know from html and click events. I don't know if the way its worded right now is a better technical description but based on what we have learned in the class so far the two kind of conflict and were the source of confusion for me.

Change computed property item verbiage

IS:

Persist item changes to the API

In the listr-list/item component
Make listItemCompleted a computed property alias of the item component

TO:

Persist item changes to the API

In the listr-list/item component
Make listItemCompleted a computed property alias of the bound item

Add back "content-editable" instructions.

Took it out of the lesson for time, but I ended up covering it anyway and it didn't actually take that long.

{{#unless isEditable}}
  <h4>#{{pokemon.nationalPokeNum}} : {{pokemon.name}}</h4>
  <p> Generation: {{pokemon.generation}} </p>
  <p>
    Type(s): {{pokemon.typeOne}}
    {{#if twoTypes}}
      / {{pokemon.typeTwo}}
    {{/if}}
  </p>
{{else}}
  <h4>#{{input value=pokemon.nationalPokeNum}} : {{input value=pokemon.name}}</h4>
  <p> Generation: {{input value=pokemon.generation}} </p>
  <p>
    Type(s): {{input value=pokemon.typeOne}}
    {{#if twoTypes}}
      / {{input value=pokemon.typeTwo}}
    {{/if}}
  </p>
{{/unless}}
<button {{action 'updatePokemon'}}>EDIT</button>
<button {{action 'destroyPokemon'}}>DESTROY</button>

typo

IS:
Make listItemCompleted a computed property alias in the item component
SHOULD BE:
Make listItemCompleted a computed property alias of the item component

Consider splitting the `createRecord` and `save` lines in the Route's `createPokemon` action.`

This would emphasize that .save() does not return the record you want, so if you want to do something else to that record, you have to do it before you call save(). Additionally, since that's exactly what we need to do when we get to CRUD with associations, it might be a good idea to set it up that way in advance.

Maybe also add a route transition to the end; that would (a) demonstrate that save() returns a promise (not your record), and (b) reset the form for a new Pokemon.
e.g.

export default Ember.Route.extend({
  model: function(){
    return {
      pokemon: this.store.findAll('pokemon'),
      generations: this.store.findAll('generation'),
      regions: this.store.findAll('region'),
      types: this.store.findAll('type')
    };
  },
  actions: {
    createPokemon: function(newPokemonData, generationNum){
      var self = this;
      var pokemon = this.store.createRecord('pokemon',newPokemonData);
      pokemon.save().then(function(){
        self.transitionToRoute('pokemon.new');
      });
    },
    destroyPokemon: function(pokemon){
      this.store.findRecord('pokemon', pokemon.get('id')).then(function(pokemonRecord){
        pokemonRecord.destroyRecord();
        console.log('record destroyed');
      });
    }
  }
});

`valueBinding` is deprecated

See deprecation messages below. It still works, for now, and correcting it should be a simple fix.

DEPRECATION: You're using legacy binding syntax: valueBinding="item.name" ('ember-repo-template/components/item-row/template.hbs' @ L6:C14) . Please replace with value=item.name [deprecation id: ember-template-compiler.transform-old-binding-syntax]
DEPRECATION: You're using legacy binding syntax: valueBinding="newItem.effect" ('ember-repo-template/components/new-item-form/template.hbs' @ L5:C19) . Please replace with value=newItem.effect [deprecation id: ember-template-compiler.transform-old-binding-syntax]
DEPRECATION: You're using legacy binding syntax: valueBinding="view.form.nationalPokeNum" ('ember-repo-template/pokemon/new/template.hbs' @ L3:C8) . Please replace with value=view.form.nationalPokeNum [deprecation id: ember-template-compiler.transform-old-binding-syntax]

Don't use nested routes?

This requires an update to the listr-api

Jeff urges showing the error and refactoring.

This technique may be necessary for using third-party APIs

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.