actionview-component 1.6.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/README.md +168 -0
- data/lib/action_view/component.rb +1 -0
- data/lib/action_view/component/base.rb +70 -30
- data/lib/action_view/component/railtie.rb +1 -3
- data/lib/action_view/component/template_error.rb +11 -0
- data/lib/action_view/component/version.rb +2 -2
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: '085434585e1e3fa1493bbc7bd90439b1645015acd66a713a33545c0eb4677dc4'
         | 
| 4 | 
            +
              data.tar.gz: 34ec8d8f390471689fbd1085c94885b74c4f9a1b0b7d9b78650dae506520dd13
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 89e3c7d8b6c1d67a29f2ad67a032e424c42f57dd719d5b8cbde01b167385d9d2aeb5feb8fd77fcd9284e0a33da828b935edeebbb758c7cd68951178eea2d3b14
         | 
| 7 | 
            +
              data.tar.gz: bc76f28233c72f7d5ff3a818429e8c763649db3ec46e406b554070cd456b73ae923905ecb94d3b37949ca5c6b0960890594a341e72a9755343462cf4c5c24cff
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    
    
        data/Gemfile.lock
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -199,6 +199,174 @@ An error will be raised: | |
| 199 199 |  | 
| 200 200 | 
             
            `ActiveModel::ValidationError: Validation failed: Title can't be blank`
         | 
| 201 201 |  | 
| 202 | 
            +
            #### Content Areas
         | 
| 203 | 
            +
             | 
| 204 | 
            +
             | 
| 205 | 
            +
            A component can declare additional content areas to be rendered in the component. For example:
         | 
| 206 | 
            +
             | 
| 207 | 
            +
            `app/components/modal_component.rb`:
         | 
| 208 | 
            +
            ```ruby
         | 
| 209 | 
            +
            class ModalComponent < ActionView::Component::Base
         | 
| 210 | 
            +
              validates :user, :header, :body, presence: true
         | 
| 211 | 
            +
             | 
| 212 | 
            +
              with_content_areas :header, :body
         | 
| 213 | 
            +
             | 
| 214 | 
            +
              def initialize(user:)
         | 
| 215 | 
            +
                @user = user
         | 
| 216 | 
            +
              end
         | 
| 217 | 
            +
            end
         | 
| 218 | 
            +
            ```
         | 
| 219 | 
            +
             | 
| 220 | 
            +
            `app/components/modal_component.html.erb`:
         | 
| 221 | 
            +
            ```erb
         | 
| 222 | 
            +
            <div class="modal">
         | 
| 223 | 
            +
              <div class="header"><%= header %></div>
         | 
| 224 | 
            +
              <div class="body"><%= body %>"></div>
         | 
| 225 | 
            +
            </div>
         | 
| 226 | 
            +
            ```
         | 
| 227 | 
            +
             | 
| 228 | 
            +
            We can render it in a view as:
         | 
| 229 | 
            +
             | 
| 230 | 
            +
            ```erb
         | 
| 231 | 
            +
            <%= render(ModalComponent, user: {name: 'Jane'}) do |component| %>
         | 
| 232 | 
            +
              <% component.with(:header) do %>
         | 
| 233 | 
            +
                  Hello <%= user[:name] %>
         | 
| 234 | 
            +
                <% end %>
         | 
| 235 | 
            +
              <% component.with(:body) do %>
         | 
| 236 | 
            +
                <p>Have a great day.</p>
         | 
| 237 | 
            +
              <% end %>
         | 
| 238 | 
            +
            <% end %>
         | 
| 239 | 
            +
            ```
         | 
| 240 | 
            +
             | 
| 241 | 
            +
            Which returns:
         | 
| 242 | 
            +
             | 
| 243 | 
            +
            ```html
         | 
| 244 | 
            +
            <div class="modal">
         | 
| 245 | 
            +
              <div class="header">Hello Jane</div>
         | 
| 246 | 
            +
              <div class="body"><p>Have a great day.</p></div>
         | 
| 247 | 
            +
            </div>
         | 
| 248 | 
            +
            ```
         | 
| 249 | 
            +
             | 
| 250 | 
            +
            Content for content areas can be passed as arguments to the render method or as named blocks passed to the `with` method.
         | 
| 251 | 
            +
            This allows a few different combinations of ways to render the component:
         | 
| 252 | 
            +
             | 
| 253 | 
            +
            ##### Required render argument optionally overridden or wrapped by a named block
         | 
| 254 | 
            +
             | 
| 255 | 
            +
            `app/components/modal_component.rb`:
         | 
| 256 | 
            +
            ```ruby
         | 
| 257 | 
            +
            class ModalComponent < ActionView::Component::Base
         | 
| 258 | 
            +
              validates :header, :body, presence: true
         | 
| 259 | 
            +
             | 
| 260 | 
            +
              with_content_areas :header, :body
         | 
| 261 | 
            +
             | 
| 262 | 
            +
              def initialize(header:)
         | 
| 263 | 
            +
                @header = header
         | 
| 264 | 
            +
              end
         | 
| 265 | 
            +
            end
         | 
| 266 | 
            +
            ```
         | 
| 267 | 
            +
             | 
| 268 | 
            +
            ```erb
         | 
| 269 | 
            +
            <%= render(ModalComponent, header: "Hi!") do |component| %>
         | 
| 270 | 
            +
              <% help_enabled? && component.with(:header) do %>
         | 
| 271 | 
            +
                <span class="help_icon"><%= component.header %></span>
         | 
| 272 | 
            +
              <% end %>
         | 
| 273 | 
            +
              <% component.with(:body) do %>
         | 
| 274 | 
            +
                <p>Have a great day.</p>
         | 
| 275 | 
            +
              <% end %>
         | 
| 276 | 
            +
            <% end %>
         | 
| 277 | 
            +
            ```
         | 
| 278 | 
            +
             | 
| 279 | 
            +
            ##### Required argument passed by render argument or by named block
         | 
| 280 | 
            +
             | 
| 281 | 
            +
            `app/components/modal_component.rb`:
         | 
| 282 | 
            +
            ```ruby
         | 
| 283 | 
            +
            class ModalComponent < ActionView::Component::Base
         | 
| 284 | 
            +
              validates :header, :body, presence: true
         | 
| 285 | 
            +
             | 
| 286 | 
            +
              with_content_areas :header, :body
         | 
| 287 | 
            +
             | 
| 288 | 
            +
              def initialize(header: nil)
         | 
| 289 | 
            +
                @header = header
         | 
| 290 | 
            +
              end
         | 
| 291 | 
            +
            end
         | 
| 292 | 
            +
            ```
         | 
| 293 | 
            +
             | 
| 294 | 
            +
            `app/views/render_arg.html.erb`:
         | 
| 295 | 
            +
            ```erb
         | 
| 296 | 
            +
            <%= render(ModalComponent, header: "Hi!") do |component| %>
         | 
| 297 | 
            +
              <% component.with(:body) do %>
         | 
| 298 | 
            +
                <p>Have a great day.</p>
         | 
| 299 | 
            +
              <% end %>
         | 
| 300 | 
            +
            <% end %>
         | 
| 301 | 
            +
            ```
         | 
| 302 | 
            +
             | 
| 303 | 
            +
            `app/views/with_block.html.erb`:
         | 
| 304 | 
            +
            ```erb
         | 
| 305 | 
            +
            <%= render(ModalComponent) do |component| %>
         | 
| 306 | 
            +
              <% component.with(:header) do %>
         | 
| 307 | 
            +
                <span class="help_icon">Hello</span> 
         | 
| 308 | 
            +
              <% end %>
         | 
| 309 | 
            +
              <% component.with(:body) do %>
         | 
| 310 | 
            +
                <p>Have a great day.</p>
         | 
| 311 | 
            +
              <% end %>
         | 
| 312 | 
            +
            <% end %>
         | 
| 313 | 
            +
            ```
         | 
| 314 | 
            +
             | 
| 315 | 
            +
            ##### Optional argument passed by render argument, by named block, or neither
         | 
| 316 | 
            +
             | 
| 317 | 
            +
            `app/components/modal_component.rb`:
         | 
| 318 | 
            +
            ```ruby
         | 
| 319 | 
            +
            class ModalComponent < ActionView::Component::Base
         | 
| 320 | 
            +
              validates :body, presence: true
         | 
| 321 | 
            +
             | 
| 322 | 
            +
              with_content_areas :header, :body
         | 
| 323 | 
            +
             | 
| 324 | 
            +
              def initialize(header: nil)
         | 
| 325 | 
            +
                @header = header
         | 
| 326 | 
            +
              end
         | 
| 327 | 
            +
            end
         | 
| 328 | 
            +
            ```
         | 
| 329 | 
            +
             | 
| 330 | 
            +
            `app/components/modal_component.html.erb`:
         | 
| 331 | 
            +
            ```erb
         | 
| 332 | 
            +
            <div class="modal">
         | 
| 333 | 
            +
              <% if header %>
         | 
| 334 | 
            +
                <div class="header"><%= header %></div>
         | 
| 335 | 
            +
              <% end %>
         | 
| 336 | 
            +
              <div class="body"><%= body %>"></div>
         | 
| 337 | 
            +
            </div>
         | 
| 338 | 
            +
            ```
         | 
| 339 | 
            +
             | 
| 340 | 
            +
            `app/views/render_arg.html.erb`:
         | 
| 341 | 
            +
            ```erb
         | 
| 342 | 
            +
            <%= render(ModalComponent, header: "Hi!") do |component| %>
         | 
| 343 | 
            +
              <% component.with(:body) do %>
         | 
| 344 | 
            +
                <p>Have a great day.</p>
         | 
| 345 | 
            +
              <% end %>
         | 
| 346 | 
            +
            <% end %>
         | 
| 347 | 
            +
            ```
         | 
| 348 | 
            +
             | 
| 349 | 
            +
            `app/views/with_block.html.erb`:
         | 
| 350 | 
            +
            ```erb
         | 
| 351 | 
            +
            <%= render(ModalComponent) do |component| %>
         | 
| 352 | 
            +
              <% component.with(:header) do %>
         | 
| 353 | 
            +
                <span class="help_icon">Hello</span> 
         | 
| 354 | 
            +
              <% end %>
         | 
| 355 | 
            +
              <% component.with(:body) do %>
         | 
| 356 | 
            +
                <p>Have a great day.</p>
         | 
| 357 | 
            +
              <% end %>
         | 
| 358 | 
            +
            <% end %>
         | 
| 359 | 
            +
            ```
         | 
| 360 | 
            +
             | 
| 361 | 
            +
            `app/views/no_header.html.erb`:
         | 
| 362 | 
            +
            ```erb
         | 
| 363 | 
            +
            <%= render(ModalComponent) do |component| %>
         | 
| 364 | 
            +
              <% component.with(:body) do %>
         | 
| 365 | 
            +
                <p>Have a great day.</p>
         | 
| 366 | 
            +
              <% end %>
         | 
| 367 | 
            +
            <% end %>
         | 
| 368 | 
            +
            ```
         | 
| 369 | 
            +
             | 
| 202 370 | 
             
            ### Testing
         | 
| 203 371 |  | 
| 204 372 | 
             
            Components are unit tested directly. The `render_inline` test helper wraps the result in `Nokogiri.HTML`, allowing us to test the component above as:
         | 
| @@ -11,6 +11,9 @@ module ActionView | |
| 11 11 |  | 
| 12 12 | 
             
                  delegate :form_authenticity_token, :protect_against_forgery?, to: :helpers
         | 
| 13 13 |  | 
| 14 | 
            +
                  class_attribute :content_areas, default: []
         | 
| 15 | 
            +
                  self.content_areas = [] # default doesn't work until Rails 5.2
         | 
| 16 | 
            +
             | 
| 14 17 | 
             
                  # Entrypoint for rendering components. Called by ActionView::Base#render.
         | 
| 15 18 | 
             
                  #
         | 
| 16 19 | 
             
                  # view_context: ActionView context from calling view
         | 
| @@ -37,7 +40,7 @@ module ActionView | |
| 37 40 | 
             
                  # <span title="greeting">Hello, world!</span>
         | 
| 38 41 | 
             
                  #
         | 
| 39 42 | 
             
                  def render_in(view_context, *args, &block)
         | 
| 40 | 
            -
                    self.class.compile
         | 
| 43 | 
            +
                    self.class.compile!
         | 
| 41 44 | 
             
                    @view_context = view_context
         | 
| 42 45 | 
             
                    @view_renderer ||= view_context.view_renderer
         | 
| 43 46 | 
             
                    @lookup_context ||= view_context.lookup_context
         | 
| @@ -47,7 +50,8 @@ module ActionView | |
| 47 50 | 
             
                    old_current_template = @current_template
         | 
| 48 51 | 
             
                    @current_template = self
         | 
| 49 52 |  | 
| 50 | 
            -
                    @content = view_context.capture(&block) if block_given?
         | 
| 53 | 
            +
                    @content = view_context.capture(self, &block) if block_given?
         | 
| 54 | 
            +
             | 
| 51 55 | 
             
                    validate!
         | 
| 52 56 |  | 
| 53 57 | 
             
                    send(self.class.call_method_name(@variant))
         | 
| @@ -87,6 +91,19 @@ module ActionView | |
| 87 91 | 
             
                    @variant
         | 
| 88 92 | 
             
                  end
         | 
| 89 93 |  | 
| 94 | 
            +
                  def with(area, content = nil, &block)
         | 
| 95 | 
            +
                    unless content_areas.include?(area)
         | 
| 96 | 
            +
                      raise ArgumentError.new "Unknown content_area '#{area}' - expected one of '#{content_areas}'"
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    if block_given?
         | 
| 100 | 
            +
                      content = view_context.capture(&block)
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    instance_variable_set("@#{area}".to_sym, content)
         | 
| 104 | 
            +
                    nil
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 90 107 | 
             
                  private
         | 
| 91 108 |  | 
| 92 109 | 
             
                  def request
         | 
| @@ -110,34 +127,40 @@ module ActionView | |
| 110 127 | 
             
                      end
         | 
| 111 128 | 
             
                    end
         | 
| 112 129 |  | 
| 113 | 
            -
                    def has_initializer?
         | 
| 114 | 
            -
                      self.instance_method(:initialize).owner == self
         | 
| 115 | 
            -
                    end
         | 
| 116 | 
            -
             | 
| 117 130 | 
             
                    def source_location
         | 
| 118 | 
            -
                       | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 131 | 
            +
                      @source_location ||=
         | 
| 132 | 
            +
                          begin
         | 
| 133 | 
            +
                            # Require #initialize to be defined so that we can use
         | 
| 134 | 
            +
                            # method#source_location to look up the file name
         | 
| 135 | 
            +
                            # of the component.
         | 
| 136 | 
            +
                            #
         | 
| 137 | 
            +
                            # If we were able to only support Ruby 2.7+,
         | 
| 138 | 
            +
                            # We could just use Module#const_source_location,
         | 
| 139 | 
            +
                            # rendering this unnecessary.
         | 
| 140 | 
            +
                            #
         | 
| 141 | 
            +
                            initialize_method = instance_method(:initialize)
         | 
| 142 | 
            +
                            initialize_method.source_location[0] if initialize_method.owner == self
         | 
| 143 | 
            +
                          end
         | 
| 128 144 | 
             
                    end
         | 
| 129 145 |  | 
| 130 146 | 
             
                    def compiled?
         | 
| 131 147 | 
             
                      @compiled && ActionView::Base.cache_template_loading
         | 
| 132 148 | 
             
                    end
         | 
| 133 149 |  | 
| 150 | 
            +
                    def compile!
         | 
| 151 | 
            +
                      compile(validate: true)
         | 
| 152 | 
            +
                    end
         | 
| 153 | 
            +
             | 
| 134 154 | 
             
                    # Compile templates to instance methods, assuming they haven't been compiled already.
         | 
| 135 155 | 
             
                    # We could in theory do this on app boot, at least in production environments.
         | 
| 136 156 | 
             
                    # Right now this just compiles the first time the component is rendered.
         | 
| 137 | 
            -
                    def compile
         | 
| 157 | 
            +
                    def compile(validate: false)
         | 
| 138 158 | 
             
                      return if compiled?
         | 
| 139 159 |  | 
| 140 | 
            -
                       | 
| 160 | 
            +
                      if template_errors.present?
         | 
| 161 | 
            +
                        raise ActionView::Component::TemplateError.new(template_errors) if validate
         | 
| 162 | 
            +
                        return false
         | 
| 163 | 
            +
                      end
         | 
| 141 164 |  | 
| 142 165 | 
             
                      templates.each do |template|
         | 
| 143 166 | 
             
                        class_eval <<-RUBY, __FILE__, __LINE__ + 1
         | 
| @@ -164,9 +187,18 @@ module ActionView | |
| 164 187 | 
             
                      source_location
         | 
| 165 188 | 
             
                    end
         | 
| 166 189 |  | 
| 190 | 
            +
                    def with_content_areas(*areas)
         | 
| 191 | 
            +
                      if areas.include?(:content)
         | 
| 192 | 
            +
                        raise ArgumentError.new ":content is a reserved content area name. Please use another name, such as ':body'"
         | 
| 193 | 
            +
                      end
         | 
| 194 | 
            +
                      attr_reader *areas
         | 
| 195 | 
            +
                      self.content_areas = areas
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
             | 
| 167 198 | 
             
                    private
         | 
| 168 199 |  | 
| 169 200 | 
             
                    def matching_views_in_source_location
         | 
| 201 | 
            +
                      return [] unless source_location
         | 
| 170 202 | 
             
                      (Dir["#{source_location.sub(/#{File.extname(source_location)}$/, '')}.*{#{ActionView::Template.template_handler_extensions.join(',')}}"] - [source_location])
         | 
| 171 203 | 
             
                    end
         | 
| 172 204 |  | 
| @@ -177,26 +209,34 @@ module ActionView | |
| 177 209 |  | 
| 178 210 | 
             
                          memo << {
         | 
| 179 211 | 
             
                            path: path,
         | 
| 180 | 
            -
                            variant: pieces.second.split("+") | 
| 212 | 
            +
                            variant: pieces.second.split("+").second&.to_sym,
         | 
| 181 213 | 
             
                            handler: pieces.last
         | 
| 182 214 | 
             
                          }
         | 
| 183 215 | 
             
                        end
         | 
| 184 216 | 
             
                    end
         | 
| 185 217 |  | 
| 186 | 
            -
                    def  | 
| 187 | 
            -
                       | 
| 188 | 
            -
                         | 
| 189 | 
            -
             | 
| 218 | 
            +
                    def template_errors
         | 
| 219 | 
            +
                      @template_errors ||=
         | 
| 220 | 
            +
                        begin
         | 
| 221 | 
            +
                          errors = []
         | 
| 222 | 
            +
                          errors << "#{self} must implement #initialize." if source_location.nil?
         | 
| 223 | 
            +
                          errors << "Could not find a template file for #{self}." if templates.empty?
         | 
| 190 224 |  | 
| 191 | 
            -
             | 
| 192 | 
            -
             | 
| 193 | 
            -
             | 
| 225 | 
            +
                          if templates.count { |template| template[:variant].nil? } > 1
         | 
| 226 | 
            +
                            errors << "More than one template found for #{self}. There can only be one default template file per component."
         | 
| 227 | 
            +
                          end
         | 
| 194 228 |  | 
| 195 | 
            -
             | 
| 196 | 
            -
             | 
| 229 | 
            +
                          invalid_variants = templates
         | 
| 230 | 
            +
                                               .group_by { |template| template[:variant] }
         | 
| 231 | 
            +
                                               .map { |variant, grouped| variant if grouped.length > 1 }
         | 
| 232 | 
            +
                                               .compact
         | 
| 233 | 
            +
                                               .sort
         | 
| 197 234 |  | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 235 | 
            +
                          unless invalid_variants.empty?
         | 
| 236 | 
            +
                            errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
         | 
| 237 | 
            +
                          end
         | 
| 238 | 
            +
                          errors
         | 
| 239 | 
            +
                        end
         | 
| 200 240 | 
             
                    end
         | 
| 201 241 |  | 
| 202 242 | 
             
                    def compiled_template(file_path)
         | 
| @@ -35,9 +35,7 @@ module ActionView | |
| 35 35 |  | 
| 36 36 | 
             
                  initializer "action_view_component.eager_load_actions" do
         | 
| 37 37 | 
             
                    ActiveSupport.on_load(:after_initialize) do
         | 
| 38 | 
            -
                      ActionView::Component::Base.descendants.each | 
| 39 | 
            -
                        descendant.compile if descendant.has_initializer? && config.eager_load
         | 
| 40 | 
            -
                      end
         | 
| 38 | 
            +
                      ActionView::Component::Base.descendants.each(&:compile)
         | 
| 41 39 | 
             
                    end
         | 
| 42 40 | 
             
                  end
         | 
| 43 41 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: actionview-component
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1. | 
| 4 | 
            +
              version: 1.7.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - GitHub Open Source
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2020-01- | 
| 11 | 
            +
            date: 2020-01-09 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -150,6 +150,7 @@ files: | |
| 150 150 | 
             
            - lib/action_view/component/previewable.rb
         | 
| 151 151 | 
             
            - lib/action_view/component/railtie.rb
         | 
| 152 152 | 
             
            - lib/action_view/component/render_monkey_patch.rb
         | 
| 153 | 
            +
            - lib/action_view/component/template_error.rb
         | 
| 153 154 | 
             
            - lib/action_view/component/test_case.rb
         | 
| 154 155 | 
             
            - lib/action_view/component/test_helpers.rb
         | 
| 155 156 | 
             
            - lib/action_view/component/version.rb
         |