Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render collections #274

Merged
merged 36 commits into from
Mar 27, 2020
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e1f0bce
Sketch out API for rendering collections
tclem Mar 23, 2020
c8997b1
Final newlines are nice
tclem Mar 23, 2020
698fa7d
Rename template
tclem Mar 24, 2020
0fe965f
Handle :as, fill out test coverage
tclem Mar 24, 2020
7aaef37
Test out not using :as
tclem Mar 24, 2020
d6f435e
Merge remote-tracking branch 'origin/master' into render-collections
tclem Mar 24, 2020
6df4749
Name the ivar item for clarity
tclem Mar 24, 2020
6750a3a
Change up the api to .all(collection: ...)
tclem Mar 25, 2020
6d96256
Move default kw to as_variable
tclem Mar 25, 2020
6a3e54c
Auto generate item name
tclem Mar 25, 2020
492c711
Handle passing collection object directly
tclem Mar 25, 2020
85806b4
Duplicative of ProductCoupon
tclem Mar 25, 2020
a41527b
Just change the listing order
tclem Mar 25, 2020
4b7e367
Move ViewComponent:Collection to a dedicated file
tclem Mar 25, 2020
c872eae
Draft a changelog entry
tclem Mar 25, 2020
2255967
Add myself to the readme and tidy contributors
tclem Mar 25, 2020
606d568
Add space after comma
tclem Mar 25, 2020
9baecd1
Use the component class name directly
tclem Mar 26, 2020
556de9e
More specific asserts for extra arguments
tclem Mar 26, 2020
f897365
ViewComponent::Collection constructor can be private
tclem Mar 26, 2020
3793dd3
Be defensive against component changing the args hash
tclem Mar 26, 2020
f294f99
s/all/with_collection
tclem Mar 26, 2020
44f326d
Iterate on the api
tclem Mar 26, 2020
9f1bc8a
Bring tests up-to-date
tclem Mar 26, 2020
84a6495
Express these as list items
tclem Mar 26, 2020
6c3e7ee
Wire up an integration test
tclem Mar 26, 2020
7196bef
html_safe is required for rendered content to show up
tclem Mar 26, 2020
1b186fe
Different versions of ruby show slightly different errors here
tclem Mar 26, 2020
340122b
Update api in changelog entry
tclem Mar 26, 2020
4364c69
Bump version to 2.1.0
tclem Mar 26, 2020
6612f8b
No need to use ivars in these tests
tclem Mar 26, 2020
5a13abe
Update test/view_component/view_component_test.rb
tclem Mar 26, 2020
a1b85fc
Revert "Bump version to 2.1.0"
tclem Mar 26, 2020
93b3cdc
Add some documentation to the readme
tclem Mar 26, 2020
b09ca46
Didn't mean to replace these
tclem Mar 27, 2020
c32d066
Update CHANGELOG.md
joelhawksley Mar 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# master

# v2.1.0

* Support rendering collection (e.g., `render(MyComponent.all(@items))`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just dropping a note here to make sure we update this to reflect whatever API we land on ❤️


*Tim Clem, Joel Hawksley*
joelhawksley marked this conversation as resolved.
Show resolved Hide resolved

# v2.0.0

* Move to `ViewComponent` namespace, removing all references to `ActionView`.
Expand Down
13 changes: 4 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,15 +446,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/github
|@mellowfish|@horacio|@dukex|@dark-panda|@smashwilson|
tclem marked this conversation as resolved.
Show resolved Hide resolved
|Spring Hill, TN|Buenos Aires|São Paulo||Gambrills, MD|

|<img src="https://avatars.githubusercontent.com/blakewilliams?s=256" alt="blakewilliams" width="128" />|
|:---:|
|@blakewilliams|
|Boston, MA|

|<img src="https://avatars.githubusercontent.com/seanpdoyle?s=256" alt="seanpdoyle" width="128" />|
|:---:|
|@seanpdoyle|
|New York, NY|
|<img src="https://avatars.githubusercontent.com/blakewilliams?s=256" alt="blakewilliams" width="128" />|<img src="https://avatars.githubusercontent.com/seanpdoyle?s=256" alt="seanpdoyle" width="128" />|<img src="https://avatars.githubusercontent.com/tclem?s=256" alt="tclem" width="128" />|
|:---:|:---:|:---:|
|@blakewilliams|@seanpdoyle|@tclem|
|Boston, MA|New York, NY|San Francisco, CA|

## License

Expand Down
15 changes: 15 additions & 0 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "action_view"
require "active_support/configurable"
require "view_component/collection"
require "view_component/previewable"

module ViewComponent
Expand All @@ -14,6 +15,11 @@ class Base < ActionView::Base
class_attribute :content_areas, default: []
self.content_areas = [] # default doesn't work until Rails 5.2

# Render a component collection.
def self.with_collection(*args)
Collection.new(self, *args)
end

# Entrypoint for rendering components.
#
# view_context: ActionView context from calling view
Expand Down Expand Up @@ -213,6 +219,15 @@ def with_content_areas(*areas)
self.content_areas = areas
end

# Support overriding this component's collection parameter name
def with_collection_parameter(param)
@with_collection_parameter = param
end

def collection_parameter_name
(@with_collection_parameter || name.demodulize.underscore.chomp("_component")).to_sym
end

private

def matching_views_in_source_location
Expand Down
30 changes: 30 additions & 0 deletions lib/view_component/collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

# frozen_string_literal: true

module ViewComponent
class Collection
def render_in(view_context, &block)
as = @component.collection_parameter_name

@collection.map do |item|
@component.new(@options.merge(as => item)).render_in(view_context, &block)
end.join.html_safe
end

private

def initialize(component, object, **options)
@component = component
@collection = collection_variable(object || [])
@options = options
end
tclem marked this conversation as resolved.
Show resolved Hide resolved

def collection_variable(object)
if object.respond_to?(:to_ary)
object.to_ary
else
raise ArgumentError.new("The value of the argument isn't a valid collection. Make sure it responds to to_ary: #{object.inspect}")
end
end
end
end
7 changes: 7 additions & 0 deletions test/app/components/product_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<li>
<h1>Product</h1>
<h2><%= @product.name %></h2>
<p>
<%= @notice %>
</p>
</li>
8 changes: 8 additions & 0 deletions test/app/components/product_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class ProductComponent < ViewComponent::Base
def initialize(product:, notice:)
@product = product
@notice = notice
end
end
1 change: 1 addition & 0 deletions test/app/components/product_coupon_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h3><%= @item.percent_off %>%</h3>
9 changes: 9 additions & 0 deletions test/app/components/product_coupon_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class ProductCouponComponent < ViewComponent::Base
with_collection_parameter :item

def initialize(item:)
@item = item
end
end
4 changes: 4 additions & 0 deletions test/app/controllers/integration_examples_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ def controller_inline
def controller_inline_baseline
render("integration_examples/_controller_inline", locals: { message: "bar" })
end

def products
@products = [OpenStruct.new(name: "Radio clock"), OpenStruct.new(name: "Mints")]
end
end
4 changes: 4 additions & 0 deletions test/app/views/integration_examples/products.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h1>Products for sale</h1>
<ul>
<%= render(ProductComponent.with_collection(@products, notice: "Today only")) %>
</ul>
1 change: 1 addition & 0 deletions test/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
get :partial, to: "integration_examples#partial"
get :content, to: "integration_examples#content"
get :variants, to: "integration_examples#variants"
get :products, to: "integration_examples#products"
get :cached, to: "integration_examples#cached"
get :render_check, to: "integration_examples#render_check"
get :controller_inline, to: "integration_examples#controller_inline"
Expand Down
10 changes: 10 additions & 0 deletions test/view_component/integration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,14 @@ class IntegrationTest < ActionDispatch::IntegrationTest

assert_select("div", "hello,world!")
end

test "renders collections" do
get "/products"

assert_select("h1", text: "Products for sale")
assert_select("h1", text: "Product", count: 2)
assert_select("h2", text: "Radio clock")
assert_select("h2", text: "Mints")
assert_select("p", text: "Today only", count: 2)
end
end
61 changes: 58 additions & 3 deletions test/view_component/view_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def test_renders_content_for_template
end

def test_renders_content_areas_template_with_initialize_arguments
render_inline(ContentAreasComponent.new(title: "Hi!", footer: "Bye!")) do |component|
render_inline(ContentAreasComponent.new(title: "Radio clock!", footer: "Bye!")) do |component|
component.with(:body) { "Have a nice day." }
end
end
Expand Down Expand Up @@ -129,11 +129,11 @@ def test_renders_content_areas_template_with_block
def test_renders_content_areas_template_replaces_content
render_inline(ContentAreasComponent.new(footer: "Bye!")) do |component|
component.with(:title) { "Hello!" }
component.with(:title, "Hi!")
component.with(:title, "Radio clock!")
component.with(:body) { "Have a nice day." }
end

assert_selector(".title", text: "Hi!")
assert_selector(".title", text: "Radio clock!")
assert_selector(".body", text: "Have a nice day.")
assert_selector(".footer", text: "Bye!")
end
Expand Down Expand Up @@ -386,6 +386,61 @@ def test_backtrace_returns_correct_file_and_line_number
assert_match %r[app/components/exception_in_template_component\.html\.erb:2], error.backtrace[0]
end

# Rendering collections
tclem marked this conversation as resolved.
Show resolved Hide resolved

def test_render_collection
@products = [OpenStruct.new(name: "Radio clock"), OpenStruct.new(name: "Mints")]
tclem marked this conversation as resolved.
Show resolved Hide resolved
render_inline(ProductComponent.with_collection(@products, notice: "On sale"))

assert_selector("h1", text: "Product", count: 2)
assert_selector("h2", text: "Radio clock")
assert_selector("h2", text: "Mints")
assert_selector("p", text: "On sale", count: 2)
end

def test_render_collection_custom_collection_parameter_name
@coupons = [OpenStruct.new(percent_off: 20), OpenStruct.new(percent_off: 50)]
render_inline(ProductCouponComponent.with_collection(@coupons))

assert_selector("h3", text: "20%")
assert_selector("h3", text: "50%")
end

def test_render_collection_nil_and_empty_collection
[nil, []].each do |collection|
render_inline(ProductComponent.with_collection(collection, notice: "On sale"))

assert_no_text("Products")
end
end

def test_render_collection_missing_collection_object
exception = assert_raises ArgumentError do
render_inline(ProductComponent.with_collection(notice: "On sale"))
end

assert_equal exception.message, "The value of the argument isn't a valid collection. Make sure it responds to to_ary: {:notice=>\"On sale\"}"
end

def test_render_collection_missing_arg
@products = [OpenStruct.new(name: "Radio clock"), OpenStruct.new(name: "Mints")]
exception = assert_raises ArgumentError do
render_inline(ProductComponent.with_collection(@products))
end

assert_match /missing keyword/, exception.message
assert_match /notice/, exception.message
end

def test_render_single_item_from_collection
@product = OpenStruct.new(name: "Mints")
render_inline(ProductComponent.new(product: @product, notice: "On sale"))

assert_selector("h1", text: "Product", count: 1)
assert_selector("h2", text: "Mints")
assert_selector("p", text: "On sale", count: 1)
end

private

def modify_file(file, content)
Expand Down