From 7ccb52c3c1b75bcd7164e7c280ab3d0e13196c77 Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Sat, 12 Jun 2010 10:00:52 -0700 Subject: [PATCH] Provide view helper methods that encapsulate styling patterns that are common in rails applications by making it easier to add class names and other attributes in the layout's body tag. --- actionpack/lib/action_view/helpers.rb | 2 + actionpack/lib/action_view/helpers/body_helper.rb | 63 ++++++++++++++++++++ actionpack/lib/action_view/helpers/tag_helper.rb | 55 +++++++++++++++++ actionpack/test/template/body_helper_test.rb | 58 ++++++++++++++++++ actionpack/test/template/tag_helper_test.rb | 54 +++++++++++++++++ .../app/views/layouts/application.html.erb.tt | 4 +- 6 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 actionpack/lib/action_view/helpers/body_helper.rb create mode 100644 actionpack/test/template/body_helper_test.rb diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index ba3bdd0..1e62543 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -7,6 +7,7 @@ module ActionView #:nodoc: autoload :ActiveModelHelper autoload :AssetTagHelper autoload :AtomFeedHelper + autoload :BodyHelper autoload :CacheHelper autoload :CaptureHelper autoload :CsrfHelper @@ -39,6 +40,7 @@ module ActionView #:nodoc: include ActiveModelHelper include AssetTagHelper include AtomFeedHelper + include BodyHelper include CacheHelper include CaptureHelper include CsrfHelper diff --git a/actionpack/lib/action_view/helpers/body_helper.rb b/actionpack/lib/action_view/helpers/body_helper.rb new file mode 100644 index 0000000..4978a96 --- /dev/null +++ b/actionpack/lib/action_view/helpers/body_helper.rb @@ -0,0 +1,63 @@ +require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/object/blank' + +module ActionView + module Helpers #:nodoc: + # The TextHelper module provides a set of methods for filtering, formatting + # and transforming strings, which can reduce the amount of inline Ruby code in + # your views. These helper methods extend ActionView making them callable + # within your template files. + # + # XXX: Should the @body_attributes hash have indifferent access? + module BodyHelper + # Returns a hash of attributes for the body tag + # with the default body id and class. + # + # You can override the defaults by calling set_body_id and set_body_class + # + # You can also pass in an attribute hash directly to provide other attributes: + # + # body_attributes(:style => "padding: 1em;") + # + # When using haml, this can be passed directly to the body tag: + # %body{body_attributes} + # + # When using ERB: + # <% content_tag(:body, body_attributes) do - %> + # Hello, World! + # <% end %> + def body_attributes(additional_attributes = {}) + default_body_attributes.merge(merge_tag_attributes(@body_attributes || {}, additional_attributes)) + end + + # This method makes creating a default body tag even easier: + # <%= body_tag do -%> + # Hello world! + # <% end -%> + def body_tag(additional_attributes = {}, &block) + content_tag("body", body_attributes(additional_attributes), &block) + end + + # Returns the default body attributes for the current controller + # For instance, in the PostsController: + # => {:class => "posts", :id => "posts-index"} + # And in the WizzBangController: + # => {:class => "wizz-bang", :id => "wizz-bang-show"} + def default_body_attributes + { + :class => controller.controller_path.gsub(%r{/},"-").dasherize, + :id => "#{controller.controller_path}_#{controller.action_name}".gsub(%r{/},"-").dasherize + } + end + + # Uses the TagHelper#merge_tag_attributes! method to add + # additional attributes to the body tag in the layout. + def add_body_attributes(attributes) + @body_attributes ||= {} + merge_tag_attributes!(@body_attributes, attributes) + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 66277f7..57ecfba 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -106,6 +106,61 @@ module ActionView ActiveSupport::Multibyte.clean(html.to_s).gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] } end + # Adds one or more css classes to a string of css classes + # This method guards against nil and duplicate classes. + # + # Strings or symbols may be passed in, but this method always returns a string. + # + # Examples: + # add_css_class(nil,"myclass") + # => "myclass" + # add_css_class("existing","new") + # => "existing new" + # add_css_class("another existing","existing","new") + # => "another existing new" + def add_css_class(*classes) + classes.flatten.compact.map{|c| c.to_s.strip.split(/\s+/)}.flatten.uniq.sort.join(" ") + end + + # merge with_attrs into the original_attrs. + # making sure to merge existing attributes instead of overwriting them. + # + # original_attrs = {} + # merge_tag_attributes!(original_attrs, :id => "special", :class => "foo", :style => "color: red;") + # => {:id => "special", :class => "foo", :style => "color: red;"} + # + # A second call: + # merge_tag_attributes!(:id => "more-special", :class => "bar", :style => "background-color: #ccc;") + # => {:id => "more-special", :class => "bar foo", :style => "color: red; background-color: #ccc;"} + # + # The following attributes are merged instead of being overwritten: + # * :class - class attributes are merged with the add_css_class method. + # * :style - style attributes are merged with a semi-colon separator + # * :onanything - Anything starting with `on` is merged like javascript. + # + # Otherwise, the attribute is overwritten by the value in with_attrs. + # + # XXX TODO: is there anything to do for html_safe and escaping? + def merge_tag_attributes!(original_attrs, with_attrs) + with_attrs = with_attrs.dup + original_attrs.merge!(with_attrs) do |k, v1, v2| + case k.to_s + when "class" + add_css_class(v1, v2) + when "style", /^on/ + [v1, v2].compact.map{|a| a.sub(/;\s*$/,"")}.join("; ") + else + v2 + end + end + end + + # Returns a new hash that has merged original_attrs with with_attrs. + # See merge_tag_attributes! for more information. + def merge_tag_attributes(original_attrs, with_attrs) + merge_tag_attributes!(original_attrs.dup, with_attrs) + end + private def content_tag_string(name, content, options, escape = true) diff --git a/actionpack/test/template/body_helper_test.rb b/actionpack/test/template/body_helper_test.rb new file mode 100644 index 0000000..d42d785 --- /dev/null +++ b/actionpack/test/template/body_helper_test.rb @@ -0,0 +1,58 @@ +require 'abstract_unit' + +class BodyHelperTest < ActionView::TestCase + tests ActionView::Helpers::BodyHelper + + MockController = Struct.new(:controller_path, :action_name) + + attr_accessor :controller + + def setup + @controller = MockController.new("foo", "bar") + end + + def teardown + @controller = nil + @body_attributes = nil + end + + def test_default_body_attributes + assert_equal({:class=>"foo", :id=>"foo-bar"}, default_body_attributes) + end + + def test_default_body_attributes_for_complex_controllers + @controller.controller_path = "scoped/camel_cased" + assert_equal({:class=>"scoped-camel-cased", :id=>"scoped-camel-cased-bar"}, default_body_attributes) + end + + def test_unspecified_body_attributes_are_default + assert_equal(default_body_attributes, body_attributes) + end + + def test_body_attributes_can_be_overridden + # The default attributes are there unless specified + add_body_attributes(:style=>"color:#333;") + assert_equal({:class=>"foo", :id=>"foo-bar", :style=>"color:#333;"}, body_attributes) + + # can specify only some of the default attributes + add_body_attributes(:id=>"special-id") + assert_equal({:class=>"foo", :id=>"special-id", :style=>"color:#333;"}, body_attributes) + add_body_attributes(:class=>"special-class") + assert_equal({:class=>"special-class", :id=>"special-id", :style=>"color:#333;"}, body_attributes) + + # tag attributes are merged when they can be + add_body_attributes(:class=>"another-special-class") + assert_equal({:class=>"another-special-class special-class", :id=>"special-id", :style=>"color:#333;"}, body_attributes) + end + + def test_body_tag + buffer = body_tag { concat "Hello world!" } + assert_dom_equal %Q{Hello world!}, buffer + + buffer = body_tag(:style => "color:red;") { concat "Hello world!" } + assert_dom_equal %Q{Hello world!}, buffer + + buffer = body_tag(:class=>"hi", :id=>"world") { concat "Hello world, again!" } + assert_dom_equal %Q{Hello world, again!}, buffer + end +end \ No newline at end of file diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index ec5fe3d..661c83c 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -110,4 +110,58 @@ class TagHelperTest < ActionView::TestCase def test_disable_escaping assert_equal '', tag('a', { :href => '&' }, false, false) end + + def test_add_css_class + # tolerant of nils + assert_equal "", add_css_class(nil, nil) + assert_equal "a", add_css_class("a", nil) + assert_equal "a", add_css_class(nil, "a") + # joined with a space + assert_equal "a b", add_css_class("a", "b") + # output is sorted + assert_equal "a b", add_css_class("b", "a") + # symbols work too + assert_equal "a b", add_css_class(:b, :a) + # several classes can be specified per argument + assert_equal "a b c d", add_css_class("b c", "a d") + # Any number of arguments are accepted, and the output should be uniq'd + assert_equal "a b", add_css_class("a", "b", "b a") + end + + def test_merge_tag_attributes! + attributes = {} + + merge_tag_attributes!(attributes, :class => "a", :id => "id") + assert_equal({:class => "a", :id => "id"}, attributes) + + merge_tag_attributes!(attributes, :class => "b", :id => "id2") + assert_equal({:class => "a b", :id => "id2"}, attributes) + + # Style attribute gets merged with semi-colon separators + attributes = {:style => "color: red;"} #with a semicolon + merge_tag_attributes!(attributes, :style => "font-weight: bold") + assert_equal({:style => "color: red; font-weight: bold"}, attributes) + + attributes = {:style => "color: red"} #without a semicolon + merge_tag_attributes!(attributes, :style => "font-weight: bold") + assert_equal({:style => "color: red; font-weight: bold"}, attributes) + + # Event attributes gets merged with semi-colon separators + attributes = {:onclick => "alert('hello');"} #with a semicolon + merge_tag_attributes!(attributes, :onclick => "alert('world');") + assert_equal({:onclick => "alert('hello'); alert('world')"}, attributes) + + attributes = {:onclick => "alert('hello')"} #without a semicolon + merge_tag_attributes!(attributes, :onclick => "alert('world');") + assert_equal({:onclick => "alert('hello'); alert('world')"}, attributes) + end + + def test_merge_tag_attributes + attributes = {} + # Make sure the returned value is merged + assert_equal({:class=>"a"}, merge_tag_attributes(attributes, :class => "a")) + # but the original value is unchanged + assert_equal({}, attributes) + end + end diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index 1dd112b..c32f294 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -6,9 +6,9 @@ <%%= javascript_include_tag :defaults %> <%%= csrf_meta_tag %> - +<%%= body_tag do -%> <%%= yield %> - +<%% end %> -- 1.6.0