Advertisement

Building a Single Page Application using Ember.js ... for fun and profit

Sep. 30, 2015
Advertisement

More Related Content

Advertisement
Advertisement

Building a Single Page Application using Ember.js ... for fun and profit

  1. Building a Single Page App with Ember.js …for fun and profit!
  2. largest free entrepreneurial event of its kind in North America
  3. yay, sponsors!
  4. Ben Limmer blimmer @l1m5 hello@benlimmer.com ember.party
  5. conceptual
  6. Ron White ronco @ronco1337
  7. live coding
  8. $2 cash back
  9. $5 cash back
  10. $5 cash back $10 cash back $10 cash back
  11. we need a website
  12. ok - what does it do?
  13. it’s simple, really
  14. show rebates it needs to allow registration allow account mgmt provide cash out explain how it works show where it works … build a shopping list be location aware track all the things
  15. how do we build it?
  16. server-rendered?
  17. ibotta.com
  18. ibotta.com
  19. ibotta.com html, js, css
  20. ibotta.com *click*
  21. html, js, css ibotta.com
  22. ibotta.com
  23. client rendered? (single page app)
  24. ibotta.com
  25. ibotta.com
  26. ibotta.com html, js, css
  27. ibotta.com *click*
  28. ibotta.com json
  29. ibotta.com *click*
  30. ibotta.com *click*
  31. ibotta.com
  32. ibotta.com
  33. single page apps are not always the best choice.
  34. pre-render challenges (seo, social share) overkill for simple sites some duplication of backend data requires (building) an API
  35. but you’re here
  36. single page app frameworks
  37. single page app frameworks
  38. single page app frameworks
  39. single page app frameworks
  40. single page app frameworks
  41. single page app frameworks
  42. very un-opinionated very opinionated
  43. so why did we go with Ember @ Ibotta?
  44. ¯_(ツ)_/¯
  45. convention over configuration
  46. – Ruby on Rails Guides “conventions will speed up development, keep your code concise and readable and - most important - these conventions allow you an easy navigation inside your application.” https://en.wikibooks.org/wiki/Ruby_on_Rails/Getting_Started/Convention_Over_Configuration* emphasis mine
  47. structure of an ember app app ├── components ├── controllers ├── helpers ├── models ├── routes ├── styles └── templates └── components tests ├── helpers ├── integration │ └── components └── unit
  48. who cares? it’s just a framework…
  49. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0
  50. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0
  51. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0
  52. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0
  53. knowledge in the head vs. knowledge in the world adapted from concepts in Don Norman’s The Design of Everyday Things
  54. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0 used by many used by few
  55. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0 used by few used by many more knowledge in the head
  56. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0 used by few used by many more knowledge in the head more knowledge in the world
  57. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0 used by few used by many more knowledge in the head more knowledge in the world
  58. *Icons made Freepik from www.flaticon.com is licensed by CC BY 3.0 used by few used by many more knowledge in the head more knowledge in the world
  59. }how it works
  60. how it works
  61. how it works
  62. how it works
  63. ?
  64. }how it works
  65. }how it works
  66. best practices are established
  67. testing routing data definition validation i18n builds deployment dev tools upgrade paths
  68. “there’s an add-on for that”
  69. automated testing out-of-the-box support w/ qunit and phantom
  70. modern language features out-of-the-box support for ES6/ES2015++ with Babel.js
  71. ✓Array comprehensions ✓Arrow functions ✓Async functions ✓Async generator functions ✓Classes ✓Class properties ✓Computed property names ✓Constants ✓Decorators ✓Default parameters ✓Destructuring ✓Exponentiation operator ✓For-of ✓Function bind ✓Generators ✓Generator comprehensions ✓Let scoping ✓Modules ✓Module export extensions ✓Object rest/spread ✓Property method assignment ✓Property name shorthand ✓Rest parameters ✓React ✓Spread ✓Template literals ✓Type annotations ✓Unicode regex
  72. no more var self = this;
  73. design frameworks plug-and-play support for the most popular design frameworks and preprocessors
  74. i18n ember i18n 您好 Здравствуйте! hello bonjour hola
  75. “there’s an add-on for that”
  76. “there’s (not) an add-on for that”
  77. ember add-on
  78. thriving community
  79. 3000 members (and growing)
  80. 400 members (and growing) Denver Devs
  81. DenverDevs.org
  82. meetup.com/ Ember-js-Denver
  83. ember @ ibotta
  84. 0 to prod in two months
  85. > 2x traffic
  86. internal ember apps
  87. let’s build something.
  88. …but first initial questions?
  89. let’s build something.
  90. Our App
  91. Our App
  92. $ ember new bbs
  93. demo
  94. structure of our app app ├── components ├── controllers ├── helpers ├── models ├── routes ├── styles └── templates └── components tests ├── helpers ├── integration │ └── components └── unit
  95. ./app/templates/application.hbs 1 <h2 id="title">Welcome to Ember</h2> 2 3 {{outlet}} Live reload demo here. Hello Denver!
  96. demo
  97. $ ember install ember-cli-materialize $ ember install ember-i18n
  98. demo
  99. I18n 1 <!-- ./app/templates/index.hbs --> 2 <div class="home-feature"> 3 <div class="feature-content"> 4 <h1>{{t 'index.headline'}}</h1> 5 <h3>{{t 'index.subTitle'}}</h3> 6 </div> 7 </div> 1 // ./app/locales/en/translations.js 2 export default { 3 'index': { 4 'headline': 'Frozen Bananas!', 5 'subTitle': 'Coming Soon' 6 } 7 };
  100. I18n 3 <div class="feature-content"> 4 <h1>{{t 'index.headline'}}</h1> 5 <h3>{{t 'index.subTitle'}}</h3> 6 </div>
  101. I18n 1 <!-- ./app/templates/index.hbs --> 2 <div class="home-feature"> 3 <div class="feature-content"> 4 <h1>{{t 'index.headline'}}</h1> 5 <h3>{{t 'index.subTitle'}}</h3> 6 </div> 7 </div> 1 // ./app/locales/en/translations.js 2 export default { 3 'index': { 4 'headline': 'Frozen Bananas!', 5 'subTitle': 'Coming Soon' 6 } 7 };
  102. I18n 1 // ./app/locales/en/translations.js 2 export default { 3 'index': { 4 'headline': 'Frozen Bananas!', 5 'subTitle': 'Coming Soon' 6 } 7 };
  103. $ ember generate acceptance-test index
  104. ./tests/acceptance/index-test.js 1 // ... 2 3 test('visiting /index', function(assert) { 4 assert.expect(3); 5 visit('/'); 6 7 andThen(function() { 8 assert.equal(currentURL(), '/'); 9 let headline = find('.home-feature h1'); 10 assert.equal(headline.length, 1); 11 assert.equal(headline.text(), 'Frozen Bananas!'); 12 }); 13 }); Promise explainer Here?
  105. ./tests/acceptance/index-test.js 5 visit('/');
  106. ./tests/acceptance/index-test.js 1 // ... 2 3 test('visiting /index', function(assert) { 4 assert.expect(3); 5 visit('/'); 6 7 andThen(function() { 8 assert.equal(currentURL(), '/'); 9 let headline = find('.home-feature h1'); 10 assert.equal(headline.length, 1); 11 assert.equal(headline.text(), 'Frozen Bananas!'); 12 }); 13 });
  107. ./tests/acceptance/index-test.js 7 andThen(function() { 8 assert.equal(currentURL(), '/'); 9 let headline = find('.home-feature h1'); 10 assert.equal(headline.length, 1); 11 assert.equal(headline.text(), 'Frozen Bananas!'); 12 });
  108. √ ok 1 PhantomJS 2.0 - Acceptance | index: visiting /index
  109. Our App
  110. Our App
  111. $ ember generate route about
  112. ./app/router.js 1 import Ember from 'ember'; 2 import config from './config/environment'; 3 4 var Router = Ember.Router.extend({ 5 location: config.locationType 6 }); 7 8 Router.map(function() { 9 this.route('about'); 10 }); 11 12 export default Router;
  113. ./app/router.js 8 Router.map(function() { 9 this.route('about'); 10 });
  114. ./app/templates/about.hbs 1 <div class="container"> 2 {{#md-card 3 title=(t 'about.title') 4 class="teal" 5 titleClass="white-text" 6 bodyClass="white-text" 7 id="address-card"}} 8 {{#md-card-content class="white-text"}} 9 <p>{{t 'about.number'}}</p> 10 <p>{{t 'about.street'}}</p> 11 {{/md-card-content}} 12 {{/md-card}} 13 </div>
  115. ./app/templates/about.hbs 2 {{#md-card 3 title=(t 'about.title') 4 class="teal" 5 titleClass="white-text" 6 bodyClass="white-text" 7 id="address-card"}} 12 {{/md-card}}
  116. ./app/templates/about.hbs 1 <div class="container"> 2 {{#md-card 3 title=(t 'about.title') 4 class="teal" 5 titleClass="white-text" 6 bodyClass="white-text" 7 id="address-card"}} 8 {{#md-card-content class="white-text"}} 9 <p>{{t 'about.number'}}</p> 10 <p>{{t 'about.street'}}</p> 11 {{/md-card-content}} 12 {{/md-card}} 13 </div>
  117. ./app/templates/about.hbs 8 {{#md-card-content class="white-text"}} 9 <p>{{t 'about.number'}}</p> 10 <p>{{t 'about.street'}}</p> 11 {{/md-card-content}}
  118. ./app/templates/index.hbs 1 <div class="home-feature"> 2 <div class="feature-content"> 3 <h1>{{t 'index.headline'}}</h1> 4 <h3>{{t 'index.subTitle'}}</h3> 5 <div class="about-link"> 6 {{#link-to 'about'}} 7 <img src="/images/banana_grabber.png"> 8 {{/link-to}} 9 </div> 10 </div> 11 </div>
  119. ./app/templates/index.hbs 5 <div class="about-link"> 6 {{#link-to 'about'}} 7 <img src="/images/banana_grabber.png"> 8 {{/link-to}} 9 </div>
  120. demo
  121. $ ember generate acceptance-test about
  122. ./tests/acceptance/about-test.js 1 // ... 2 3 test('visiting /about from index', function(assert) { 4 visit('/'); 5 click('.about-link img'); 6 7 andThen(function() { 8 assert.equal(currentURL(), '/about'); 9 }); 10 });
  123. √ ok 1 PhantomJS 2.0 - Acceptance | about: visiting /about from index
  124. $ ember generate model company-address
  125. ./app/models/company-address.js 1 import DS from 'ember-data'; 2 3 export default DS.Model.extend({ 4 name: DS.attr('string'), 5 street1: DS.attr('string'), 6 street2: DS.attr('string'), 7 city: DS.attr('string'), 8 state: DS.attr('string'), 9 zip: DS.attr('string') //string not number 10 });
  126. ./app/models/company-address.js 4 name: DS.attr('string'), 5 street1: DS.attr('string'), 6 street2: DS.attr('string'), 7 city: DS.attr('string'), 8 state: DS.attr('string'), 9 zip: DS.attr('string') //string not number
  127. ./app/routes/about.js 1 import Ember from 'ember'; 2 3 export default Ember.Route.extend({ 4 model() { 5 return this.get('store').findAll('company-address'); 6 } 7 });
  128. Promises
  129. Objects not Callbacks Complete or Not
  130. Chainable
  131. Traditional async 1 asyncCall1(function() { 2 asyncCall2(function() { 3 asyncCall3(function() { 4 asyncCall4(function() { 5 asyncCall5(function() { 6 finalCall(); 7 }); 8 }); 9 }); 10 }); 11 });
  132. Chained Promises 1 asyncCall1() 2 .then(asyncCall2) 3 .then(asyncCall3) 4 .then(asyncCall4) 5 .then(asyncCall5) 6 .then(finalCall);
  133. Chained Promises 1 asyncCall1() 2 .then(asyncCall2) 3 .then(asyncCall3) 4 .then(asyncCall4) 5 .then(asyncCall5) 6 .then(finalCall) 7 .catch(errorHandler);
  134. Deferred Interest
  135. ./app/routes/about.js 4 model() { 5 return this.get('store').findAll('company-address'); 6 }
  136. .then(the-template)
  137. ./app/templates/about.hbs 1 <div class="container"> 2 {{#md-card 3 title=model.lastObject.name 4 class="teal" 5 titleClass="white-text" 6 bodyClass="white-text" 7 id="address-card"}} 8 {{#md-card-content class="white-text"}} 9 <p>{{model.lastObject.street1}}</p> 10 <p>{{model.lastObject.street2}}</p> 11 <p> 12 {{model.lastObject.city}}, {{model.lastObject.state}} 13 {{model.lastObject.zip}} 14 </p> 15 {{/md-card-content}} 16 {{/md-card}} 17 </div> Demo, show error page
  138. ./app/templates/about.hbs 9 <p>{{model.lastObject.street1}}</p> 10 <p>{{model.lastObject.street2}}</p> 11 <p> 12 {{model.lastObject.city}}, {{model.lastObject.state}} 13 {{model.lastObject.zip}} 14 </p> Demo, show error page
  139. demo
  140. ./app/templates/error.hbs 1 <div class="error-page"> 2 <h1>Ooops!</h1> 3 {{shrug-guy}} 4 </div>
  141. ./app/templates/error.hbs 3 {{shrug-guy}}
  142. ./app/templates/error.hbs ¯_(ツ)_/¯
  143. Gob Server Engineer
  144. $ ember install ember-cli-mirage
  145. $ ember generate fixture company-addresses
  146. ./app/mirage/fixtures/company-addresses.js 1 export default [ 2 { 3 id: 1, 4 name: 'Bluth's Banana Stand', 5 street1: 'In a Van', 6 street2: 'Down by the river', 7 city: 'Denver', 8 state: 'CO', 9 zip: 80202 10 } 11 ];
  147. ./app/mirage/config.js 1 export default function() { 2 3 this.get('/companyAddresses', 'company-addresses'); 4 5 }
  148. ./app/mirage/config.js 3 this.get('/companyAddresses', 'company-addresses');
  149. demo
  150. ./app/mirage/config.js 1 export default function() { 2 this.timing = 2000; 3 4 this.get('/companyAddresses', 'company-addresses'); 5 6 }
  151. ./app/mirage/config.js 2 this.timing = 2000;
  152. ./app/templates/loading.hbs 1 <div class="loading-page"> 2 {{md-loader}} 3 </div>
  153. ./app/templates/loading.hbs 2 {{md-loader}}
  154. demo
  155. Our App
  156. Our App
  157. $ ember generate component subscribe-form
  158. Components Are Reusable
  159. Components Are Reusable
  160. Our App
  161. ./app/templates/index.hbs 1 <div class="home-feature"> 2 <div class="feature-content row"> 3 <div class="col m6"> 4 <h1>{{t 'index.headline'}}</h1> 5 <h3>{{t 'index.subTitle'}}</h3> 6 <div class="about-link"> 7 {{#link-to 'about'}} 8 <img src="/images/banana_grabber.png"> 9 {{/link-to}} 10 </div> 11 </div> 12 <div class="subscribe-container col m6"> 13 {{subscribe-form model=model}} 14 </div> 15 </div> 16 </div>
  162. ./app/templates/index.hbs 13 {{subscribe-form model=model}}
  163. $ ember generate model email-subscription
  164. ./app/models/email-subscription.js 1 import DS from 'ember-data'; 2 3 export default DS.Model.extend({ 4 email: DS.attr('string'), 5 marketing: DS.attr('boolean', { 6 defaultValue: true 7 }) 8 });
  165. ./app/models/email-subscription.js 5 marketing: DS.attr('boolean', { 6 defaultValue: true 7 })
  166. ./app/routes/index.js 1 import Ember from 'ember'; 2 3 export default Ember.Route.extend({ 4 model() { 5 return this.get('store').createRecord('email-subscription'); 6 } 7 });
  167. ./app/routes/index.js 5 return this.get(‘store') .createRecord('email-subscription');
  168. ./app/templates/components/subscribe-form.hbs 1 {{#md-card title=title}} 2 {{#if saved}} 3 {{#md-card-content}} 4 {{t 'subscribe.successMessage' email=model.email}} 5 {{/md-card-content}} 6 {{else}} 7 {{#md-card-content}} 8 {{md-input value=model.email label=(t 'subscribe.email.label') 9 type='email' validate=true}} 10 {{md-check checked=model.marketing 11 name=(t 'subscribe.marketing.label')}} 12 {{#if saving}} 13 {{md-loader}} 14 {{/if}} 15 {{/md-card-content}} 16 {{#md-card-action}} 17 <button {{action 'subscribe'}}> 18 {{t 'subscribe.submit'}} 19 </button> 20 {{/md-card-action}} 21 {{/if}} 22 {{/md-card}}
  169. ./app/templates/components/subscribe-form.hbs 8 {{md-input value=model.email label=(t subscribe.email.label') 9 type='email' validate=true}}
  170. ./app/templates/components/subscribe-form.hbs 1 {{#md-card title=title}} 2 {{#if saved}} 3 {{#md-card-content}} 4 {{t 'subscribe.successMessage' email=model.email}} 5 {{/md-card-content}} 6 {{else}} 7 {{#md-card-content}} 8 {{md-input value=model.email label=(t 'subscribe.email.label') 9 type='email' validate=true}} 10 {{md-check checked=model.marketing 11 name=(t 'subscribe.marketing.label')}} 12 {{#if saving}} 13 {{md-loader}} 14 {{/if}} 15 {{/md-card-content}} 16 {{#md-card-action}} 17 <button {{action 'subscribe'}}> 18 {{t 'subscribe.submit'}} 19 </button> 20 {{/md-card-action}} 21 {{/if}} 22 {{/md-card}}
  171. ./app/templates/components/subscribe-form.hbs 10 {{md-check checked=model.marketing 11 name=(t 'subscribe.marketing.label')}}
  172. ./app/templates/components/subscribe-form.hbs 1 {{#md-card title=title}} 2 {{#if saved}} 3 {{#md-card-content}} 4 {{t 'subscribe.successMessage' email=model.email}} 5 {{/md-card-content}} 6 {{else}} 7 {{#md-card-content}} 8 {{md-input value=model.email label=(t 'subscribe.email.label') 9 type='email' validate=true}} 10 {{md-check checked=model.marketing 11 name=(t 'subscribe.marketing.label')}} 12 {{#if saving}} 13 {{md-loader}} 14 {{/if}} 15 {{/md-card-content}} 16 {{#md-card-action}} 17 <button {{action 'subscribe'}}> 18 {{t 'subscribe.submit'}} 19 </button> 20 {{/md-card-action}} 21 {{/if}} 22 {{/md-card}}
  173. ./app/templates/components/subscribe-form.hbs 17 <button {{action 'subscribe'}}> 18 {{t 'subscribe.submit'}} 19 </button>
  174. ./app/components/subscribe-form.js 1 import Ember from 'ember'; 2 3 export default Ember.Component.extend({ 4 i18n: Ember.inject.service('i18n'), 5 title: Ember.computed('saved', function() { 6 if (this.get('saved')) { 7 return this.get('i18n').t('subscribe.successHeader'); 8 } else { 9 return this.get('i18n').t('subscribe.header'); 10 } 11 }), 12 saved: false, 13 actions: { 14 subscribe() { 15 this.set('saving', true); 16 this.get('model').save().then(() => { 17 this.set('saving', false); 18 this.set('saved', true); 19 }, () => { 20 //error handling here 21 }); 22 } 23 } 24 });
  175. ./app/components/subscribe-form.js 5 title: Ember.computed('saved', function() { 6 if (this.get('saved')) { 7 return this.get('i18n').t('subscribe.successHeader'); 8 } else { 9 return this.get('i18n').t('subscribe.header'); 10 } 11 }),
  176. ./app/components/subscribe-form.js 1 import Ember from 'ember'; 2 3 export default Ember.Component.extend({ 4 i18n: Ember.inject.service('i18n'), 5 title: Ember.computed('saved', function() { 6 if (this.get('saved')) { 7 return this.get('i18n').t('subscribe.successHeader'); 8 } else { 9 return this.get('i18n').t('subscribe.header'); 10 } 11 }), 12 saved: false, 13 actions: { 14 subscribe() { 15 this.set('saving', true); 16 this.get('model').save().then(() => { 17 this.set('saving', false); 18 this.set('saved', true); 19 }, () => { 20 //error handling here 21 }); 22 } 23 } 24 });
  177. ./app/components/subscribe-form.js 13 actions: { 14 subscribe() { 15 this.set('saving', true); 16 this.get('model').save().then(() => { 17 this.set('saving', false); 18 this.set('saved', true); 19 }, () => { 20 //error handling here 21 }); 22 } 23 }
  178. ./app/mirage/config.js 1 export default function() { 2 this.timing = 2000; 3 4 this.get('/companyAddresses', 'company-addresses'); 5 6 this.post('/emailSubscriptions', 7 'email-subscriptions'); 8 }
  179. ./app/mirage/config.js 6 this.post('/emailSubscriptions', 7 'email-subscriptions');
  180. demo
  181. ./tests/integration/components/subscribe-form-test.js 1 test('it renders', function(assert) { 2 assert.expect(3); 3 4 this.render(hbs`{{subscribe-form}}`); 5 6 assert.equal( 7 this.$('.card-title').text().trim(), 8 'Subscribe for updates on the stand.' 9 ); 10 11 assert.equal( 12 this.$('.input-field label').text().trim(), 13 'Please enter your email address.' 14 ); 15 16 assert.equal( 17 this.$('.materialize-checkbox label').text().trim(), 18 'Yes I would like to receive marketing material from Bluth sponsors. 19 ' 20 ); 21 22 });
  182. ./tests/acceptance/index-test.hbs 1 test('subscribe for updates', function(assert) { 2 visit('/'); 3 4 fillIn( 5 '.subscribe-container .input-field input', 6 'veep@whitehouse.gov' 7 ); 8 click('.card-action button'); 9 andThen(() => { 10 // stays on index 11 assert.equal(currentURL(), '/'); 12 13 assert.equal( 14 find('.subscribe-container .card-title').text().trim(), 15 'Thanks for signing up.' 16 ); 17 assert.equal( 18 find('.card p').text().trim(), 19 'We'll send all of our updates to veep@whitehouse.gov.' 20 ); 21 }); 22 23 });
  183. test demo
  184. what we just built
  185. • https://github.com/Ibotta/dsw-2015-ember-demo
  186. ibotta.com/careers
  187. Thanks!
Advertisement