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

Rethinking the View Layer with Components

Rethinking the View Layer with Components

While most of Rails has evolved over time, the view layer hasn’t changed much. At GitHub, we are incorporating the lessons of the last decade into a new paradigm: components. This approach has enabled us to leverage traditional object-oriented techniques to test our views in isolation, avoid side-effects, refactor with confidence, and perhaps most importantly, make our views first-class citizens in Rails.

16c548309c54a9bcda2b551fa82deec0?s=128

Joel Hawksley

May 01, 2019
Tweet

Transcript

  1. Rethinking the View Layer with Components

  2. Joel Hawksley hawksley.org

  3. None
  4. None
  5. Creativity

  6. Imagine

  7. Something

  8. Nothing

  9. New ideas

  10. Combining

  11. Changing

  12. Reapplying

  13. Existing Ideas

  14. None
  15. None
  16. Testing

  17. Code Coverage

  18. Data Flow

  19. Standards

  20. ActionView::Component

  21. Testing

  22. Code Coverage

  23. Data Flow

  24. Standards

  25. >200x

  26. None
  27. Views

  28. Data → HTML

  29. 2004 ERB 1.0 2005 Rails 1.0 2012 Turbolinks 2016 API

    Mode
  30. None
  31. Rails is not only a great choice when you

  32. want to build a full-stack application that uses

  33. server-side rendering of HTML templates

  34. but also a great companion for the

  35. new crop of client-side JavaScript or native applications

  36. just needs the backend to speak JSON.

  37. 2004 ERB 1.0 2005 Rails 1.0 2012 Turbolinks 2016 API

    Mode
  38. None
  39. ERB

  40. Progressive Enhancement

  41. Why?

  42. Performance

  43. Browser Support

  44. <% if supported_browser? %> <%= javascript_bundle 'polyfills' if compatibility_browser? %>

    <%= javascript_bundle 'frameworks' %> <%= javascript_bundle 'github', async: true %> <%= yield :scripts %> <%= controller_javascript_bundles %> <% else %> <%= javascript_bundle 'unsupported' %> <% end %>
  45. Progressive Enhancement

  46. None
  47. None
  48. None
  49. None
  50. None
  51. None
  52. None
  53. class Issue < ApplicationRecord belongs_to :pull_request end class PullRequest <

    ApplicationRecord has_one :issue, inverse_of: :pull_request end
  54. <% if pull_request && pull_request.merged? %> <div class="State State--purple"> <%=

    octicon('git-merge') %> Merged </div> <% elsif pull_request && pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif pull_request && pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% elsif pull_request %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% elsif issue && issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% elsif issue %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %>
  55. None
  56. None
  57. None
  58. Rails @ GitHub

  59. 209

  60. 556

  61. 3718

  62. None
  63. Testing

  64. 6s

  65. None
  66. None
  67. What trips you up with Rails views?

  68. Data Flow

  69. N + 1

  70. <% if pull_request && pull_request.merged? %> <div class="State State--purple"> <%=

    octicon('git-merge') %> Merged </div> <% elsif pull_request && pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif pull_request && pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% elsif pull_request %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% elsif issue && issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% elsif issue %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %>
  71. Unit Testing

  72. Partials

  73. Code Coverage

  74. SimpleCov Coveralls

  75. Implicit Arguments

  76. <% if pull_request && pull_request.merged? %> <div class="State State--purple"> <%=

    octicon('git-merge') %> Merged </div> <% elsif pull_request && pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif pull_request && pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% elsif pull_request %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% elsif issue && issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% elsif issue %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %>
  77. Standards

  78. None
  79. None
  80. None
  81. None
  82. Standards

  83. Testing

  84. Code Coverage

  85. Data Flow

  86. Implicit Arguments

  87. Standards

  88. MVC

  89. MvC

  90. None
  91. Components

  92. class Greeting extends React.Component { render() { return <div>Hello, {this.props.name}!</div>;

    } } React.render(<Greeting name="World" />, document.getElementById('example'));
  93. class IssueBadge extends React.Component { render() { return ( <div

    className={ "State " + this._stateClass() }> <i className={this._icon()} /> {this._label()} </div> ) } _icon() { ... } _stateClass() { ... } _label() { ... } }
  94. Types

  95. IssueBadge.propTypes = { issue: PropTypes.exact({ isClosed: PropTypes.bool.isRequired }).isRequired, pullRequest: PropTypes.exact({

    isClosed: PropTypes.bool.isRequired, isMerged: PropTypes.bool.isRequired, isDraft: PropTypes.bool.isRequired }), };
  96. class IssueBadge extends React.Component { render() { return ( <div

    className={ "State " + this._stateClass() }> <i className={this._icon()} /> {this._label()} </div> ) } _icon() { return this.props.issue.isClosed ... } _stateClass() { ... } _label() { ... } }
  97. Data Flow

  98. Values > Objects

  99. Testing

  100. it('should render the closed issue badge', function() { expect(shallow(<IssueBadge props={{

    issue: { isClosed: true }}} />). contains(<div className="State State--red">Closed</div>)).toBe(true); });
  101. None
  102. Components

  103. Types

  104. Data flow

  105. Testing

  106. None
  107. None
  108. None
  109. Tests

  110. <% if pull_request && pull_request.merged? %> <div class="State State--purple"> <%=

    octicon('git-merge') %> Merged </div> <% elsif pull_request && pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif pull_request && pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% elsif pull_request %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% elsif issue && issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% elsif issue %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %>
  111. it "renders the open issue badge" do create(:issue, :open) get

    :index assert_select(".State.State--green") assert_select(".octicon-issue-opened") assert_includes(response.body, "Open") end it "renders the closed issue badge" it "renders the open pull request badge" it "renders the closed pull request badge" it "renders the merged pull request badge" it "renders the draft pull request badge" it "renders the closed pull request badge for a closed draft pull request"
  112. None
  113. 7 examples, 7 failures

  114. None
  115. # app/components/issues/badge.rb module Issues class Badge end end

  116. API

  117. <%= render Issues::Badge, issue: issue, pull_request: issue.pull_request %>

  118. module Issues class Badge def html <<-erb <div class="State State--green">

    #{octicon('issue-opened')} Open </div> erb end end end
  119. 'Issues::Badge' is not an ActiveModel-compatible object.

  120. class ActionView::Base module RenderMonkeyPatch def render(component, *_args) return super unless

    component == Issues::Badge component.new.html end end prepend RenderMonkeyPatch end
  121. undefined method 'octicon'

  122. None
  123. module Issues class Badge def html <<-erb <div class="State State--green">

    #{octicon('issue-opened')} Open </div> erb end end end
  124. module Issues class Badge include OcticonsHelper def html <<-erb <div

    class="State State--green"> #{octicon('issue-opened')} Open </div> erb end end end
  125. Expected element matching ".State.State--green", found 0

  126. &lt;div class=&quot;State State--green&quot;...

  127. module Issues class Badge include OcticonsHelper def html eval( "output_buffer

    = ActionView::OutputBuffer.new;" + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end def template <<-erb <div class="State State--green"> #{octicon('issue-opened')} Open </div> erb end end end
  128. None
  129. it "renders the closed issue badge" do create(:issue, :closed) get

    :index assert_select(".State.State--red") assert_select(".octicon-issue-closed") assert_includes(response.body, "Closed") end
  130. Expected element matching ".State.State--red", found 0

  131. <%= render Issues::Badge, issue: issue, pull_request: issue.pull_request %>

  132. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component == Issues::Badge component.new.html end end prepend RenderMonkeyPatch end
  133. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component == Issues::Badge component.new(*args).html end end prepend RenderMonkeyPatch end
  134. module Issues class Badge include OcticonsHelper def initialize(issue:, pull_request: nil)

    @issue = issue, @pull_request = pull_request end def html; end def template; end end end
  135. module Issues class Badge include OcticonsHelper def initialize; end def

    html; end def template <<-erb <% if @issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% else %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %> erb end end end
  136. None
  137. Implicit Arguments

  138. None
  139. def template <<-erb <% if @pull_request && @pull_request.merged? %> <div

    class="State State--purple"> <%= octicon('git-merge') %> Merged </div> <% elsif @pull_request && @pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif @pull_request && @pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% elsif @pull_request %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% elsif @issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% else %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %> erb end
  140. None
  141. <% if @pull_request && @pull_request.merged? %> <div class="State State--purple"> <%=

    octicon('git-merge') %> Merged </div> <% elsif @pull_request && @pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif @pull_request && @pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% elsif @pull_request %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% elsif @issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %> Closed </div> <% else %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %>
  142. <%= render Issues::Badge, issue: issue, pull_request: issue.pull_request %>

  143. <% if issue.pull_request %> <%= render PullRequests::Badge, pull_request: issue.pull_request %>

    <% else %> <%= render Issues::Badge, issue: issue %> <% end %>
  144. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component == Issues::Badge component.new(*args).html end end prepend RenderMonkeyPatch end
  145. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    [Issues::Badge, PullRequests::Badge].include?(component) component.new(*args).html end end prepend RenderMonkeyPatch end
  146. None
  147. None
  148. <% if @issue.closed? %> <div class="State State--red"> <%= octicon('issue-closed') %>

    Closed </div> <% else %> <div class="State State--green"> <%= octicon('issue-opened') %> Open </div> <% end %>
  149. <% if @pull_request && @pull_request.merged? %> <div class="State State--purple"> <%=

    octicon('git-merge') %> Merged </div> <% elsif @pull_request && @pull_request.closed? %> <div class="State State--red"> <%= octicon('git-pull-request') %> Closed </div> <% elsif @pull_request && @pull_request.draft? %> <div class="State"> <%= octicon('git-pull-request') %> Draft </div> <% else %> <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div> <% end %>
  150. <div class="State State--green"> <%= octicon('git-pull-request') %> Open </div>

  151. <%= render Primer::State, color: :green do %> <%= octicon('git-pull-request') %>

    Open <% end %>
  152. module Primer class State end end

  153. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    [Issues::Badge, PullRequests::Badge].include?(component) component.new(*args).html end end prepend RenderMonkeyPatch end
  154. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    [Issues::Badge, PullRequests::Badge, Primer::State].include?(component) component.new(*args).html end end prepend RenderMonkeyPatch end
  155. module ActionView class Component < ActionView::Base end end

  156. module Issues class Badge < ActionView::Component end end module PullRequests

    class Badge < ActionView::Component end end module Primer class State < ActionView::Component end end
  157. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    [Issues::Badge, PullRequests::Badge, Primer::State].include?(component) component.new(*args).html end end prepend RenderMonkeyPatch end
  158. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component < ActionView::Component component.new(*args).html end end prepend RenderMonkeyPatch end
  159. module Issues class Badge def html eval( "output_buffer = ActionView::OutputBuffer.new;

    " + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end end end
  160. module ActionView class Component < ActionView::Base def html eval( "output_buffer

    = ActionView::OutputBuffer.new; " + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end end end
  161. None
  162. <%= render Primer::State, color: :green do %> <%= octicon('git-pull-request') %>

    Open <% end %>
  163. None
  164. it('should render the closed issue badge', function() { expect(shallow(<IssueBadge props={{

    issue: { isClosed: true }}} />). contains(<div className="State State--red">Closed</div>)).toBe(true); });
  165. it "renders content passed to it as a block" do

    result = render_string("<%= render Primer::State do %>content<% end %>") assert_includes result.css(".State.State--green").text, "content" end
  166. def render_string(string) html = ApplicationController.new.view_context.render(inline: string) Nokogiri::HTML(html) end

  167. no implicit conversion of Class into Hash

  168. def render_string(string) html = ApplicationController.new.view_context.render(inline: string) Nokogiri::HTML(html) end

  169. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component < ActionView::Component component.new(*args).html end end prepend RenderMonkeyPatch end
  170. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component.is_a?(Class) && component < ActionView::Component component.new(*args).html end end prepend RenderMonkeyPatch end
  171. Expected " " to include "content".

  172. <%= render Issues::Badge, color: :green do %> <%= octicon('issue-opened') %>

    Open <% end %>
  173. class ActionView::Base module RenderMonkeyPatch def render(component, *args) return super unless

    component.is_a?(Class) && component < ActionView::Component component.new(*args).html end end prepend RenderMonkeyPatch end
  174. class ActionView::Base module RenderMonkeyPatch def render(component, *args, &block) return super

    unless component.is_a?(Class) && component < ActionView::Component component.new(*args).html end end prepend RenderMonkeyPatch end
  175. class ActionView::Base module RenderMonkeyPatch def render(component, *args, &block) return super

    unless component.is_a?(Class) && component < ActionView::Component component.new(*args).html end end prepend RenderMonkeyPatch end
  176. class ActionView::Base module RenderMonkeyPatch def render(component, *args, &block) return super

    unless component.is_a?(Class) && component < ActionView::Component instance = component.new(*args) instance.content = self.capture(&block) if block_given? instance.html end end prepend RenderMonkeyPatch end
  177. module ActionView class Component < ActionView::Base def html eval( "output_buffer

    = ActionView::OutputBuffer.new; " + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end end end
  178. module ActionView class Component < ActionView::Base attr_accessor :content def html

    eval( "output_buffer = ActionView::OutputBuffer.new; " + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end end end
  179. module Primer class State < ActionView::Component def template <<-erb <div

    class="State State--green"> </div> erb end end end
  180. module Primer class State < ActionView::Component def template <<-erb <div

    class="State State--green"> <%= content %> </div> erb end end end
  181. None
  182. module Primer class State < ActionView::Component def template <<-erb <div

    class="State State--green"> <%= content %> </div> erb end end end
  183. module Primer class State < ActionView::Component def initialize(color:) @color =

    color end def template <<-erb <div class="State State--green"> <%= content %> </div> erb end end end
  184. None
  185. None
  186. module Primer class State < ActionView::Component def initialize(color:) @color =

    color end def template <<-erb <div class="State State--green"> <%= content %> </div> erb end end end
  187. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze def initialize; end def template; end end end
  188. it "raises an error when color is not one of

    valid values" do exception = assert_raises ActionView::Template::Error do render_string("<%= render Primer::State, color: :chartreuse do %>foo<% end %>") end assert_includes exception.message, "Color is not included in the list" end
  189. ActionView::Template::Error expected but nothing was raised.

  190. None
  191. ActiveModel::Validation

  192. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze def initialize; end def template; end end end
  193. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze validates :color, inclusion: { in: COLOR_CLASS_MAPPINGS.keys } def initialize; end def template; end end end
  194. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze attr_reader :color validates :color, inclusion: { in: COLOR_CLASS_MAPPINGS.keys } def initialize; end def template; end end end
  195. module ActionView class Component < ActionView::Base attr_accessor :content def html

    eval( "output_buffer = ActionView::OutputBuffer.new; " + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end end end
  196. module ActionView class Component < ActionView::Base include ActiveModel::Validations attr_accessor :content

    def html eval( "output_buffer = ActionView::OutputBuffer.new; " + ActionView::Template::Handlers::ERB.erb_implementation.new(template, trim: true).src ) end end end
  197. class ActionView::Base module RenderMonkeyPatch def render(component, *args, &block) return super

    unless component.is_a?(Class) && component < ActionView::Component instance = component.new(*args) instance.content = self.capture(&block) if block_given? instance.render end end prepend RenderMonkeyPatch end
  198. class ActionView::Base module RenderMonkeyPatch def render(component, *args, &block) return super

    unless component.is_a?(Class) && component < ActionView::Component instance = component.new(*args) instance.content = self.capture(&block) if block_given? instance.validate! instance.html end end prepend RenderMonkeyPatch end
  199. None
  200. it "assigns the correct CSS class for color" do result

    = render_string("<%= render Primer::State, color: :purple do %>content<% end %>") assert result.css(".State.State--purple").any? end
  201. Expected false to be truthy.

  202. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze attr_reader :color validates :color, inclusion: { in: COLOR_CLASS_MAPPINGS.keys } def template <<-erb <div class="State State--green"> <%= content %> </div> erb end end end
  203. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze attr_reader :color validates :color, inclusion: { in: COLOR_CLASS_MAPPINGS.keys } def template <<-erb <div class="State State--green"> <%= content %> </div> erb end def class_name COLOR_CLASS_MAPPINGS[color] end end end
  204. module Primer class State < ActionView::Component COLOR_CLASS_MAPPINGS = { default:

    "", green: "State--green", red: "State--red", purple: "State--purple", }.freeze attr_reader :color validates :color, inclusion: { in: COLOR_CLASS_MAPPINGS.keys } def template <<-erb <div class="State <%= class_name %>"> <%= content %> </div> erb end def class_name COLOR_CLASS_MAPPINGS[color] end end end
  205. None
  206. None
  207. None
  208. it "raises an error when title is not present" do

    exception = assert_raises ActionView::Template::Error do render_string("<%= render Primer::State, title: '' do %>foo<% end %>") end assert_includes exception.message, "Title can't be blank" end
  209. Expected false to be truthy.

  210. module Primer class State < ActionView::Component attr_reader :title validates :title,

    presence: true end end
  211. None
  212. missing keyword: title

  213. module Issues class Badge < ActionView::Component def template <<-erb <%

    if @issue.closed? %> <%= render Primer::State, color: :red do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  214. module Issues class Badge < ActionView::Component def template <<-erb <%

    if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  215. None
  216. Data Flow

  217. N + 1

  218. module Issues class Badge < ActionView::Component include OcticonsHelper def initialize(issue:)

    @issue = issue end def template <<-erb <% if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  219. class Issue < ApplicationRecord def closed? state == "closed" end

    end
  220. module Issues class Badge < ActionView::Component include OcticonsHelper def initialize(issue:)

    @issue = issue end def template <<-erb <% if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  221. module Issues class Badge < ActionView::Component include OcticonsHelper def initialize(state:)

    @state = state end def template <<-erb <% if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  222. module Issues class Badge < ActionView::Component include OcticonsHelper attr_reader :state

    validates :state, inclusion: { in: [:open, :closed] } def initialize(state:) @state = state end def template <<-erb <% if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  223. module Issues class Badge < ActionView::Component def template <<-erb <%

    if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  224. module Issues class Badge < ActionView::Component include OcticonsHelper STATES =

    { open: { color: :green, octicon_name: "issue-opened", label: "Open" }, closed: { color: :red, octicon_name: "issue-closed", label: "Closed" } }.freeze attr_reader :state validates :state, inclusion: { in: STATES.keys } def initialize; end def template; end end end
  225. module Issues class Badge < ActionView::Component def template <<-erb <%

    if @issue.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('issue-closed') %> Closed <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('issue-opened') %> Open <% end %> <% end %> erb end end end
  226. module Issues class Badge < ActionView::Component def template <<-erb <%=

    render Primer::State, color: color, title: "Status: #{label}" do %> <%= octicon(octicon_name) %> <%= label %> <% end %> erb end def color STATES[state][:color] end def octicon_name STATES[state][:octicon_name] end def label STATES[state][:label] end end end
  227. None
  228. None
  229. module PullRequests class Badge < ActionView::Component def template <<-erb <%

    if pull_request.merged? %> <% elsif pull_request.closed? %> <% elsif pull_request.draft? %> <% else %> <% end %> erb end end end
  230. class PullRequest < ApplicationRecord def state return :open if open?

    return :merged if merged? return :closed if closed? end # autogenerated def draft?; end end
  231. it "renders the draft state" do result = render_string("<%= render

    PullRequests::Badge, state: :open, is_draft: true %>") assert_includes result.text, "Draft" assert result.css("[title='Status: Draft']").any? assert result.css(".octicon-git-pull-request").any? end it "renders the closed draft state" it "renders the merged state" it "renders the closed state" it "renders the open state"
  232. missing keyword: pull_request

  233. module PullRequests class Badge < ActionView::Component def initialize(pull_request:) @pull_request =

    pull_request end end end
  234. module PullRequests class Badge < ActionView::Component def initialize(state:, is_draft:) @state,

    @is_draft = state, is_draft end end end
  235. module PullRequests class Badge < ActionView::Component def template <<-erb <%

    if pull_request.merged? %> <%= render Primer::State, color: :purple, title: "Status: Merged" do %> <%= octicon('git-merge') %> Merged <% end %> <% elsif pull_request.closed? %> <%= render Primer::State, color: :red, title: "Status: Closed" do %> <%= octicon('git-pull-request') %> Closed <% end %> <% elsif pull_request.draft? %> <%= render Primer::State, color: :default, title: "Status: Draft" do %> <%= octicon('git-pull-request') %> Draft <% end %> <% else %> <%= render Primer::State, color: :green, title: "Status: Open" do %> <%= octicon('git-pull-request') %> Open <% end %> <% end %> erb end end end
  236. module PullRequests class Badge < ActionView::Component def template <<-erb <%=

    render Primer::State, title: title, color: color do %> <%= octicon(octicon_name) %> <%= label %> <% end %> erb end def title; end def color; end def octicon_name; end def label; end end end
  237. class IssueBadge extends React.Component { render() { return ( <div

    className={ "State " + this._stateClass() }> <i className={this._icon()} /> {this._label()} </div> ) } _icon() { ... } _stateClass() { ... } _label() { ... } }
  238. None
  239. None
  240. Data Flow

  241. Values > Objects

  242. Code Coverage

  243. None
  244. None
  245. Production?

  246. ! Mid-March

  247. None
  248. None
  249. Implementation

  250. missing keyword: title

  251. Standards

  252. Reusability

  253. None
  254. Performance

  255. 6s

  256. 25ms

  257. 240x

  258. None
  259. .25s vs. 1m

  260. None
  261. Creativity

  262. Imagine

  263. Something

  264. Nothing

  265. New ideas

  266. Combining

  267. Changing

  268. Reapplying

  269. Existing Ideas

  270. None
  271. None
  272. Testing

  273. Code Coverage

  274. Data Flow

  275. Standards

  276. ActionView::Component

  277. Testing

  278. Code Coverage

  279. Data Flow

  280. Standards

  281. MvC

  282. MVC

  283. Thanks

  284. Q & A Slides & source code: hawksley.org