From 2e67f1adc8e52121270d1139665a965e3f0792d8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 3 Mar 2006 19:34:23 +0000 Subject: RJS now does enumerations, baby! (closes #3876) [Rick Olson] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3754 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- .../lib/action_view/helpers/prototype_helper.rb | 108 +++++++++++++++++++-- actionpack/test/template/prototype_helper_test.rb | 104 +++++++++++++++++++- activesupport/lib/active_support/json.rb | 10 +- .../lib/active_support/json/encoders/core.rb | 4 + activesupport/test/json.rb | 34 ++++--- 5 files changed, 233 insertions(+), 27 deletions(-) diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 4c6c020ebe..db36f3de5f 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -434,7 +434,7 @@ module ActionView # page.select('p.welcome b').first # => $$('p.welcome b').first(); # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide(); def select(pattern) - JavaScriptCollectionProxy.new(self, pattern) + JavaScriptElementCollectionProxy.new(self, pattern) end # Inserts HTML at the specified +position+ relative to the DOM element @@ -687,9 +687,9 @@ module ActionView # Converts chained method calls on DOM proxy elements into JavaScript chains class JavaScriptProxy < Builder::BlankSlate #:nodoc: - def initialize(generator, root) + def initialize(generator, root = nil) @generator = generator - @generator << root + @generator << root if root end private @@ -697,7 +697,7 @@ module ActionView if method.to_s =~ /(.*)=$/ assign($1, arguments.first) else - call(method, *arguments) + call("#{method.to_s.first}#{method.to_s.classify[1..-1]}", *arguments) end end @@ -707,7 +707,7 @@ module ActionView end def assign(variable, value) - append_to_function_chain! "#{variable} = #{@generator.send(:javascript_object_for, value)}" + append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}") end def function_chain @@ -715,7 +715,7 @@ module ActionView end def append_to_function_chain!(call) - function_chain[-1] = function_chain[-1][0..-2] if function_chain[-1][-1..-1] == ";" # strip last ; + function_chain[-1].chomp!(';') function_chain[-1] += ".#{call};" end end @@ -737,15 +737,105 @@ module ActionView def reload replace :partial => @id.to_s end + + end + + class JavaScriptVariableProxy < JavaScriptProxy #:nodoc: + def initialize(generator, variable) + @variable = variable + @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks + super(generator) + end + + # The JSON Encoder calls this to check for the #to_json method + # Since it's a blank slate object, I suppose it responds to anything. + def respond_to?(method) + true + end + + def to_json + @variable + end + + private + def append_to_function_chain!(call) + @generator << @variable if @empty + @empty = false + super + end end class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc: + ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :findAll, :select, :max, :min, :partition, :reject, :sortBy] + ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] + def initialize(generator, pattern) - @pattern = pattern - super(generator, "$$('#{pattern}')") + super(generator, @pattern = pattern) + end + + def grep(variable, pattern, &block) + enumerable_method("grep(#{pattern.to_json}, function(value, index) {", variable, %w(value index), &block) + end + + def inject(variable, memo, &block) + enumerable_method("inject(#{memo.to_json}, function(memo, value, index) {", variable, %w(memo value index), &block) + end + + def pluck(variable, property) + add_variable_assignment!(variable) + append_enumerable_function!("pluck(#{property.to_json});") + end + + def zip(variable, *arguments, &block) + add_variable_assignment!(variable) + append_enumerable_function!("zip(#{arguments.collect { |a| a.to_json } * ', '}") + if block + function_chain[-1] += ", function(array) {" + yield @generator, ActiveSupport::JSON::Variable.new('array') + add_return_statement! + @generator << '});' + else + function_chain[-1] += ');' + end end - # TODO: Implement funky stuff like .each + private + def method_missing(method, *arguments, &block) + ENUMERABLE_METHODS.include?(method) ? enumerate(method, ENUMERABLE_METHODS_WITH_RETURN.include?(method), &block) : super + end + + def enumerate(enumerable, variable = nil, &block) + enumerable_method("#{enumerable}(function(value, index) {", variable, %w(value index), &block) + end + + def enumerable_method(enumerable, variable, yield_params, &block) + add_variable_assignment!(variable) if variable + append_enumerable_function!(enumerable) + yield *([@generator] + yield_params.collect { |p| JavaScriptVariableProxy.new(@generator, p) }) + add_return_statement! if variable + @generator << '});' + end + + def add_variable_assignment!(variable) + function_chain.push("#{variable} = #{function_chain.pop}") + end + + def add_return_statement! + unless function_chain.last =~ /return/ + function_chain.push("return #{function_chain.pop.chomp(';')};") + end + end + + def append_enumerable_function!(call) + function_chain[-1].chomp!(';') + function_chain[-1] += ".#{call}" + end + end + + class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\ + def initialize(generator, pattern) + super(generator, "$$('#{pattern}')") + end end end end diff --git a/actionpack/test/template/prototype_helper_test.rb b/actionpack/test/template/prototype_helper_test.rb index 5a4ec61b39..8ae7a98108 100644 --- a/actionpack/test/template/prototype_helper_test.rb +++ b/actionpack/test/template/prototype_helper_test.rb @@ -148,6 +148,8 @@ class PrototypeHelperTest < Test::Unit::TestCase end end +ActionView::Helpers::JavaScriptCollectionProxy.send :public, :enumerate + class JavaScriptGeneratorTest < Test::Unit::TestCase include BaseTest @@ -244,8 +246,8 @@ Element.update("baz", "

This is a test

"); end def test_element_proxy_two_deep - @generator['hello'].hide("first").display - assert_equal %($('hello').hide("first").display();), @generator.to_s + @generator['hello'].hide("first").clean_whitespace + assert_equal %($('hello').hide("first").cleanWhitespace();), @generator.to_s end def test_select_access @@ -281,4 +283,102 @@ Element.update("baz", "

This is a test

"); assert_equal %(Droppables.add('blah', {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});), @generator.drop_receiving('blah', :url => { :action => "order" }) end + + def test_collection_proxy_with_each + @generator.select('p.welcome b').each do |page, value| + value.remove_class_name 'selected' + end + @generator.select('p.welcome b').each do |page, value, index| + page.call 'alert', index + page.call 'alert', value, 'selected' + end + assert_equal <<-EOS.strip, @generator.to_s +$$('p.welcome b').each(function(value, index) { +value.removeClassName("selected"); +}); +$$('p.welcome b').each(function(value, index) { +alert(index); +alert(value, "selected"); +}); + EOS + end + + def test_collection_proxy_on_enumerables_with_return_and_index + iterator = Proc.new { |page, value| page << '(value.className == "welcome")' } + iterator_with_index = Proc.new { |page, value, index| page.call 'alert', index ; page << '(value.className == "welcome")' } + ActionView::Helpers::JavaScriptCollectionProxy::ENUMERABLE_METHODS_WITH_RETURN.each do |enum| + @generator.select('p').enumerate(enum, 'a', &iterator) + @generator.select('p').enumerate(enum, 'b', &iterator_with_index) + + assert_equal <<-EOS.strip, @generator.to_s +a = $$('p').#{enum}(function(value, index) { +return (value.className == "welcome"); +}); +b = $$('p').#{enum}(function(value, index) { +alert(index); +return (value.className == "welcome"); +}); + EOS + @generator = create_generator + end + end + + def test_collection_proxy_with_grep + @generator.select('p').grep 'a', /^a/ do |page, value| + page << '(value.className == "welcome")' + end + @generator.select('p').grep 'b', /b$/ do |page, value, index| + page.call 'alert', value + page << '(value.className == "welcome")' + end + + assert_equal <<-EOS.strip, @generator.to_s +a = $$('p').grep(/^a/, function(value, index) { +return (value.className == "welcome"); +}); +b = $$('p').grep(/b$/, function(value, index) { +alert(value); +return (value.className == "welcome"); +}); + EOS + end + + def test_collection_proxy_with_inject + @generator.select('p').inject 'a', [] do |page, memo, value| + page << '(value.className == "welcome")' + end + @generator.select('p').inject 'b', nil do |page, memo, value, index| + page.call 'alert', memo + page << '(value.className == "welcome")' + end + + assert_equal <<-EOS.strip, @generator.to_s +a = $$('p').inject([], function(memo, value, index) { +return (value.className == "welcome"); +}); +b = $$('p').inject(null, function(memo, value, index) { +alert(memo); +return (value.className == "welcome"); +}); + EOS + end + + def test_collection_proxy_with_pluck + @generator.select('p').pluck('a', 'className') + assert_equal %(a = $$('p').pluck("className");), @generator.to_s + end + + def test_collection_proxy_with_zip + ActionView::Helpers::JavaScriptCollectionProxy.new(@generator, '[1, 2, 3]').zip('a', [4, 5, 6], [7, 8, 9]) + ActionView::Helpers::JavaScriptCollectionProxy.new(@generator, '[1, 2, 3]').zip('b', [4, 5, 6], [7, 8, 9]) do |page, array| + page.call 'array.reverse' + end + + assert_equal <<-EOS.strip, @generator.to_s +a = [1, 2, 3].zip([4, 5, 6], [7, 8, 9]); +b = [1, 2, 3].zip([4, 5, 6], [7, 8, 9], function(array) { +return array.reverse(); +}); + EOS + end end \ No newline at end of file diff --git a/activesupport/lib/active_support/json.rb b/activesupport/lib/active_support/json.rb index 77c7225c66..943adebd35 100644 --- a/activesupport/lib/active_support/json.rb +++ b/activesupport/lib/active_support/json.rb @@ -3,7 +3,15 @@ require 'active_support/json/encoders' module ActiveSupport module JSON #:nodoc: class CircularReferenceError < StandardError; end - + # returns the literal string as its JSON encoded form. Useful for passing javascript variables into functions. + # + # page.call 'Element.show', ActiveSupport::JSON::Variable.new("$$(#items li)") + class Variable < String + def to_json + self + end + end + class << self REFERENCE_STACK_VARIABLE = :json_reference_stack diff --git a/activesupport/lib/active_support/json/encoders/core.rb b/activesupport/lib/active_support/json/encoders/core.rb index 003c938be4..d3193af555 100644 --- a/activesupport/lib/active_support/json/encoders/core.rb +++ b/activesupport/lib/active_support/json/encoders/core.rb @@ -56,6 +56,10 @@ module ActiveSupport result << '}' end end + + define_encoder Regexp do |regexp| + regexp.inspect + end end end end diff --git a/activesupport/test/json.rb b/activesupport/test/json.rb index c5b597abd7..fc4d7705b1 100644 --- a/activesupport/test/json.rb +++ b/activesupport/test/json.rb @@ -9,26 +9,30 @@ class Foo end class TestJSONEmitters < Test::Unit::TestCase - TrueTests = [[ true, %(true) ]] - FalseTests = [[ false, %(false) ]] - NilTests = [[ nil, %(null) ]] - NumericTests = [[ 1, %(1) ], - [ 2.5, %(2.5) ]] + TrueTests = [[ true, %(true) ]] + FalseTests = [[ false, %(false) ]] + NilTests = [[ nil, %(null) ]] + NumericTests = [[ 1, %(1) ], + [ 2.5, %(2.5) ]] - StringTests = [[ 'this is the string', %("this is the string") ], - [ 'a "string" with quotes', %("a \\"string\\" with quotes") ]] + StringTests = [[ 'this is the string', %("this is the string") ], + [ 'a "string" with quotes', %("a \\"string\\" with quotes") ]] - ArrayTests = [[ ['a', 'b', 'c'], %([\"a\", \"b\", \"c\"]) ], - [ [1, 'a', :b, nil, false], %([1, \"a\", \"b\", null, false]) ]] + ArrayTests = [[ ['a', 'b', 'c'], %([\"a\", \"b\", \"c\"]) ], + [ [1, 'a', :b, nil, false], %([1, \"a\", \"b\", null, false]) ]] - HashTests = [[ {:a => :b, :c => :d}, %({\"c\": \"d\", \"a\": \"b\"}) ]] + HashTests = [[ {:a => :b, :c => :d}, %({\"c\": \"d\", \"a\": \"b\"}) ]] - SymbolTests = [[ :a, %("a") ], - [ :this, %("this") ], - [ :"a b", %("a b") ]] + SymbolTests = [[ :a, %("a") ], + [ :this, %("this") ], + [ :"a b", %("a b") ]] + + ObjectTests = [[ Foo.new(1, 2), %({\"a\": 1, \"b\": 2}) ]] + + VariableTests = [[ ActiveSupport::JSON::Variable.new('foo'), 'foo'], + [ ActiveSupport::JSON::Variable.new('alert("foo")'), 'alert("foo")']] + RegexpTests = [[ /^a/, '/^a/' ], /^\w{1,2}[a-z]+/ix, '/^\\w{1,2}[a-z]+/ix'] - ObjectTests = [[ Foo.new(1, 2), %({\"a\": 1, \"b\": 2}) ]] - constants.grep(/Tests$/).each do |class_tests| define_method("test_#{class_tests[0..-6].downcase}") do self.class.const_get(class_tests).each do |pair| -- cgit v1.2.3