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. 3.
  2. 4.
  3. 6.
  4. 8.
  5. 10.
  6. 11.
  7. 14.
  8. 15.
  9. 16.
  10. 18.
  11. 19.
  12. 21.
  13. 23.
  14. 24.
  15. 25.
  16. 26.
  17. 27.
  18. 30.
  19. 38.
  20. 39.

    ERB

  21. 41.
  22. 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 %>
  23. 46.
  24. 47.
  25. 48.
  26. 49.
  27. 50.
  28. 51.
  29. 52.
  30. 53.

    class Issue < ApplicationRecord belongs_to :pull_request end class PullRequest <

    ApplicationRecord has_one :issue, inverse_of: :pull_request end
  31. 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 %>
  32. 55.
  33. 56.
  34. 57.
  35. 59.

    209

  36. 60.

    556

  37. 61.
  38. 62.
  39. 63.
  40. 64.

    6s

  41. 65.
  42. 66.
  43. 68.
  44. 69.
  45. 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 %>
  46. 72.
  47. 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 %>
  48. 77.
  49. 78.
  50. 79.
  51. 80.
  52. 81.
  53. 82.
  54. 83.
  55. 85.
  56. 87.
  57. 88.

    MVC

  58. 89.

    MvC

  59. 90.
  60. 92.

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

    } } React.render(<Greeting name="World" />, document.getElementById('example'));
  61. 93.

    class IssueBadge extends React.Component { render() { return ( <div

    className={ "State " + this._stateClass() }> <i className={this._icon()} /> {this._label()} </div> ) } _icon() { ... } _stateClass() { ... } _label() { ... } }
  62. 94.
  63. 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 }), };
  64. 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() { ... } }
  65. 97.
  66. 99.
  67. 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); });
  68. 101.
  69. 102.
  70. 103.
  71. 104.
  72. 105.
  73. 106.
  74. 107.
  75. 108.
  76. 109.
  77. 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 %>
  78. 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"
  79. 112.
  80. 114.
  81. 116.

    API

  82. 118.

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

    #{octicon('issue-opened')} Open </div> erb end end end
  83. 120.

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

    component == Issues::Badge component.new.html end end prepend RenderMonkeyPatch end
  84. 122.
  85. 123.

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

    #{octicon('issue-opened')} Open </div> erb end end end
  86. 124.

    module Issues class Badge include OcticonsHelper def html <<-erb <div

    class="State State--green"> #{octicon('issue-opened')} Open </div> erb end end end
  87. 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
  88. 128.
  89. 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
  90. 132.

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

    component == Issues::Badge component.new.html end end prepend RenderMonkeyPatch end
  91. 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
  92. 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
  93. 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
  94. 136.
  95. 138.
  96. 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
  97. 140.
  98. 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 %>
  99. 143.

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

    <% else %> <%= render Issues::Badge, issue: issue %> <% end %>
  100. 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
  101. 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
  102. 146.
  103. 147.
  104. 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 %>
  105. 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 %>
  106. 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
  107. 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
  108. 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
  109. 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
  110. 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
  111. 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
  112. 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
  113. 161.
  114. 163.
  115. 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); });
  116. 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
  117. 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
  118. 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
  119. 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
  120. 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
  121. 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
  122. 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
  123. 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
  124. 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
  125. 179.

    module Primer class State < ActionView::Component def template <<-erb <div

    class="State State--green"> </div> erb end end end
  126. 180.

    module Primer class State < ActionView::Component def template <<-erb <div

    class="State State--green"> <%= content %> </div> erb end end end
  127. 181.
  128. 182.

    module Primer class State < ActionView::Component def template <<-erb <div

    class="State State--green"> <%= content %> </div> erb end end end
  129. 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
  130. 184.
  131. 185.
  132. 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
  133. 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
  134. 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
  135. 190.
  136. 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
  137. 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
  138. 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
  139. 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
  140. 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
  141. 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
  142. 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
  143. 199.
  144. 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
  145. 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
  146. 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
  147. 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
  148. 205.
  149. 206.
  150. 207.
  151. 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
  152. 211.
  153. 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
  154. 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
  155. 215.
  156. 216.
  157. 217.
  158. 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
  159. 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
  160. 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
  161. 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
  162. 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
  163. 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
  164. 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
  165. 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
  166. 227.
  167. 228.
  168. 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
  169. 230.

    class PullRequest < ApplicationRecord def state return :open if open?

    return :merged if merged? return :closed if closed? end # autogenerated def draft?; end end
  170. 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"
  171. 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
  172. 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
  173. 237.

    class IssueBadge extends React.Component { render() { return ( <div

    className={ "State " + this._stateClass() }> <i className={this._icon()} /> {this._label()} </div> ) } _icon() { ... } _stateClass() { ... } _label() { ... } }
  174. 238.
  175. 239.
  176. 240.
  177. 243.
  178. 244.
  179. 247.
  180. 248.
  181. 251.
  182. 253.
  183. 255.

    6s

  184. 256.
  185. 257.
  186. 258.
  187. 260.
  188. 261.
  189. 262.
  190. 263.
  191. 264.
  192. 265.
  193. 266.
  194. 267.
  195. 268.
  196. 270.
  197. 271.
  198. 272.
  199. 274.
  200. 275.
  201. 277.
  202. 279.
  203. 280.
  204. 281.

    MvC

  205. 282.

    MVC

  206. 283.