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.

6fd16b1b6a307ca583526e2ec4dab52d?s=128

tehviking

April 24, 2015
Tweet

Transcript

  1. HELLO THERE.

  2. BRANDON HAYS @TEHVIKING “We do chicken right!”

  3. IT’S TIME TO STOP BEING POLITE AND START GETTING REAL.

  4. EMBER IN:

  5. YOU PROBABLY ALREADY KNOW ABOUT EMBER

  6. EMBER’S ABOUT DUE FOR ITS ANNUAL REVIEW

  7. BUT FIRST: STATE OF THE ECOSYSTEM

  8. None
  9. None
  10. WHY ALL THIS CHURN AND TURBULENCE?

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

    OF DISILLUSIONMENT TECHNOLOGY TRIGGER PEAK OF INFLATED EXPECTATIONS VISIBILITY MATURITY
  12. TECHNOLOGY TRIGGER TECHNOLOGY TRIGGER

  13. PEAK OF INFLATED EXPECTATIONS PEAK OF INFLATED EXPECTATIONS “Look at

    all the code I didn’t write!”
  14. INFLATED PROMISE: “THIS WILL MAKE YOU THE 10x DEVELOPER YOU

    ALWAYS KNEW YOU WERE”
  15. YOU ARE NOW A JEDI ROCKSTAR …RIGHT?

  16. (WE’LL TALK ABOUT THE REAL PROMISE AT THE END)

  17. TROUGH OF DISILLUSIONMENT TROUGH OF DISILLUSIONMENT

  18. “CIRCLE OF ETERNAL HOPE” CIRCLE OF ETERNAL HOPE

  19. SLOPE OF ENLIGHTENMENT SLOPE OF ENLIGHTENMENT

  20. PLATEAU OF PRODUCTIVITY PLATEAU OF PRODUCTIVITY

  21. NO ONE UPVOTES THE PLATEAU OF PRODUCTIVITY

  22. “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
  23. EMBER’S ANNUAL 360° REVIEW

  24. COMPLIMENT SANDWICH

  25. SO LET’S START WITH SOME NICE STUFF

  26. THE INITIAL DEVELOPER EXPERIENCE

  27. 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
  28. 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
  29. EMBER IS PRETTY GOOD AT TURNING WEAKNESSES INTO STRENGTHS

  30. THE MATERIALS HAVE GOTTEN… REALLY GOOD

  31. None
  32. https://leanpub.com/ember-cli-101

  33. https://teamgaslight.com/training/courses/14-introduction-to-ember-js

  34. https://teamgaslight.com/training/courses/14-introduction-to-ember-js https://www.codeschool.com/courses/warming-up-with-ember-js

  35. EMBER CLI. OMIGOSH.

  36. LET’S MAKE SOMETHING UTTERLY NECESSARY

  37. EMBER CLI GENERATORS

  38. ember  new  use-­‐lss

  39. NPM INSTALL ENTIRE-UNIVERSE

  40. 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
  41. TEST INTEGRATION

  42. 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
  43. ember  generate  acceptance-­‐test  list-­‐products   installing      create  tests/acceptance/list-­‐products-­‐test.js

    LOL J/K
  44. THE FIRST TIME I DID THIS

  45. BUILD PIPELINE

  46. HOW DEVS SEE THEIR CUSTOM BUILD PIPELINE

  47. HOW IT ACTUALLY WORKS

  48. FAST, INCREMENTAL BUILDS WITH BROCCOLI

  49. DROP-IN SUPPORT FOR SASS, STYLUS, EVEN COFFEESCRIPT

  50. ES6 TRANSPILER: MODULES & NEW ES6 SYNTAX, FOR FREE

  51. EMBER CLI ADDONS

  52. NEW, BUT IMPRESSIVE ECOSYSTEM

  53. LOOK AT ALL THE YAKS I HAVE TO SHAVE

  54. AND IT FEELS AWESOME

  55. YER A WIZARD HARRY

  56. THE EMBER WAY: FOLLOW IT, GET FREE STUFF

  57. EMBER ROUTER & NESTED ROUTES

  58. DATA, UI, STATE, URLs, IN PERFECT HARMONY

  59. ApplicationRoute (generated): ‘/’ ProductRoute: ‘/:id’ Rendered into products {{outlet}} URL:

    / products / 230 ProductsRoute: ‘/products’ Rendered into application {{outlet}}
  60. ACTION HANDLING ProductController {{action “add”}} ProductRoute ProductsRoute ApplicationRoute

  61. EMBER COMPONENTS

  62. <cc-input number={{cardNumber}} class=“cc-number {{isValid}}“> EMBER COMPONENTS OF THE NEAR FUTURE

  63. <x-select value={{bob}} action=“selectPerson”> {{each people as |person|}} <x-option value={{person}}>{{person.name}}</x-option> {{/each}}

    </x-select> EMBER COMPONENTS OF THE NEAR FUTURE
  64. COMPUTED PROPERTIES & COMPUTED MACROS

  65. 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");
 }),
 });
  66. 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");
 }),
 });
  67. IF YOU UNDERSTAND: • Router & Routes • Components •

    Computed Properties • Generating with Ember CLI YOU CAN BUILD AWESOME STUFF
  68. POST-TUTORIAL HIGH

  69. RE-ENTERING EARTH’s GRAVITY :(

  70. 3-week PoC 6-month slog ESTIMATION IS HARD IMPOSSIBLE

  71. TRUE 10x ENGINEERING

  72. THE PROBLEM: YOUR BOSS THINKS POC IS PRODUCTION

  73. YEAH… IF YOU COULD JUST PUT THAT PoC IN PRODUCTION,

    THAT’D BE GREAT
  74. NOW WE’RE GOING TO HURT EMBER’S FEELINGS

  75. THE TROUGH

  76. 5 THINGS THAT EMBER COULD DO BETTER

  77. 1. API CUSTOMIZATION 2. AUTHENTICATION 3. EXTERNAL jQUERY LIBRARIES 4.

    TESTING 5. INTEGRATION WITH EXISTING APPS
  78. WITH GUIDANCE, THE TROUGH CAN EVEN BE FUN TO NAVIGATE

  79. 1. EMBER DATA API CUSTOMIZATION

  80. MY API DOESN’T PLAY NICELY WITH EMBER DATA. EMBER GENERATE

    YOUR WAY OUT OF THAT ONE, SMART GUY.
  81. OUR APP WANTS DATA FROM A NON-REST ENDPOINT

  82. WHAT DO YOU DO WHEN YOUR API DOESN’T CONFORM?

  83. SOLUTION 1 AJAX & pushPayload

  84. store.pushPayload() Push any JSON (properly formatted) into the store, skipping

    Ember Data’s AJAX.
  85. 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
  86. THE PAGE LOADS!

  87. ADD TO CART

  88. ADD ITEM TO CART ISN’T WORKING

  89. GOOD API DOCS: ESSENTIAL

  90. SOLUTION 2 Custom Serializer/Adapter

  91. ADAPTER: DEALS WITH THE SERVER SERIALIZER: DEALS WITH THE DATA

  92. ADAPTERS & SERIALIZERS Your API Object({id: 3}).save() Adapter Builds URL,

    sends XHR Serializer Generates payload & parses response
  93. CUSTOM ADAPTERS: WHEN YOU NEED TO MODIFY API ENDPOINTS

  94. YES, THERE’S A GENERATOR FOR THAT

  95. ember  generate  adapter  cart-­‐item

  96. 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
  97. 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
  98. 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
  99. 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”
  100. 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'
  101. CUSTOM SERIALIZERS: WHEN YOU NEED TO MODIFY THE DATA

  102. http://jsonapi.org/ THE FUTURE

  103. THE PRESENT http://emberjs.com/blog/2013/05/03/ember-data-progress-update.html

  104. HI THERE I’M AN API COWBOY BANG BANG PAYLOAD PAYLOAD

    ENDPOINT ENPOINT URL
  105. 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
  106. 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
  107. ember  generate  serializer  cart-­‐item

  108. 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
  109. 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
  110. 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
  111. SET IT AND FORGET IT

  112. ADD TO CART: DONE!

  113. 2. AUTHENTICATION

  114. WHY THE HECK ISN’T AUTHENTICATION BUILT INTO EMBER?

  115. 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
  116. 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
  117. IF ONLY THERE WERE A GREAT CODE SHARING STORY IN

    EMBER… OH WELL
  118. ember  install  ember-­‐cli-­‐simple-­‐auth USE ADDONS

  119. USING ADDONS

  120. AUTH & CLIENT SIDE DATA SECURITY

  121. “DID YOU JUST SAY CLIENT SIDE DATA SECURITY?”

  122. import Ember from 'ember';
 
 export default Ember.Route.extend({
 model: function()

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

    {
 return this.currentUser.get(“posts”);
 }
 }); MORE SECURE
  124. YOU’RE IN THE API SECURITY BUSINESS NOW, NOT JUST WEB

    PAGE SECURITY
  125. 3. jQUERY WIDGETS

  126. HOW DO I GET jQUERY WIDGETS TO PLAY NICE IN

    MY SINGLE PAGE APP?
  127. WRAP IN COMPONENTS

  128. 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”)
  129. TYPEAHEAD.JS

  130. None
  131. BUT WE’LL BUILD OUR OWN

  132. INSTALLING DEPENDENCIES: BOWER & BROCCOLI

  133. bower  install  -­‐-­‐save-­‐dev  typeahead.js INSTALL TYPEAHEAD

  134. ... var app = new EmberApp(); ...
 app.import("bower_components/typeahead.js/dist/typeahead.jquery.js");
 ... IMPORT

    IN BROCFILE Brocfile.js
  135. I HAVE THE WIDGET, HOW DO I GET DATA IN

    & OUT?
  136. FIND REQUIRED OPTIONS IN DOCS https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md

  137. YOU ARE AT THE MERCY OF THE API DESIGNER OF

    YOUR jQUERY WIDGET
  138. None
  139. MAYBE THE API AUTHOR IS TRYING TO TELL YOU “DON’T

    USE THIS.”
  140. $(document).ready() IS OVER

  141. 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
  142. {{twitter-typeahead source=getStates displayKey=formatState}} From the controller… To the component DATA

    IN: PASSING PROPERTIES TO COMPONENT VIA TEMPLATE
  143. 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
  144. 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”}}
  145. TYPEAHEAD COMPONENT: DONE!

  146. 4. TESTING

  147. MAKE NO MISTAKE: EMBER CLI IS WAY BEYOND MOST OTHER

    TOOLS
  148. BUT WE’RE HOLDING IT TO A HIGHER STANDARD

  149. 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); }); });
  150. 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
  151. UNIT TEST: Verify a unit (model, route, component) works in

    isolation
  152. ACCEPTANCE TEST: Simulate a user’s path through using a feature

    & verify it works
  153. EMBER ACCEPTANCE TEST HELPERS visit click fillIn keyEvent triggerEvent find

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

    Bottom line: It’s personal preference.
  155. TDD, FOR ME, USUALLY MEANS ACCEPTANCE TESTS FIRST

  156. PRETENDER https://github.com/trek/pretender

  157. 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
  158. 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
  159. 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
  160. module('Acceptance: ListProducts', {
 setup: function() {
 App = startApp();
 //

    Assign this.server to createServer()
 this.server = createServer();
 },
 SET UP PRETENDER tests/acceptance/list-products-test.js
  161. 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
  162. 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
  163. ember test 1..53 # tests 53 # pass 53 #

    fail 0 # ok RUN TESTS
  164. WHAT ABOUT WHEN MY FIXTURE DATA BECOMES UNMAINTAINABLE?

  165. ¯\_(ツ)_/¯

  166. None
  167. 5. “SPRINKLING” EMBER

  168. `EMBER NEW` IS AWESOME. BUT I ALREADY HAVE AN APP.

  169. None
  170. None
  171. None
  172. None
  173. NOW YOU CAN CHOOSE: COMPONENT-BASED “SPRINKLING” OR ROUTER-BASED “PJAX” REFACTOR

  174. BONUS: ANIMATION

  175. HOW THE HECK AM I SUPPOSED TO ANIMATE WHEN EMBER

    IS IN CHARGE OF THE DOM?
  176. None
  177. None
  178. 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
  179. DEMO

  180. BONUS: IF YOU UNDERSTAND LIQUID FIRE’S TRANSITION MAP, YOU UNDERSTAND

    EMBER ROUTING
  181. IT TOOK LONGER THAN WE’D HOPED, BUT WE’VE FINISHED THE

    APP!
  182. OUR STORE IS FULLY ARMED AND OPERATIONAL

  183. 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
  184. WHY WADE THROUGH THE TROUGH OF DISILLUSIONMENT?

  185. WHILE YOUR BOSS WASN’T LOOKING, EVERYTHING CHANGED

  186. WHILE WE SWAM THE AVALANCHE, EVERYTHING CHANGED

  187. CLIENT DRIVEN UX IS A THING

  188. EMBER IS A MATURE, ROBUST ANSWER TO THIS

  189. WITH 2.0, EMBER IS APPROACHING THE PLATEAU OF PRODUCTIVITY

  190. HOW DID EMBER “CROSS THE CHASM”?

  191. BECAUSE WHILE SOME DEVS CHURN IN THIS CYCLE… CIRCLE OF

    ETERNAL HOPE
  192. None
  193. EMBER HAS ALWAYS BEEN TARGETING THIS PLATEAU OF PRODUCTIVITY

  194. I USED TO THINK EMBER’S PROMISE WAS TO MAKE ME

    A 10x DEV
  195. “LOOK AT ALL THE CODE I’M NOT WRITING!”

  196. EMBER’S REAL PROMISE IS ABOUT HELPING YOU REACH THE PLATEAU

  197. ON THE PLATEAU THERE IS TIME TO FOCUS

  198. FOCUS ON THINGS THAT ADD VALUE

  199. (BUSINESS VALUE)

  200. ON THE PLATEAU THERE IS TIME TO THINK

  201. MAYBE ABOUT HOW YOUR BOSS IS AN IDIOT

  202. AND SINCE YOU FOCUS ON BUSINESS VALUE

  203. YOU CAN AFFORD TO FIRE YOUR BOSS

  204. AND THEN YOU DO

  205. AND YOU TURN OUT OK

  206. BUT THAT’S ANOTHER TALK

  207. None
  208. THANKS @tehviking http://frontside.io

  209. 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/