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

A Discourse with Ember.js

A Discourse with Ember.js

Discourse is one of the most starred projects on Github, and in its first year was featured in their state of the octoverse as one of the open source projects with the highest momentum.

When Discourse was released, about a year ago, I didn't know either Ember.js or Ruby on Rails. In this talk, I will share my experience and challenges learning Ember.js while contributing to one of the largest open-source Ember.js project and present the things that worked well and the ones that didn't.

B7797beb47cfb7aa0fe60d09604aaa09?s=128

Régis Hanol

August 28, 2014
Tweet

More Decks by Régis Hanol

Other Decks in Programming

Transcript

  1. None
  2. Régis Hanol @ZogStriP

  3. None
  4. Jeff Atwood @codinghorror Sam Saffron @samsaffron

  5. Jeff Atwood @codinghorror Sam Saffron @samsaffron

  6. Jeff Atwood @codinghorror Sam Saffron @samsaffron Robin Ward @eviltrout

  7. Jeff Atwood @codinghorror Sam Saffron @samsaffron Robin Ward @eviltrout

  8. None
  9. None
  10. A with

  11. February '13

  12. JSON API BROWSER APPLICATION

  13. Ruby Rails JavaScript Ember

  14. Ruby Rails JavaScript Ember

  15. Ruby Rails JavaScript Ember

  16. Ruby Rails JavaScript Ember

  17. Ruby Rails JavaScript Ember

  18. None
  19. Ember.js incorporates common idioms so you can focus on what

    makes your app special, not reinventing the wheel.
  20. Convention Configuration

  21. this.route('user');

  22. UserRoute /user this.route('user');

  23. UserRoute User LOADS /user this.route('user');

  24. UserRoute User LOADS UserController SETS MODEL ON /user this.route('user');

  25. UserRoute User LOADS UserController UserView SETS MODEL ON RENDERS /user

    this.route('user');
  26. UserRoute User LOADS UserController UserView user.handlebars SETS MODEL ON RENDERS

    BASED ON /user this.route('user');
  27. UserRoute User LOADS UserController UserView user.handlebars SETS MODEL ON RENDERS

    GETS STATE FROM BASED ON /user this.route('user');
  28. None
  29. None
  30. None
  31. None
  32. None
  33. None
  34. None
  35. None
  36. MVC != MVC Ember Data Outdated help

  37. None
  38. CLIENT SIDE SERVER SIDE MVC

  39. CLIENT SIDE SERVER SIDE LONG-LIVED STATE-FULL

  40. CLIENT SIDE SERVER SIDE LONG-LIVED SHORT-LIVED STATE-FULL STATE-LESS

  41. CLIENT SERVER

  42. CLIENT SERVER GET /posts

  43. CLIENT SERVER GET /posts Router

  44. CLIENT SERVER GET /posts Router PostsController

  45. CLIENT SERVER GET /posts Router PostsController Post

  46. CLIENT SERVER GET /posts Router PostsController Post PostsView

  47. CLIENT SERVER GET /posts Router PostsController Post PostsView <HTML>

  48. CLIENT SERVER GET /posts Router PostsController Post PostsView <HTML> {

  49. CLIENT SERVER Router

  50. CLIENT SERVER Router Post

  51. CLIENT SERVER Router Post GET /posts.json {…}

  52. CLIENT SERVER Router PostsController Post GET /posts.json {…}

  53. CLIENT SERVER Router PostsController Post PostsView PostsTemplate GET /posts.json {…}

  54. DATA

  55. App.Post = Ember.Object.extend({});

  56. var post = App.Post.create(); ! ! ! App.Post = Ember.Object.extend({});

  57. var post = App.Post.create(); ! post.set(‘title’, ‘Hello World’); ! App.Post

    = Ember.Object.extend({});
  58. var post = App.Post.create(); ! post.set(‘title’, ‘Hello World’); ! post.get(‘title’);

    // Hello World App.Post = Ember.Object.extend({});
  59. var post = App.Post.create({ title: “EmberFest" }); ! App.Post =

    Ember.Object.extend({});
  60. var post = App.Post.create({ title: “EmberFest" }); ! post.get(‘title’); //

    EmberFest App.Post = Ember.Object.extend({});
  61. $.getJSON(‘http://...', function(posts) { // posts contains the JSON result !

    }); });
  62. $.getJSON(‘http://...', function(posts) { return posts.map(function(post){ return App.Post.create(post); }); });

  63. $.getJSON(‘http://...') .then(function(posts){ return posts.map(function(post){ return App.Post.create(post); }); });

  64. App.Post.reopenClass({ ! findAll: function(){ return $.getJSON('http://...') .then(function(posts){ return posts.map(function(post){ return

    App.Post.create(post); });
 }); } ! });
  65. App.PostRoute = Ember.Route.extend({ ! model: function() {
 return App.Post.findAll(); }

    ! };
  66. RELEASE BETA CANARY

  67. RELEASE BETA CANARY day

  68. RELEASE BETA CANARY week day

  69. RELEASE BETA CANARY 6 weeks week day

  70. None
  71. None
  72. DEBUG ALL THE THINGS!

  73. {{log object}}

  74. {{debugger}}

  75. Ember.Application.create({ ! });

  76. Transitioned into 'latest' Transitioned into 'new' Transitioned into 'categories' Ember.Application.create({

    LOG_TRANSITIONS: true });
  77. Ember.Application.create({ LOG_TRANSITIONS_INTERNAL: true }); Attempting URL transition to / Transition

    #0: application: calling beforeModel hook Transition #0: application: calling deserialize hook Transition #0: application: calling afterModel hook Transition #0: discovery: calling beforeModel hook Transition #0: discovery: calling deserialize hook Transition #0: discovery: calling afterModel hook Transition #0: discovery.latest: calling beforeModel hook Transition #0: discovery.latest: calling deserialize hook Transition #0: discovery.latest: calling afterModel hook Transition #0: Resolved all models on destination route; finalizing transition. Transition #0: TRANSITION COMPLETE.
  78. Ember.Application.create({ LOG_VIEW_LOOKUPS: true }); Rendering application with <Discourse.ApplicationView:ember1278> Object {fullName:

    "view:application"} ! Rendering topic with <Discourse.TopicView:ember1330> Object {fullName: "view:topic"} ! Could not find "topic.fromParams" template or view. Nothing will be rendered Object {fullName: "template:topic.fromParams"} ! Rendering discovery with default view <Ember._MetamorphView:ember1981> Object {fullName: "view:discovery"}
  79. Ember.Application.create({ LOG_ACTIVE_GENERATION: true }); generated -> route:adminUsers Object {fullName: "route:adminUsers"}

    generated -> route:adminGroups.index Object {fullName: "route:adminGroups.index"} generated -> route:adminEmail Object {fullName: "route:adminEmail"} generated -> route:adminFlags Object {fullName: "route:adminFlags"} generated -> route:adminLogs Object {fullName: "route:adminLogs"} generated -> route:adminCustomize Object {fullName: "route:adminCustomize"}
  80. Ember.Application.create({ LOG_RESOLVER: true }); [✓] view:modal ............................. Discourse.ModalView [✓] controller:modal

    ....................... Discourse.ModalController [ ] template:modal ......................... template at modal [✓] template:modal/modal ................... template at modal/modal [✓] view:topic-entrance .................... Discourse.TopicEntranceView [✓] controller:topic-entrance .............. Discourse.TopicEntranceController [✓] template:topic-entrance ................ template at topic-entrance [✓] controller:composer .................... Discourse.ComposerController [✓] controller:composer-messages ........... Discourse.ComposerMessagesController [✓] template:composer ...................... template at composer [✓] view:composer-messages ................. Discourse.ComposerMessagesView [ ] template:composer-messages ............. template at composer-messages
  81. var container = App.__container__; ! container.lookup("route:application") container.lookup("controller:post") container.lookup("view:post") container.lookup("template:comment")

  82. Ember.onerror = function(error){ console.log(error); };

  83. Ember.RSVP.on('error', function (error) { console.log(error); } );

  84. INSPECTOR github.com/emberjs/ember-inspector

  85. None
  86. None
  87. None
  88. None
  89. None
  90. None
  91. None
  92. None
  93. None
  94. Y U NO TEST?!!

  95. INTEGRATION UNIT TESTS

  96. App.rootElement = '#ember-testing'; ! ! !

  97. App.rootElement = '#ember-testing'; ! App.setupForTesting(); !

  98. App.rootElement = '#ember-testing'; ! App.setupForTesting(); ! App.injectTestHelpers();

  99. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Ember Tests</title> <link rel="stylesheet"

    href="qunit.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <div id="ember-testing"></div> <script src="qunit.js"></script> <script src="ember_code.js"></script> <script src="test_code.js"></script> </body> </html>
  100. github.com/airportyh/testem github.com/karma-runner/karma TEST’EM ARMA

  101. INTEGRATION UNIT TESTS

  102. visit(url) 1/

  103. visit(url) fillIn(selector, text) click(selector) keyEvent(selector, type, keyCode) triggerEvent(selector, type, options)

    1/ 2/
  104. visit(url) fillIn(selector, text) click(selector) keyEvent(selector, type, keyCode) triggerEvent(selector, type, options)

    find(selector, context) 1/ 2/ 3/
  105. visit(url) fillIn(selector, text) click(selector) keyEvent(selector, type, keyCode) triggerEvent(selector, type, options)

    find(selector, context) 1/ 2/ 3/
  106. visit(url) fillIn(selector, text) click(selector) keyEvent(selector, type, keyCode) triggerEvent(selector, type, options)

    find(selector, context) 1/ 2/ 3/
  107. test(“creating a post", function(){ ! ! ! ! ! !

    ! ! ! ! ! ! ! ! });
  108. test(“creating a post", function(){ ! // 1 - visit visit(“/posts/new");

    ! ! ! ! ! ! ! ! ! ! ! });
  109. test(“creating a post", function(){ ! // 1 - visit visit(“/posts/new");

    ! // 2 - interact fillIn("input.title", "My new post"); click(“button.submit"); ! ! ! ! ! ! ! });
  110. test(“creating a post", function(){ ! // 1 - visit visit(“/posts/new");

    ! // 2 - interact fillIn("input.title", "My new post"); click(“button.submit"); ! ! ! // 3 - check equal(find(“ul.posts li:last").text(), "My new post”); ! ! });
  111. visit(url) fillIn(selector, text) click(selector) keyEvent(selector, type, keyCode) triggerEvent(selector, type, options)

    find(selector, context) 1/ 2/ 3/ ASYNCHRONOUS
  112. visit(url) fillIn(selector, text) click(selector) keyEvent(selector, type, keyCode) triggerEvent(selector, type, options)

    find(selector, context) 1/ 2/ 3/ ASYNCHRONOUS SYNCHRONOUS
  113. test(“creating a post", function(){ ! // 1 - visit visit(“/posts/new");

    ! // 2 - interact fillIn("input.title", "My new post"); click(“button.submit"); ! ! ! // 3 - check equal(find(“ul.posts li:last").text(), "My new post”); ! ! });
  114. test(“creating a post", function(){ ! // 1 - visit visit(“/posts/new");

    ! // 2 - interact fillIn("input.title", "My new post"); click(“button.submit"); ! // wait for asynchronous helpers above to complete andThen(function(){ // 3 - check equal(find(“ul.posts li:last").text(), "My new post”); }); ! });
  115. Ember.Test.registerAsyncHelper("addPost", function(app, title, context) { visit("/posts/new"); fillIn("input.title", title); click("button.submit"); }

    ); ! !
  116. Ember.Test.registerAsyncHelper("addPost", function(app, title, context) { visit("/posts/new"); fillIn("input.title", title); click("button.submit"); }

    ); ! // addPost(“Hello World"); // addPost(“Hello Emberfest");
  117. module("Integration", { setup: function() { App.reset(); } });

  118. INTEGRATION UNIT TESTS

  119. Ember.Object

  120. Ember.Object 1/ instantiate

  121. Ember.Object 1/ 2/ instantiate change state

  122. Ember.Object 1/ 2/ 3/ instantiate change state assert

  123. App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return

    this.get(‘status’) == 'published'; }.property(‘status') ! });
  124. App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return

    this.get(‘status’) == 'published'; }.property(‘status') ! }); test(‘isPublished is false by default', function() { ! ! ! });
  125. App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return

    this.get(‘status’) == 'published'; }.property(‘status') ! }); test(‘isPublished is false by default', function() { var post = App.Post.create(); ! ! });
  126. App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return

    this.get(‘status’) == 'published'; }.property(‘status') ! }); test(‘isPublished is false by default', function() { var post = App.Post.create(); ! equal(post.get(‘isPublished’), false); });
  127. test(‘isPublished is true when published', function() { ! ! !

    }); App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return this.get(‘status’) == 'published'; }.property(‘status') ! });
  128. test(‘isPublished is true when published', function() { var post =

    App.Post.create(); ! ! }); App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return this.get(‘status’) == 'published'; }.property(‘status') ! });
  129. test(‘isPublished is true when published', function() { var post =

    App.Post.create(); post.set('status', ‘published'); ! }); App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return this.get(‘status’) == 'published'; }.property(‘status') ! });
  130. test(‘isPublished is true when published', function() { var post =

    App.Post.create(); post.set('status', 'published'); equal(post.get(‘isPublished’), true); }); App.Post = Ember.Object.extend({ status: 'draft', ! isPublished: function() { return this.get(‘status’) == 'published'; }.property(‘status') ! });
  131. ACHIEVEMENT UNLOCKED - - Model - Controller - Route -

    Component - View
  132. None
  133. None
  134. None
  135. THANKS!