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

Get Off My Grass: Weeding Out Logic with Presenters (THAT 2019)

Get Off My Grass: Weeding Out Logic with Presenters (THAT 2019)

Logic in the view layer is a bit of a dandelion on the lawn. What starts with one or two cases often grows and grows to the point of the markup lawn being unrecognizable. Just as I love weeding my yard, I love to refactor brittle controller-view relationships with presenter objects.

In this talk, I'll cover the theory behind the presenter pattern, why it's useful, and breakdown a real-world example where a presenter was the proverbial "right tool for the job". You will learn how to use presenters to let controllers focus on integration and views on displaying data. Lawn care not included.

481a1f18bdd124c255bcf9e79a281ec3?s=128

tmikeschu

August 08, 2019
Tweet

Transcript

  1. GET OFF MY GRASS WEEDING OUT LOGIC WITH PRESENTERS Mike

    Schutte 8/8/19 THAT Conference 1/76 — @tmikeschu
  2. 2/76 — @tmikeschu

  3. 3/76 — @tmikeschu

  4. 4/76

  5. ▸ @tmikeschu ( !"# ) 4/76

  6. ▸ @tmikeschu ( !"# ) ▸ $ 4/76

  7. ▸ @tmikeschu ( !"# ) ▸ $ ▸ % 4/76

  8. ▸ @tmikeschu ( !"# ) ▸ $ ▸ % ▸

    & " ' 4/76
  9. ▸ @tmikeschu ( !"# ) ▸ $ ▸ % ▸

    & " ' ▸ ( ) 4/76
  10. ▸ @tmikeschu ( !"# ) ▸ $ ▸ % ▸

    & " ' ▸ ( ) ▸ 4/76
  11. Questions along the way? @tmikeschu #getOffMyGrass 5/76 — @tmikeschu

  12. FWBAT friendgineers will be able to... 6/76 — @tmikeschu

  13. 7/76 — @tmikeschu

  14. ▸ Explain what a presenter object is 7/76 — @tmikeschu

  15. ▸ Explain what a presenter object is ▸ Explain why

    a presenter object is useful 7/76 — @tmikeschu
  16. ▸ Explain what a presenter object is ▸ Explain why

    a presenter object is useful ▸ Detect when a presenter is the tool for the job 7/76 — @tmikeschu
  17. ▸ Explain what a presenter object is ▸ Explain why

    a presenter object is useful ▸ Detect when a presenter is the tool for the job ▸ Expound upon the virtues of the Ruby language 7/76 — @tmikeschu
  18. ROADMAP 8/76 — @tmikeschu

  19. 9/76 — @tmikeschu

  20. ▸ The View Layer 9/76 — @tmikeschu

  21. ▸ The View Layer ▸ The Presenter Pattern 9/76 —

    @tmikeschu
  22. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies 9/76 — @tmikeschu
  23. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering 9/76 — @tmikeschu
  24. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering ▸ Rendering Collections 9/76 — @tmikeschu
  25. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering ▸ Rendering Collections ▸ Message passing 9/76 — @tmikeschu
  26. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering ▸ Rendering Collections ▸ Message passing ▸ Testing 9/76 — @tmikeschu
  27. THE VIEW LAYER 10/76 — @tmikeschu

  28. THE VIEW LAYER ▸ MVC 10/76 — @tmikeschu

  29. THE VIEW LAYER ▸ MVC ▸ Modern JS libraries 10/76

    — @tmikeschu
  30. THE VIEW LAYER ▸ MVC ▸ Modern JS libraries ▸

    HTML 10/76 — @tmikeschu
  31. THE VIEW LAYER ▸ MVC ▸ Modern JS libraries ▸

    HTML ▸ JSON 10/76 — @tmikeschu
  32. THE PRESENTER PATTERN 11/76 — @tmikeschu

  33. THE PRESENTER PATTERN ▸ Controller gathers model data and 11/76

    — @tmikeschu
  34. THE PRESENTER PATTERN ▸ Controller gathers model data and ▸

    Controller instantiates a presenter object with model data 11/76 — @tmikeschu
  35. THE PRESENTER PATTERN ▸ Controller gathers model data and ▸

    Controller instantiates a presenter object with model data ▸ View sends messages to the presenter for all logical needs 11/76 — @tmikeschu
  36. CONTROLLER CONSTRUCTS VIEW CONSUMES 12/76 — @tmikeschu

  37. # app/controllers/quidditch_controller.rb class QuidditchController def index @presenter = QuidditchPresenter.present(players: Player,

    scores: Score) end end 13/76 — @tmikeschu
  38. # app/presenters/quidditch_presenter.rb class QuidditchPresenter def self.present(players:, scores:) # ... end

    def leading_scorers # ... end end 14/76 — @tmikeschu
  39. <!-- # app/views/quidditch/index.html.erb --> <div> <% @presenter.leading_scorers.each do |leading_scorer| %>

    ... <% end %> </div> 15/76 — @tmikeschu
  40. NOT JUST SERVER-SIDE TEMPLATES! 16/76 — @tmikeschu

  41. class QuidditchContainer extends React.Component { state = { players: [],

    scores: [] }; componentDidMount() { PlayerService.get().then(players => { this.setState({ players }); }); ScoresService.get().then(scores => { this.setState({ scores }); }); } render() { const { players, scores } = this.state; return <QuidditchScores {...quidditchPresenter({ players, scores })} />; } } 17/76 — @tmikeschu
  42. function quidditchPresenter({ players, scores }) { return { leadingScorers: /*

    ... */ }; } 18/76 — @tmikeschu
  43. function QuidditchScores({ leadingScorers }) { return ( <div> {leadingScorers.map(leadingScorer =>

    ( <div>...</div> ))} </div> ); } 19/76 — @tmikeschu
  44. WHAT 20/76 — @tmikeschu

  45. A presenter object encapsulates logic (e.g., boolean and transformation) needed

    for building a particular view. 21/76 — @tmikeschu
  46. WHY 22/76 — @tmikeschu

  47. CONTROLLER: INTEGRATION ! 23/76 — @tmikeschu

  48. VIEW: CONTENT AND STYLE ! 24/76 — @tmikeschu

  49. VIEW: DECLARATIVE INTERFACE ! 25/76 — @tmikeschu

  50. VIEW: SIGNAL TO NOISE 26/76 — @tmikeschu

  51. VIEW: EXPLICIT DEPENDENCIES 27/76 — @tmikeschu

  52. UNIT TEST ✅ 28/76 — @tmikeschu

  53. LESSONS FROM THE FIELD 29/76 — @tmikeschu

  54. CONTEXT 30/76 — @tmikeschu

  55. CONTEXT ▸ Event highlights for each attendee of an event

    30/76 — @tmikeschu
  56. CONTEXT ▸ Event highlights for each attendee of an event

    ▸ Same general layout for all events 30/76 — @tmikeschu
  57. CONTEXT ▸ Event highlights for each attendee of an event

    ▸ Same general layout for all events ▸ Unique branding for each event 30/76 — @tmikeschu
  58. CONTEXT ▸ Event highlights for each attendee of an event

    ▸ Same general layout for all events ▸ Unique branding for each event ▸ Various data sources 30/76 — @tmikeschu
  59. CONTEXT ▸ Event highlights for each attendee of an event

    ▸ Same general layout for all events ▸ Unique branding for each event ▸ Various data sources ▸ Precedence: copy and paste previous event 30/76 — @tmikeschu
  60. 31/76 — @tmikeschu

  61. 32/76 — @tmikeschu

  62. ▸ 84-line controller action, 0 helper methods 32/76 — @tmikeschu

  63. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view 32/76 — @tmikeschu
  64. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view ▸ 3 view directories with basically identical code 32/76 — @tmikeschu
  65. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view ▸ 3 view directories with basically identical code ▸ 7 &&, 11 || 32/76 — @tmikeschu
  66. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view ▸ 3 view directories with basically identical code ▸ 7 &&, 11 || ▸ 26 if statements, 20 ternary expressions 32/76 — @tmikeschu
  67. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view ▸ 3 view directories with basically identical code ▸ 7 &&, 11 || ▸ 26 if statements, 20 ternary expressions ▸ 5 cases of manually iterated markup 32/76 — @tmikeschu
  68. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view ▸ 3 view directories with basically identical code ▸ 7 &&, 11 || ▸ 26 if statements, 20 ternary expressions ▸ 5 cases of manually iterated markup ▸ 20+ Law of Demeter violations 32/76 — @tmikeschu
  69. ▸ 84-line controller action, 0 helper methods ▸ 23 instance

    variables used by view ▸ 3 view directories with basically identical code ▸ 7 &&, 11 || ▸ 26 if statements, 20 ternary expressions ▸ 5 cases of manually iterated markup ▸ 20+ Law of Demeter violations 33/76 — @tmikeschu
  70. POST-PRESENTER... 34/76 — @tmikeschu

  71. 35/76 — @tmikeschu

  72. ▸ 84 12-line controller action, 0 16 helper methods 35/76

    — @tmikeschu
  73. ▸ 84 12-line controller action, 0 16 helper methods ▸

    23 1 instance variable used by view 35/76 — @tmikeschu
  74. ▸ 84 12-line controller action, 0 16 helper methods ▸

    23 1 instance variable used by view ▸ 1 view template with shared partials 35/76 — @tmikeschu
  75. ▸ 84 12-line controller action, 0 16 helper methods ▸

    23 1 instance variable used by view ▸ 1 view template with shared partials ▸ 7 5 &&, 11 1 || 35/76 — @tmikeschu
  76. ▸ 84 12-line controller action, 0 16 helper methods ▸

    23 1 instance variable used by view ▸ 1 view template with shared partials ▸ 7 5 &&, 11 1 || ▸ 19 if, 6 elsif, 20 0 ternaries 35/76 — @tmikeschu
  77. ▸ 84 12-line controller action, 0 16 helper methods ▸

    23 1 instance variable used by view ▸ 1 view template with shared partials ▸ 7 5 &&, 11 1 || ▸ 19 if, 6 elsif, 20 0 ternaries ▸ 5 0 cases of manually iterated markup 35/76 — @tmikeschu
  78. ▸ 84 12-line controller action, 0 16 helper methods ▸

    23 1 instance variable used by view ▸ 1 view template with shared partials ▸ 7 5 &&, 11 1 || ▸ 19 if, 6 elsif, 20 0 ternaries ▸ 5 0 cases of manually iterated markup ▸ 20+ 1 Law of Demeter violation 35/76 — @tmikeschu
  79. DEFINING DEPENDENCIES 36/76 — @tmikeschu

  80. <%= render "frontend/highlights/#{params[:event_slug].upcase}/index", locals: { activity_names: @activity_names, conf_activities: @conf_activities, conf_connections:

    @conf_connections, conf_general: @conf_general, conf_hearts: @conf_hearts, conf_messages: @conf_messages, dinners: @dinners, event_photos: @event_photos, faceoff: @faceoff, lunches: @lunches, photo_ids: @photo_ids, public_token: @public_token, session_list: @session_list, t19_registrant: @t19_registrant, t20_registrant: @t20_registrant, workshops: @workshops, sunday: @sunday, monday: @monday, tuesday: @tuesday, wednesday: @wednesday, thursday: @thursday, friday: @friday, saturday: @saturday } %> 37/76 — @tmikeschu
  81. def show @presenter = Frontend::HighlightsPresenter.present( json_resources.merge({ event_config: event_config, event_titles: event_titles,

    eventster: eventster, faceoff_data: faceoff_data, view_more_photos_url: view_more_photos_url, }) ) render :layout => 'layouts/highlights' end 38/76 — @tmikeschu
  82. def show @presenter = Frontend::HighlightsPresenter.present( json_resources.merge({ event_config: event_config, event_titles: event_titles,

    eventster: eventster, faceoff_data: faceoff_data, view_more_photos_url: view_more_photos_url, }) ) render :layout => 'layouts/highlights' end 38/76 — @tmikeschu
  83. def show @presenter = Frontend::HighlightsPresenter.present( json_resources.merge({ event_config: event_config, event_titles: event_titles,

    eventster: eventster, faceoff_data: faceoff_data, view_more_photos_url: view_more_photos_url, }) ) render :layout => 'layouts/highlights' end 38/76 — @tmikeschu
  84. def self.present( activities:, connections:, event_config:, event_titles:, eventster:, faceoff_data:, hearts:, view_more_photos_url:

    ) # initialize/construct end 39/76 — @tmikeschu
  85. CONDITIONAL RENDERING 40/76 — @tmikeschu

  86. SEPARATE THE NEED FOR A TEST FROM THE TEST ITSELF

    41/76 — @tmikeschu
  87. SEPARATE THE STATEMENT FROM THE EXPRESSION 42/76 — @tmikeschu

  88. <% if locals[:workshops] > 0 || locals[:dinners] > 0 ||

    locals[:lunches] > 0 || locals[:sunday].any? || locals[:monday].any? || locals[:tuesday].any? || locals[:wednesday].any? || locals[:thursday].any? || locals[:friday].any? || locals[:saturday].any? %> <!-- ... --> <% end %> 43/76 — @tmikeschu
  89. <% if locals[:workshops] > 0 || locals[:dinners] > 0 ||

    locals[:lunches] > 0 || locals[:sunday].any? || locals[:monday].any? || locals[:tuesday].any? || locals[:wednesday].any? || locals[:thursday].any? || locals[:friday].any? || locals[:saturday].any? %> <!-- ... --> <% end %> 44/76 — @tmikeschu
  90. vs. 45/76 — @tmikeschu

  91. # presenter def show_activities? @show_activities ||= went_to_workshops? || went_to_dinners? ||

    went_to_lunches? end <!-- view --> <% if @presenter.show_activities? %> <!-- ... --> <% end %> 46/76 — @tmikeschu
  92. ITERATION LOGIC 47/76 — @tmikeschu

  93. <div class="flex flex-wrap tc-ns"> <div class="w-50 w-20-ns mb3 mb0-ns"> <div

    class="f2 f1-l pv2"> 54 </div> <div class="f6 ph3"> Countries represented </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 105 </div> <div class="f6 ph3"> Speakers </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 44 </div> <div class="f6 ph3"> Discovery Sessions </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 23 </div> <div class="f6 ph3"> Exhibits + social spaces </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 1,411 </div> <div class="f6 ph3"> Conversations with Gigi </div> </div> </div> 48/76 — @tmikeschu
  94. <div class="flex flex-wrap tc-ns"> <div class="w-50 w-20-ns mb3 mb0-ns"> <div

    class="f2 f1-l pv2"> 54 </div> <div class="f6 ph3"> Countries represented </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 105 </div> <div class="f6 ph3"> Speakers </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 44 </div> <div class="f6 ph3"> Discovery Sessions </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 23 </div> <div class="f6 ph3"> Exhibits + social spaces </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 1,411 </div> <div class="f6 ph3"> Conversations with Gigi </div> </div> </div> 48/76 — @tmikeschu
  95. <div class="flex flex-wrap tc-ns"> <div class="w-50 w-20-ns mb3 mb0-ns"> <div

    class="f2 f1-l pv2"> 54 </div> <div class="f6 ph3"> Countries represented </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 105 </div> <div class="f6 ph3"> Speakers </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 44 </div> <div class="f6 ph3"> Discovery Sessions </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 23 </div> <div class="f6 ph3"> Exhibits + social spaces </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 1,411 </div> <div class="f6 ph3"> Conversations with Gigi </div> </div> </div> 48/76 — @tmikeschu
  96. <div class="flex flex-wrap tc-ns"> <div class="w-50 w-20-ns mb3 mb0-ns"> <div

    class="f2 f1-l pv2"> 54 </div> <div class="f6 ph3"> Countries represented </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 105 </div> <div class="f6 ph3"> Speakers </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 44 </div> <div class="f6 ph3"> Discovery Sessions </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 23 </div> <div class="f6 ph3"> Exhibits + social spaces </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 1,411 </div> <div class="f6 ph3"> Conversations with Gigi </div> </div> </div> 48/76 — @tmikeschu
  97. <div class="flex flex-wrap tc-ns"> <div class="w-50 w-20-ns mb3 mb0-ns"> <div

    class="f2 f1-l pv2"> 54 </div> <div class="f6 ph3"> Countries represented </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 105 </div> <div class="f6 ph3"> Speakers </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 44 </div> <div class="f6 ph3"> Discovery Sessions </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 23 </div> <div class="f6 ph3"> Exhibits + social spaces </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 1,411 </div> <div class="f6 ph3"> Conversations with Gigi </div> </div> </div> 48/76 — @tmikeschu
  98. <div class="flex flex-wrap tc-ns"> <div class="w-50 w-20-ns mb3 mb0-ns"> <div

    class="f2 f1-l pv2"> 54 </div> <div class="f6 ph3"> Countries represented </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 105 </div> <div class="f6 ph3"> Speakers </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 44 </div> <div class="f6 ph3"> Discovery Sessions </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 23 </div> <div class="f6 ph3"> Exhibits + social spaces </div> </div> <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> 1,411 </div> <div class="f6 ph3"> Conversations with Gigi </div> </div> </div> 48/76 — @tmikeschu
  99. vs. 49/76 — @tmikeschu

  100. <div class="flex flex-wrap tc-ns"> <% @presenter.at_a_glance_stats.each do |(label, value)| %>

    <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> <%= value %> </div> <div class="f6 ph3"> <%= label %> </div> </div> <% end %> </div> 50/76 — @tmikeschu
  101. <div class="flex flex-wrap tc-ns"> <% @presenter.at_a_glance_stats.each do |(label, value)| %>

    <div class="w-50 w-20-ns mb3 mb0-ns"> <div class="f2 f1-l pv2"> <%= value %> </div> <div class="f6 ph3"> <%= label %> </div> </div> <% end %> </div> 50/76 — @tmikeschu
  102. LIMIT ELEMENT INTERFACE TO SIMPLE MESSAGE PASSING 51/76 — @tmikeschu

  103. <div class="w-third w-20-ns bg-near-black square cover hide-child relative bg-center" style="background-image:url(<%=

    crushinate( locals[:event_photos][4].present? ? locals[:event_photos][4]['url'] : 'https://live.staticflickr.com/65535/33746025918_03319ba6a1_o_d.jpg', w: 550, quality:90 ) %>)"> <a href="<%= locals[:event_photos][4].present? ? locals[:event_photos][4]['url'] : 'https://live.staticflickr.com/65535/33746025918_03319ba6a1_o_d.jpg' %>" class="db f6 white child v-mid bg-black-50 w-100 h-100 absolute absolute--fill lightbox"> <div class="vertical-align"> <img src="<%= image_path 'highlights/expand.svg' %>" /> </div> </a> </div> (* 10) 52/76 — @tmikeschu
  104. <div class="w-third w-20-ns bg-near-black square cover hide-child relative bg-center" style="background-image:url(<%=

    crushinate( locals[:event_photos][4].present? ? locals[:event_photos][4]['url'] : 'https://live.staticflickr.com/65535/33746025918_03319ba6a1_o_d.jpg', w: 550, quality:90 ) %>)"> <a href="<%= locals[:event_photos][4].present? ? locals[:event_photos][4]['url'] : 'https://live.staticflickr.com/65535/33746025918_03319ba6a1_o_d.jpg' %>" class="db f6 white child v-mid bg-black-50 w-100 h-100 absolute absolute--fill lightbox"> <div class="vertical-align"> <img src="<%= image_path 'highlights/expand.svg' %>" /> </div> </a> </div> (* 10) 52/76 — @tmikeschu
  105. vs. 53/76 — @tmikeschu

  106. <% @presenter.tile_photos.each do |tile_photo| %> <div class="<%= tile_photo.css_classes %>" style="background-image:url(<%=

    crushinate(tile_photo.id, w: 550, quality:90) %>)"> <a href="<%= tile_photo.id %>" class="db f6 white child v-mid bg-black-50 w-100 h-100 absolute absolute--fill lightbox"> <div class="vertical-align"> <img src="<%= image_path 'highlights/expand.svg' %>" /> </div> </a> </div> <% end %> 54/76 — @tmikeschu
  107. <% @presenter.tile_photos.each do |tile_photo| %> <div class="<%= tile_photo.css_classes %>" style="background-image:url(<%=

    crushinate(tile_photo.id, w: 550, quality:90) %>)"> <a href="<%= tile_photo.id %>" class="db f6 white child v-mid bg-black-50 w-100 h-100 absolute absolute--fill lightbox"> <div class="vertical-align"> <img src="<%= image_path 'highlights/expand.svg' %>" /> </div> </a> </div> <% end %> 54/76 — @tmikeschu
  108. <% @presenter.tile_photos.each do |tile_photo| %> <div class="<%= tile_photo.css_classes %>" style="background-image:url(<%=

    crushinate(tile_photo.id, w: 550, quality:90) %>)"> <a href="<%= tile_photo.id %>" class="db f6 white child v-mid bg-black-50 w-100 h-100 absolute absolute--fill lightbox"> <div class="vertical-align"> <img src="<%= image_path 'highlights/expand.svg' %>" /> </div> </a> </div> <% end %> 54/76 — @tmikeschu
  109. <% @presenter.tile_photos.each do |tile_photo| %> <div class="<%= tile_photo.css_classes %>" style="background-image:url(<%=

    crushinate(tile_photo.id, w: 550, quality:90) %>)"> <a href="<%= tile_photo.id %>" class="db f6 white child v-mid bg-black-50 w-100 h-100 absolute absolute--fill lightbox"> <div class="vertical-align"> <img src="<%= image_path 'highlights/expand.svg' %>" /> </div> </a> </div> <% end %> 54/76 — @tmikeschu
  110. TESTING 55/76 — @tmikeschu

  111. MANY PRESENTER METHODS MIGHT NOT NEED UNIT TESTS 56/76 —

    @tmikeschu
  112. def show_hearts? hearts_enabled? && heart_count > 0 end def show_more_hearts?

    heart_count > 10 end def heart_count @heart_count ||= hearts.count end 57/76 — @tmikeschu
  113. Non-trivial computations or transformations are simple to test 58/76 —

    @tmikeschu
  114. Non-trivial computations or transformations are simple to test ▸ (i.e.,

    no need for spies, mocks, or stubs) 58/76 — @tmikeschu
  115. def workshop_days default_days = %i(Sunday Monday Tuesday Wednesday Thursday Friday

    Saturday) start = default_days.index(first_day.capitalize.to_sym) days = default_days.rotate(start) abbreviations = { Sun: :Sunday, Mon: :Monday, Tue: :Tuesday, Wed: :Wednesday, Thu: :Thursday, Fri: :Friday, Sat: :Saturday, } @workshop_days ||= activities .group_by { |raw| raw.values.first.second.slice(0, 3) } .map { |day, activities| WorkshopDay.new(abbreviations.fetch(day.to_sym), activities) } .sort_by { |workshop_day| days.index(workshop_day.to_s.to_sym) } end 59/76 — @tmikeschu
  116. def workshop_days default_days = %i(Sunday Monday Tuesday Wednesday Thursday Friday

    Saturday) start = default_days.index(first_day.capitalize.to_sym) days = default_days.rotate(start) abbreviations = { Sun: :Sunday, Mon: :Monday, Tue: :Tuesday, Wed: :Wednesday, Thu: :Thursday, Fri: :Friday, Sat: :Saturday, } @workshop_days ||= activities .group_by { |raw| raw.values.first.second.slice(0, 3) } .map { |day, activities| WorkshopDay.new(abbreviations.fetch(day.to_sym), activities) } .sort_by { |workshop_day| days.index(workshop_day.to_s.to_sym) } end 59/76 — @tmikeschu
  117. def workshop_days default_days = %i(Sunday Monday Tuesday Wednesday Thursday Friday

    Saturday) start = default_days.index(first_day.capitalize.to_sym) days = default_days.rotate(start) abbreviations = { Sun: :Sunday, Mon: :Monday, Tue: :Tuesday, Wed: :Wednesday, Thu: :Thursday, Fri: :Friday, Sat: :Saturday, } @workshop_days ||= activities .group_by { |raw| raw.values.first.second.slice(0, 3) } .map { |day, activities| WorkshopDay.new(abbreviations.fetch(day.to_sym), activities) } .sort_by { |workshop_day| days.index(workshop_day.to_s.to_sym) } end 59/76 — @tmikeschu
  118. REMINDER Presenters are dependent on some kind of orchestrator for

    their data 60/76 — @tmikeschu
  119. REMINDER Presenters are dependent on some kind of orchestrator for

    their data ▸ (e.g., controller, container component) 60/76 — @tmikeschu
  120. # spec/support/presenter_factory.rb module PresenterFactory def self.make_highlights_presenter(options = {}) titles =

    OpenStruct.new( asset_slug: "TEDSpaceship3000", full_title: "TEDSpaceship 3000: A Galaxy Far Far Away", title: "TEDSpaceship 3000" ) eventster = OpenStruct.new( badge: OpenStruct.new(firstname: "Voldemort") ) Frontend::HighlightsPresenter.present({ activities: [], connections: { old: [], new: [] }, event_titles: titles, event_config: {}, eventster: eventster, faceoff_data: { events: [], interesting: [], personal: [] }, hearts: [], view_more_photos_url: "www.com", }.merge(options)) end end 61/76 — @tmikeschu
  121. # spec/support/presenter_factory.rb module PresenterFactory def self.make_highlights_presenter(options = {}) titles =

    OpenStruct.new( asset_slug: "TEDSpaceship3000", full_title: "TEDSpaceship 3000: A Galaxy Far Far Away", title: "TEDSpaceship 3000" ) eventster = OpenStruct.new( badge: OpenStruct.new(firstname: "Voldemort") ) Frontend::HighlightsPresenter.present({ activities: [], connections: { old: [], new: [] }, event_titles: titles, event_config: {}, eventster: eventster, faceoff_data: { events: [], interesting: [], personal: [] }, hearts: [], view_more_photos_url: "www.com", }.merge(options)) end end 61/76 — @tmikeschu
  122. # spec/support/presenter_factory.rb module PresenterFactory def self.make_highlights_presenter(options = {}) titles =

    OpenStruct.new( asset_slug: "TEDSpaceship3000", full_title: "TEDSpaceship 3000: A Galaxy Far Far Away", title: "TEDSpaceship 3000" ) eventster = OpenStruct.new( badge: OpenStruct.new(firstname: "Voldemort") ) Frontend::HighlightsPresenter.present({ activities: [], connections: { old: [], new: [] }, event_titles: titles, event_config: {}, eventster: eventster, faceoff_data: { events: [], interesting: [], personal: [] }, hearts: [], view_more_photos_url: "www.com", }.merge(options)) end end 61/76 — @tmikeschu
  123. # spec/support/presenter_factory.rb module PresenterFactory def self.make_highlights_presenter(options = {}) titles =

    OpenStruct.new( asset_slug: "TEDSpaceship3000", full_title: "TEDSpaceship 3000: A Galaxy Far Far Away", title: "TEDSpaceship 3000" ) eventster = OpenStruct.new( badge: OpenStruct.new(firstname: "Voldemort") ) Frontend::HighlightsPresenter.present({ activities: [], connections: { old: [], new: [] }, event_titles: titles, event_config: {}, eventster: eventster, faceoff_data: { events: [], interesting: [], personal: [] }, hearts: [], view_more_photos_url: "www.com", }.merge(options)) end end 61/76 — @tmikeschu
  124. describe "#workshop_days" do let(:activities) { JSON.parse( Rails.root.join( *%w[spec fixtures frontend

    highlights s3_resources.json] ).read, symbolize_names: true ).fetch(:activities) } let(:subject) { PresenterFactory.make_highlights_presenter(activities: activities) } it "returns an ordered collection of days and their activities" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Tuesday") expect(actual.first.activities.count).to eq(2) expect(actual.second.to_s).to eq("Friday") expect(actual.second.activities.count).to eq(1) end # ... 62/76 — @tmikeschu
  125. describe "#workshop_days" do let(:activities) { JSON.parse( Rails.root.join( *%w[spec fixtures frontend

    highlights s3_resources.json] ).read, symbolize_names: true ).fetch(:activities) } let(:subject) { PresenterFactory.make_highlights_presenter(activities: activities) } it "returns an ordered collection of days and their activities" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Tuesday") expect(actual.first.activities.count).to eq(2) expect(actual.second.to_s).to eq("Friday") expect(actual.second.activities.count).to eq(1) end # ... 62/76 — @tmikeschu
  126. describe "#workshop_days" do let(:activities) { JSON.parse( Rails.root.join( *%w[spec fixtures frontend

    highlights s3_resources.json] ).read, symbolize_names: true ).fetch(:activities) } let(:subject) { PresenterFactory.make_highlights_presenter(activities: activities) } it "returns an ordered collection of days and their activities" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Tuesday") expect(actual.first.activities.count).to eq(2) expect(actual.second.to_s).to eq("Friday") expect(actual.second.activities.count).to eq(1) end # ... 62/76 — @tmikeschu
  127. describe "#workshop_days" do let(:activities) { JSON.parse( Rails.root.join( *%w[spec fixtures frontend

    highlights s3_resources.json] ).read, symbolize_names: true ).fetch(:activities) } let(:subject) { PresenterFactory.make_highlights_presenter(activities: activities) } it "returns an ordered collection of days and their activities" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Tuesday") expect(actual.first.activities.count).to eq(2) expect(actual.second.to_s).to eq("Friday") expect(actual.second.activities.count).to eq(1) end # ... 62/76 — @tmikeschu
  128. describe "#workshop_days" do let(:activities) { JSON.parse( Rails.root.join( *%w[spec fixtures frontend

    highlights s3_resources.json] ).read, symbolize_names: true ).fetch(:activities) } let(:subject) { PresenterFactory.make_highlights_presenter(activities: activities) } it "returns an ordered collection of days and their activities" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Tuesday") expect(actual.first.activities.count).to eq(2) expect(actual.second.to_s).to eq("Friday") expect(actual.second.activities.count).to eq(1) end # ... 62/76 — @tmikeschu
  129. context "when first_day is configured" do let(:subject) { PresenterFactory.make_highlights_presenter( activities:

    activities, event_config: { first_day: "friday" } ) } it "orders relative to that day" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Friday") expect(actual.first.activities.count).to eq(1) expect(actual.second.to_s).to eq("Tuesday") expect(actual.second.activities.count).to eq(2) end end end 63/76 — @tmikeschu
  130. context "when first_day is configured" do let(:subject) { PresenterFactory.make_highlights_presenter( activities:

    activities, event_config: { first_day: "friday" } ) } it "orders relative to that day" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Friday") expect(actual.first.activities.count).to eq(1) expect(actual.second.to_s).to eq("Tuesday") expect(actual.second.activities.count).to eq(2) end end end 63/76 — @tmikeschu
  131. context "when first_day is configured" do let(:subject) { PresenterFactory.make_highlights_presenter( activities:

    activities, event_config: { first_day: "friday" } ) } it "orders relative to that day" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Friday") expect(actual.first.activities.count).to eq(1) expect(actual.second.to_s).to eq("Tuesday") expect(actual.second.activities.count).to eq(2) end end end 63/76 — @tmikeschu
  132. context "when first_day is configured" do let(:subject) { PresenterFactory.make_highlights_presenter( activities:

    activities, event_config: { first_day: "friday" } ) } it "orders relative to that day" do actual = subject.workshop_days expect(actual.first.to_s).to eq("Friday") expect(actual.first.activities.count).to eq(1) expect(actual.second.to_s).to eq("Tuesday") expect(actual.second.activities.count).to eq(2) end end end 63/76 — @tmikeschu
  133. REVIEW ! 64/76 — @tmikeschu

  134. 65/76 — @tmikeschu

  135. ▸ The View Layer 65/76 — @tmikeschu

  136. ▸ The View Layer ▸ The Presenter Pattern 65/76 —

    @tmikeschu
  137. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies 65/76 — @tmikeschu
  138. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering 65/76 — @tmikeschu
  139. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering ▸ Rendering Collections 65/76 — @tmikeschu
  140. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering ▸ Rendering Collections ▸ Message passing 65/76 — @tmikeschu
  141. ▸ The View Layer ▸ The Presenter Pattern ▸ Defining

    Dependencies ▸ Conditional Rendering ▸ Rendering Collections ▸ Message passing ▸ Testing 65/76 — @tmikeschu
  142. FWBAT 66/76 — @tmikeschu

  143. WHAT IS A PRESENTER OBJECT? a) a laser pointer b)

    an object that encapsulates logic needed for building a view c) the view layer of an application 67/76 — @tmikeschu
  144. WHAT IS A PRESENTER OBJECT? a) a laser pointer b)

    an object that encapsulates logic needed for building a view c) the view layer of an application 68/76 — @tmikeschu
  145. WHY IS A PRESENTER OBJECT USEFUL? a) controller focuses on

    integration b) view focuses on content and styling c) explicitly defines the view's dependencies d) easy to unit test e) all the above 69/76 — @tmikeschu
  146. WHY IS A PRESENTER OBJECT USEFUL? a) controller focuses on

    integration b) view focuses on content and styling c) explicitly defines the view's dependencies d) easy to unit test e) all the above ✅ 70/76 — @tmikeschu
  147. WHEN MIGHT A PRESENTER BE THE TOOL FOR THE JOB?

    a) computational logic in a controller (-like object) b) repetitive markup c) complex logic in the view d) multiple objects used in the view e) all the above 71/76 — @tmikeschu
  148. WHEN MIGHT A PRESENTER BE THE TOOL FOR THE JOB?

    a) computational logic in a controller (-like object) b) repetitive markup c) complex logic in the view d) multiple objects used in the view e) all the above ✅ 72/76 — @tmikeschu
  149. WHAT DO YOU LOVE ABOUT THE RUBY LANGUAGE? a) yes

    b) I choose to fail the quiz 73/76 — @tmikeschu
  150. WHAT DO YOU LOVE ABOUT THE RUBY LANGUAGE? a) yes

    ✅ b) I choose to fail the quiz 74/76 — @tmikeschu
  151. 75/76 — @tmikeschu

  152. THANK YOU! ! " ! " ! " ! Questions/comments:

    @tmikeschu #getOffMyGrass 76/76