Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added PartialIteration object used when rendering collections #7698

Closed
wants to merge 3 commits into from

8 participants

@joeljunstrom

Based on the ideas in #5634 but refactored into an object as per the comments.

The iteration object is available as the local variable
"template_name_iteration" when rendering partials with collections.

It gives access to the +size+ of the collection beeing iterated over,
the current +index+ and two convinicence methods +first?+ and +last?+

For now the "template_name_counter" variable is kept but is deprecated.

actionpack/test/controller/render_test.rb
@@ -630,6 +630,14 @@ def partial_collection_with_as
render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
end
+ def partial_collection_with_iteration
+ render :partial => "customer_iteration", :collection => [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ]
@frodsan
frodsan added a note

Please, change this to the 1.9 hash syntax. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/test/controller/render_test.rb
@@ -630,6 +630,14 @@ def partial_collection_with_as
render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
end
+ def partial_collection_with_iteration
+ render :partial => "customer_iteration", :collection => [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ]
+ end
+
+ def partial_collection_with_as_and_iteration
+ render :partial => "customer_iteration_with_as", :collection => [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], :as => :client
@frodsan
frodsan added a note

ditto.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_view/partial_iteration.rb
@@ -0,0 +1,19 @@
+module ActionView
+ class PartialIteration
@frodsan
frodsan added a note

Can you add documentation about this?

If you think this is not part of the public API, you can add a # :nodoc: directive.

Thanks.

Yea I was unsure wether this should be documented or not. The methods are mentioned in the docs for render partial and I had a hard time finding examples of an similar utility value object. I'll go with nodoc for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@joeljunstrom

Comments applied and rebased

@frodsan

Please squash your commits.

@lucasuyezu

+1 for this pull request. Since PartialIteration implements #index, I believe the best solution regarding variable_counter would be to print/log a deprecation warning and remove it in 4.1 or another release. I could try to implement this if you guys want. Also let me know if I can help with anything.

@SKoschnicke

+1. This would be a big improvement.

@joeljunstrom

@lucasuyezu In the current implementation the counter is just a local variable making it a bit awkward adding a deprecation warning when accessing it.

If you have ideas on a implementation I can give you access to my repo.

@lucasuyezu

@joeljunstrom We can turn the counter into a method with the same name that prints/logs the deprecation warning and return the variable. I will clone your repo and implement it.

@joeljunstrom

Yea I just could'nt come up with what to put the method on =) Sounds good!

@lucasuyezu

@joeljunstrom Done! Can I has repo acces?

@joeljunstrom

@lucasuyezu sorry, I was sleeping =) You now have access!

@rafaelfranca

@joeljunstrom @lucasuyezu guys, just ping me when done.

Thanks.

@joeljunstrom

Will do

@lucasuyezu

@rafaelfranca Would you like to take a look now? I believe it is ok.

@lucasuyezu

@joeljunstrom @rafaelfranca hello? Is anyone there? :worried:

@joeljunstrom

@rafaelfranca & @lucasuyezu Sorry about the delay, I've rebased and squashed including lucasuyezu's additions (deprecating the use of the template_name_counter local. I'm a bit hesitant regarding the extra complexity surrounding the deprecation warning but maybe it does not matter much.

actionpack/lib/action_view/template.rb
@@ -326,7 +327,27 @@ def handle_render_error(view, e) #:nodoc:
end
def locals_code #:nodoc:
- @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join
+ @locals.map do |key|
+ if key =~ /(.*)_counter$/
+ "self.#{key}= local_assigns[:#{key}];"
+ else
+ "#{key}= local_assigns[:#{key}];"
+ end
+ end.join
+ end
+
+ def define_locals_code #:nodoc:
+ @locals.map do |key|
+ if key =~ /(.*)_counter$/
+ <<-local_method
+ redefine_method(:#{key}=) { |new_value| @#{key}= new_value }

I don't think we should make a setter method with an instance var for this, it'd be better to define everything in the same locals_code loop somehow.

Btw, you probably don't need to set the variable, you can delegate to #{key}_iteration.index, right?

I could try to use method_missing in the view object. In this scenario I would check if the method called is template_counter, warn and then return the value, making the redefine_method getters and setters unnecessary. But I still would need to keep either template_counter or template_iteration as an instance variable, because method_missing won't have access to local variables.

Does this sound better?

I think we should avoid method missing if possible, but I don't have an idea off the top of my head, I'll try to think about something.

I'm toying around with removing the name_counter variable from the partial renderer / locals and just setting up a method delegating to the iterator index. But tbh at the moment the module eval context in the template class is doing my head in =) Also it feels really dirty adding code to the template class for something that is just used for partials with collections, but I do not see a different way to do it yet.

@rafaelfranca Owner

@josevalim thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carlosantoniodasilva carlosantoniodasilva commented on the diff
actionpack/test/template/partial_iteration_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'action_view/partial_iteration'
+class PartialIterationTest < ActiveSupport::TestCase

Add space before this line, and remove space after class and before the last end (the class body).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carlosantoniodasilva carlosantoniodasilva commented on the diff
actionpack/lib/action_view/partial_iteration.rb
((2 lines not shown))
+ class PartialIteration # :nodoc:
+ attr_reader :size, :index
+
+ def initialize(size, index)
+ @size = size
+ @index = index
+ end
+
+ def first?
+ index == 0
+ end
+
+ def last?
+ index == size - 1
+ end
+

No need for empty line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carlosantoniodasilva

Nice @joeljunstrom, I'd like to get this in for Rails 4 :+1:

actionpack/lib/action_view/template.rb
@@ -281,6 +281,7 @@ def #{method_name}(local_assigns, output_buffer)
ensure
@virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
end
+ #{define_locals_code}
@josevalim Owner

I don't follow the need for this. Could someone please enlighten me?

We want to show a deprecation warning when a user calls variable_counter instead of the new variable_iteration.index.
Since counter variables are local variables, this code defines a getter method for counter variables so we can warn and then return its original value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mattgoldman

any updates on this? is it pretty much good to go?

@joeljunstrom

The problem is that getting this functionality with an deprecation warning for the old counter variable is as I see it pretty much impossible without adding (some what hard to follow) code to ActionView::Template without a major refactor of how locals are assigned to templates.

When looking at this again I have a hard time feeling the addition is worth hacking ActionView::Template.

So unless someone have a great idea of how to achieve this maybe we should close this for now :confused:

@lucasuyezu

The code is pretty much good to go, apart from the deprecation warning. I can revert it so this can ship, and prepare the deprecation warning on another pull request, if everybody agrees.

joeljunstrom and others added some commits
@joeljunstrom joeljunstrom Added PartialIteration class used when rendering collections
The iteration object is available as the local variable
"template_name_iteration" when rendering partials with collections.

It gives access to the +size+ of the collection beeing iterated over,
the current +index+ and two convinicence methods +first?+ and +last?+

"template_name_counter" variable is kept but is deprecated.
742989c
@lucasuyezu lucasuyezu Reverting deprecation warning. 290ddd1
@lucasuyezu lucasuyezu Fix broken spec from rebase. e3f9053
@lucasuyezu

@joeljunstrom I have removed the deprecation warning. I also rebased the code based on the current rails master and fixed a spec that broke since then. Please let me know if there is any other thing blocking this.

@joeljunstrom

I guess we should squash it and apply carlosantoniodasilva's formatting comments if I forgot to do it =)

@SKoschnicke

What prevents this from being merged? Can I help?

@rafaelfranca rafaelfranca referenced this pull request from a commit in rafaelfranca/omg-rails
@rafaelfranca rafaelfranca Add CHANGELOG entry for PartialIteration.
Closes #7698.
f5694a2
@rafaelfranca rafaelfranca referenced this pull request from a commit
@rafaelfranca rafaelfranca Merge branch 'joeljunstrom-local-iterator-for-partial-collections'
Squash and merge #7698 doing some improvements to the original
implementation.
1515569
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 3, 2013
  1. @joeljunstrom @lucasuyezu

    Added PartialIteration class used when rendering collections

    joeljunstrom authored lucasuyezu committed
    The iteration object is available as the local variable
    "template_name_iteration" when rendering partials with collections.
    
    It gives access to the +size+ of the collection beeing iterated over,
    the current +index+ and two convinicence methods +first?+ and +last?+
    
    "template_name_counter" variable is kept but is deprecated.
  2. @lucasuyezu
  3. @lucasuyezu
This page is out of date. Refresh to see the latest.
View
19 actionpack/lib/action_view/partial_iteration.rb
@@ -0,0 +1,19 @@
+module ActionView
+ class PartialIteration # :nodoc:
+ attr_reader :size, :index
+
+ def initialize(size, index)
+ @size = size
+ @index = index
+ end
+
+ def first?
+ index == 0
+ end
+
+ def last?
+ index == size - 1
+ end
+

No need for empty line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+end
View
18 actionpack/test/controller/render_test.rb
@@ -638,6 +638,14 @@ def partial_collection_with_as
render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
end
+ def partial_collection_with_iteration
+ render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ]
+ end
+
+ def partial_collection_with_as_and_iteration
+ render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client
+ end
+
def partial_collection_with_counter
render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
end
@@ -1440,6 +1448,16 @@ def test_partial_collection_with_as
assert_equal "david david davidmary mary mary", @response.body
end
+ def test_partial_collection_with_iteration
+ get :partial_collection_with_iteration
+ assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body
+ end
+
+ def test_partial_collection_with_as_and_iteration
+ get :partial_collection_with_as_and_iteration
+ assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body
+ end
+
def test_partial_collection_with_counter
get :partial_collection_with_counter
assert_equal "david0mary1", @response.body
View
1  actionpack/test/fixtures/test/_customer_iteration.erb
@@ -0,0 +1 @@
+<%= customer_iteration_iteration.size %>-<%= customer_iteration_iteration.index %>: <%= customer_iteration.name %><%= '-first' if customer_iteration_iteration.first? %><%= '-last' if customer_iteration_iteration.last? %>
View
1  actionpack/test/fixtures/test/_customer_iteration_with_as.erb
@@ -0,0 +1 @@
+<%= client_iteration.size %>-<%= client_iteration.index %>: <%= client.name %><%= '-first' if client_iteration.first? %><%= '-last' if client_iteration.last? %>
View
31 actionpack/test/template/partial_iteration_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'action_view/partial_iteration'
+class PartialIterationTest < ActiveSupport::TestCase

Add space before this line, and remove space after class and before the last end (the class body).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ def test_has_size_and_index
+ iteration = ActionView::PartialIteration.new 3, 0
+ assert_equal 0, iteration.index, "should be at the first index"
+ assert_equal 3, iteration.size, "should have the size"
+ end
+
+ def test_first_is_true_when_current_is_at_the_first_index
+ iteration = ActionView::PartialIteration.new 3, 0
+ assert iteration.first?, "first when current is 0"
+ end
+
+ def test_first_is_false_unless_current_is_at_the_first_index
+ iteration = ActionView::PartialIteration.new 3, 1
+ assert !iteration.first?, "not first when current is 1"
+ end
+
+ def test_last_is_true_when_current_is_at_the_last_index
+ iteration = ActionView::PartialIteration.new 3, 2
+ assert iteration.last?, "last when current is 2"
+ end
+
+ def test_last_is_false_unless_current_is_at_the_last_index
+ iteration = ActionView::PartialIteration.new 3, 0
+ assert !iteration.last?, "not last when current is 0"
+ end
+
+end
View
41 actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,4 +1,5 @@
require 'thread_safe'
+require "action_view/partial_iteration"
module ActionView
# = Action View Partials
@@ -56,8 +57,12 @@ module ActionView
# <%= render partial: "ad", collection: @advertisements %>
#
# This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An
- # iteration counter will automatically be made available to the template with a name of the form
- # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
+ # iteration object will automatically be made available to the template with a name of the form
+ # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
+ # the collection and the total size of the collection. The iteration object also has two convenience methods,
+ # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
+ # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
+ # +index+ method.
#
# The <tt>:as</tt> option may be used when rendering partials.
#
@@ -352,7 +357,7 @@ def setup(context, options, block)
end
if @path
- @variable, @variable_counter = retrieve_variable(@path, as)
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
@template_keys = retrieve_template_keys
else
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
@@ -385,7 +390,7 @@ def find_template(path, locals)
def collection_with_template
view, locals, template = @view, @locals, @template
- as, counter = @variable, @variable_counter
+ as, counter, iteration = @variable, @variable_counter, @variable_iteration
if layout = @options[:layout]
layout = find_template(layout, @template_keys)
@@ -393,8 +398,11 @@ def collection_with_template
index = -1
@collection.map do |object|
- locals[as] = object
- locals[counter] = (index += 1)
+ index += 1
+
+ locals[as] = object
+ locals[counter] = index
+ locals[iteration] = PartialIteration.new(@collection.size, index)
content = template.render(view, locals)
content = layout.render(view, locals) { content } if layout
@@ -410,10 +418,11 @@ def collection_without_template
index = -1
@collection.map do |object|
index += 1
- path, as, counter = collection_data[index]
+ path, as, counter, iteration = collection_data[index]
- locals[as] = object
- locals[counter] = index
+ locals[as] = object
+ locals[counter] = index
+ locals[iteration] = PartialIteration.new(@collection.size, index)
template = (cache[path] ||= find_template(path, keys + [as, counter]))
template.render(view, locals)
@@ -466,8 +475,11 @@ def merge_prefix_into_object_path(prefix, object_path)
def retrieve_template_keys
keys = @locals.keys
- keys << @variable if @object || @collection
- keys << @variable_counter if @collection
+ keys << @variable if @object || @collection
+ if @collection
+ keys << @variable_counter
+ keys << @variable_iteration
+ end
keys
end
@@ -477,8 +489,11 @@ def retrieve_variable(path, as)
raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
$1.to_sym
end
- variable_counter = :"#{variable}_counter" if @collection
- [variable, variable_counter]
+ if @collection
+ variable_counter = :"#{variable}_counter"
+ variable_iteration = :"#{variable}_iteration"
+ end
+ [variable, variable_counter, variable_iteration]
end
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
View
2  actionview/test/template/render_test.rb
@@ -257,7 +257,7 @@ def test_render_partial_collection_as_by_symbol
end
def test_render_partial_collection_without_as
- assert_equal "local_inspector,local_inspector_counter",
+ assert_equal "local_inspector,local_inspector_counter,local_inspector_iteration",
@view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ])
end
Something went wrong with that request. Please try again.