Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ember in the Real World

Ember in the Real World

If you’ve done online tutorials, courses, or followed blog posts, you probably have a decent idea of what Ember is. But how do you build real, production-grade applications in it? How do you stitch together the pieces from tutorials into a larger picture? What about all the stuff that isn’t in the tutorials?

We’ll help you sort through some of the tougher challenges beyond the guides and blog posts:

- Working with non-standard APIs
- Integrating existing jQuery components
- Authentication and security
- Testing strategies to let you sleep at night
- Refactoring toward Ember from “jQuery spaghetti”

We’ll condense 2 years of shipping production applications so you can connect the dots and put the individual pieces together in a real-world application, so you can build ambitious web applications that are fun and easy to maintain.

Presented at Fluent 2015.

tehviking

April 24, 2015
Tweet

More Decks by tehviking

Other Decks in Programming

Transcript

  1. THE HYPE CYCLE PLATEAU OF PRODUCTIVITY SLOPE OF ENLIGHTENMENT TROUGH

    OF DISILLUSIONMENT TECHNOLOGY TRIGGER PEAK OF INFLATED EXPECTATIONS VISIBILITY MATURITY
  2. “HYPE” IS NOT AN INSULT, IT’S AN INEVITABILITY PLATEAU OF

    PRODUCTIVITY SLOPE OF ENLIGHTENMENT TROUGH OF DISILLUSIONMENT TECHNOLOGY TRIGGER PEAK OF INFLATED EXPECTATIONS VISIBILITY MATURITY
  3. STUFF THAT USED TO SUCK ABOUT STARTING WITH EMBER Documentation

    Tutorials View Layer Routing Initial Setup File structure Sharing code/ plugins Build tooling Closing div tags (seriously) Authentication Testing Data layer Templating syntax Animation Deployment Integrating with existing apps Performance
  4. STUFF THAT IS GREAT ABOUT STARTING WITH EMBER Documentation Tutorials

    View Layer Routing Initial Setup File structure Sharing code/ plugins Build tooling Closing div tags (seriously) Authentication Testing Data layer Templating syntax Animation Deployment Integrating with existing apps Performance
  5. ember  generate  resource  product   installing      create  app/models/product.js

         create  tests/unit/models/product-­‐test.js      create  app/routes/products.js      create  app/templates/products.hbs      create  tests/unit/routes/products-­‐test.js
  6. Add karma, karma-cil, karma-mocha, karma-chai- plugins, karma-chrome-launcher, karma-ember- preprocessor, karma-mocha,

    karma-phantomjs- launcher, karma-junit-reporter to package.json Compile and install PhantomJS npm install Install ember-mocha-adapter and chai-jquery via Bower Set up a karma.conf file In your karma.conf: Customize your frameworks section in karma.conf to include mocha, chai, sinon-chai, and chai-jquery Set up a Grunt task to build your Sass on each test run Include your vendor dependencies in karma.conf Include your code in files section Debug file load order issues Add & configure handlebars karma preprocessor Then, create test-helper.js & include it in karma.conf In your test-helper.js: Configure chai and mocha defaults Set Ember.testing to true Set App.setupForTesting to true Call App.injectTestHelpers() Set up app on each test run Tear down app after each test run Ensure beforeEach and afterEach use the run loop Use the .done() callback for async beforeEach TEST SETUP
  7. ApplicationRoute (generated): ‘/’ ProductRoute: ‘/:id’ Rendered into products {{outlet}} URL:

    / products / 230 ProductsRoute: ‘/products’ Rendered into application {{outlet}}
  8. COMPUTED PROPERTIES app/controllers/cart.js export default Ember.Controller.extend({
 cartItemSubtotals: Ember.computed.mapBy("model.cartItems", "subtotal"),
 orderSubtotal:

    Ember.computed.sum("cartItemSubtotals"),
 shippingCost: Ember.computed(function() {
 return 0;
 }),
 orderTax: Ember.computed("orderSubtotal", function() {
 var taxCents = (this.get("orderSubtotal") * 100) * 0.0825;
 return taxCents / 100;
 }),
 orderTotal: Ember.computed("orderTax", "orderSubtotal", function() {
 return this.get("orderSubtotal") + this.get("orderTax");
 }),
 });
  9. COMPUTED MACROS app/controllers/cart.js export default Ember.Controller.extend({
 cartItemSubtotals: Ember.computed.mapBy("model.cartItems", "subtotal"),
 orderSubtotal:

    Ember.computed.sum("cartItemSubtotals"),
 shippingCost: Ember.computed(function() {
 return 0;
 }),
 orderTax: Ember.computed("orderSubtotal", function() {
 var taxCents = (this.get("orderSubtotal") * 100) * 0.0825;
 return taxCents / 100;
 }),
 orderTotal: Ember.computed("orderTax", "orderSubtotal", function() {
 return this.get("orderSubtotal") + this.get("orderTax");
 }),
 });
  10. IF YOU UNDERSTAND: • Router & Routes • Components •

    Computed Properties • Generating with Ember CLI YOU CAN BUILD AWESOME STUFF
  11. import Ember from 'ember';
 
 export default Ember.Route.extend({
 model: function()

    {
 var store = this.store;
 return Ember.$.getJSON("/user.json").then(function(response){
 Ember.run(function() {
 store.pushPayload("user", response);
 });
 return store.find("user", response.user.id);
 });
 }
 }); USING PUSHPAYLOAD app/routes/application.js
  12. ADAPTERS & SERIALIZERS Your API Object({id: 3}).save() Adapter Builds URL,

    sends XHR Serializer Generates payload & parses response
  13. export default ApplicationAdapter.extend({
 buildURL: function(type, id, record) {
 var url

    = this._super(type, id, record); // MODIFY URL HERE :) return url;
 }
 }); USE buildURL TO MODIFY THE API ENDPOINT URL
  14. export default ApplicationAdapter.extend({
 buildURL: function(type, id, record) {
 var url

    = this._super("item", id, record); var modifiedURL = "/cart" + url; return url;
 }
 }); OVERRIDE BUILDURL type is “cartItem” in the model… app/adapters/cart-item.js
  15. export default ApplicationAdapter.extend({
 buildURL: function(type, id, record) {
 var url

    = this._super("item", id, record); var modifiedURL = "/cart" + url; return url;
 }
 }); OVERRIDE BUILDURL app/adapters/cart-item.js …so we’ll replace that with “item” for our API
  16. export default ApplicationAdapter.extend({
 buildURL: function(type, id, record) {
 var url

    = this._super("item", id, record); var modifiedURL = "/cart" + url; return url;
 }
 }); OVERRIDE BUILDURL app/adapters/cart-item.js then prefix it with “cart” so it’s “/cart/items”
  17. PROBLEM: NON-COMPLIANT DATA PAYLOAD Started  POST  "/cart/items"  for  127.0.0.1  at

     2015-­‐04-­‐15  14:53:50  -­‐0500   Processing  by  CartItemsController#create  as  JSON      Parameters:  {“cart_item"=>{"product_id"=>"1",  "cart_id"=>"1"}}      User  Load  (2.0ms)    SELECT    "users".*  FROM  "users"    WHERE  "users"."id"  =  1     ORDER  BY  "users"."id"  ASC  LIMIT  1      Cart  Load  (1.0ms)    SELECT    "cart".*  FROM  "carts"    WHERE  “carts”."user_id"  =  1   LIMIT  1   Completed  500  Internal  Server  Error  in  50ms  (ActiveRecord:  3.0ms)   ActionController::UnpermittedParameters  (found  unpermitted  parameters:  cart_id):      app/controllers/cart_items_controller.rb:27:in  `create_params'      app/controllers/cart_items_controller.rb:5:in  `create'
  18. export default ApplicationSerializer.extend({
 serialize: function(record, options) {
 var json =

    this._super(record, options); // MODIFY JSON HERE :) return json;
 }
 }); USE serialize() TO MODIFY THE DATA TO THE SERVER
  19. export default ApplicationSerializer.extend({
 normalize: function(type, hash, prop) { // MODIFY

    HASH HERE :) return this._super(type, hash, prop);
 }
 }); USE normalize() TO MODIFY THE DATA FROM THE SERVER
  20. export default ApplicationSerializer.extend({
 serialize: function(cartItem, options) {
 var json =

    this._super(cartItem, options);
 delete json.cart_id;
 return json;
 }
 });
 OVERRIDE SERIALIZE app/serializers/cart-item.js override the method
  21. export default ApplicationSerializer.extend({
 serialize: function(cartItem, options) {
 var json =

    this._super(cartItem, options);
 delete json.cart_id;
 return json;
 }
 });
 OVERRIDE SERIALIZE app/serializers/cart-item.js assign super() call to var
  22. export default ApplicationSerializer.extend({
 serialize: function(cartItem, options) {
 var json =

    this._super(cartItem, options);
 delete json.cart_id;
 return json;
 }
 });
 OVERRIDE SERIALIZE app/serializers/cart-item.js modify & return modified data
  23. signIn: function(email, password) {
 Ember.$.post("/users/sign_in.json", {
 user: {
 email: email,


    password: password
 }
 }, "json").then(function(response) {
 this.store.pushPayload("user", response);
 var user = this.store.getById("user", response.user.id);
 this.send("signedIn", user);
 this.send("closeModal");
 }.bind(this));
 }, ROLL YOUR OWN app/routes/application.js
  24. signOut: function() {
 Ember.$.ajax("/users/sign_out.json", {
 method: "post",
 dataType: "json",
 data:

    {
 _method: "DELETE"
 }
 }).then(function(response) {
 this.store.pushPayload("user", response);
 var user = this.store.getById("user", response.user.id);
 this.set("controller.model", user);
 this.send("closeModal");
 }.bind(this));
 } ROLL YOUR OWN app/routes/application.js
  25. import Ember from 'ember';
 
 export default Ember.Route.extend({
 model: function()

    {
 return store.find(“post”);
 }
 }); LESS SECURE
  26. import Ember from 'ember';
 
 export default Ember.Route.extend({ model: function()

    {
 return this.currentUser.get(“posts”);
 }
 }); MORE SECURE
  27. ADAPTING TO jQUERY APIS lib-wrapper-component jquery-lib.js $.on(“lib:selected”) $.initFunction() didInsertElement sendAction

    “select” DATA IN DATA OUT “selectItem” action sent up the chain {{x-foo item=bar}} select=“selectItem” this.get(“bar”)
  28. import Ember from 'ember';
 
 export default Ember.Component.extend({
 didInsertElement: function()

    { this._super.apply(this, arguments);
 this.$('input').typeahead({}, {
 displayKey: this.get('displayKey'),
 source: this.get('source')
 });
 }
 });
 DATA IN: PASSING PROPERTIES TO jQUERY ON INIT app/components/twitter-typeahead.js Pass displayKey and source in second arg object
  29. export default Ember.Component.extend({
 didInsertElement: function() {
 this._super.apply(this, arguments); this.typeahead =

    this.$('input').typeahead({}, {
 displayKey: this.get('displayKey'),
 source: this.get('source')
 }); var _this = this;
 this.typeahead.on('typeahead:selected', function(e, suggestion) {
 Ember.run(function() {
 _this.sendAction(“on-select", suggestion);
 });
 });
 }
 });
 Listen on custom DOM event & send Ember action DATA OUT: LISTENING TO CUSTOM EVENTS & FIRING ACTIONS app/components/twitter-typeahead.js
  30. Bind the controller action to the component’s action INCLUDE THE

    COMPONENT app/templates/checkout/address.hbs {{twitter-­‐typeahead  source=getStates  displayKey=formatState  on-­‐select=“selectState”}}
  31. QUNIT module('Acceptance: ListProducts', { setup: function() { App = startApp();

    this.server = createServer(); }, teardown: function() { Ember.run(App, 'destroy'); this.server.shutdown(); } }); test("shows all products", function() { visit("/products"); andThen(function() { equal($(".spec-product-item").length, 3); }); });
  32. describe('Starting the app', function() {
 beforeEach(function() {
 App = startApp();


    this.server = createServer();
 });
 
 afterEach(function() {
 Ember.run(App, 'destroy');
 this.server.shutdown();
 });
 
 describe('visiting /products', function() {
 beforeEach(function() {
 visit('/products');
 });
 it('shows all products', function() {
 expect($(".spec-product-item").length).to.equal(3);
 });
 });
 });
 MOCHA
  33. EMBER ACCEPTANCE TEST HELPERS visit click fillIn keyEvent triggerEvent find

    currentPath currentURL currentRouteName andThen …or register your own
  34. Mocha lends itself to acceptance style, qUnit to unit style.

    Bottom line: It’s personal preference.
  35. import Pretender from 'pretender';
 import Ember from 'ember';
 
 export

    function createServer() {
 return new Pretender(function() {
 this.get("/user.json", function(req) {
 return [200, {"Content-Type": "application/json"}, JSON.stringify(userFixture())];
 });
 this.get("/products", function(req) {
 return [200, {"Content-Type": "application/json"}, JSON.stringify(productsFixture())];
 }); ...
 });
 }
 MOCK SERVER tests/fixtures.js
  36. export function productsFixture(id) {
 var productFixtures = {
 "1": {


    "id":1,
 “name":"Chicago Transit Authority",
 "artist":"Chicago",
 "amazon_url":"http://www.amazon.com/Chicago-Transit-Authority-CHICAGO"
 }, ...
 };
 
 if(arguments.length) {
 return productFixtures[id];
 } else {
 var productsArray = Object.keys(productFixtures).map(function(k){return productFixtures[k];});
 return {products: productssArray};
 }
 } FIXTURE DATA tests/fixtures.js
  37. import Ember from 'ember';
 ...
 import startApp from 'adv-ember-training/tests/helpers/start-app';
 import

    { createServer } from '../fixtures'; IMPORT CREATESERVER tests/acceptance/list-products-test.js
  38. module('Acceptance: ListProducts', {
 setup: function() {
 App = startApp();
 //

    Assign this.server to createServer()
 this.server = createServer();
 },
 SET UP PRETENDER tests/acceptance/list-products-test.js
  39. teardown: function() {
 Ember.run(App, 'destroy');
 // Tear down by calling

    .shutdown() on this.server
 this.server.shutdown();
 }
 TEAR DOWN PRETENDER tests/acceptance/list-products-test.js
  40. test("adding to cart", function(assert) {
 // Visit the /cart route


    visit('/cart');
 
 andThen(function() {
 // Use the click helper to click ".spec-add-item:first"
 click(".spec-add-item:first");
 });
 
 andThen(function() {
 // assert that $(“.spec-playlist-item).length is now 4.
 assert.equal($(".spec-cart-item").length, 4);
 });
 }); WRITE ADD ITEM SPEC tests/acceptance/add-products-to-cart—test.js
  41. this.transition(
 this.fromRoute('products.index'),
 this.toRoute('product'),
 this.use('explode', {
 matchBy: 'data-product-id',
 use: ['flyTo', {duration:

    300}]
 }, {
 use: ['toLeft', {duration: 300}]
 }),
 this.reverse('explode', {
 matchBy: 'data-product-id',
 use: ['flyTo', {duration: 300}]
 }, {
 use: ['toRight', {duration: 300}]
 })
 ); THIS IS THE CODE app/transitions.js
  42. THE EMOTIONAL ROLLER COASTER OF YOUR FIRST PRODUCTION EMBER APP

    PLATEAU OF CONTINUOUS DELIVERY SLOPE OF MASTERY TROUGH OF “IT SHOULDN’T BE THIS TOUGH” “EMBER NEW” TRIGGER PEAK OF TUTORIAL- DRIVEN DEVELOPMENT ENTHUSIASM PROJECT DURATION
  43. IMAGE CREDITS Meditate by Nadir Hashmi https://www.flickr.com/photos/nadircruise/ Sandwich by Adam

    Sherer https://www.flickr.com/photos/arsherer/ The Weight of Thought by Evan Leeson https://www.flickr.com/photos/ecstaticist/