Permalink
Browse files

Added the first part of Simply Helpful to core. The rest is pending a…

… clean integartion of polymorphic urls [DHH]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@6633 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 0dc7038 commit da257eb81ba1eab76ef2c1a256916193858418d4 @dhh dhh committed Apr 30, 2007
View
@@ -1,5 +1,20 @@
*SVN*
+* Added RecordTagHelper for using RecordIdentifier conventions on divs and other container elements [DHH]. Example:
+
+ <% div_for(post) do %> <div id="post_45" class="post">
+ <%= post.body %> What a wonderful world!
+ <% end %> </div>
+
+* Added page[record] accessor to JavaScriptGenerator that relies on RecordIdentifier to find the right dom id [DHH]. Example:
+
+ format.js do
+ # Calls: new Effect.fade('post_45');
+ render(:update) { |page| page[post].visual_effect(:fade) }
+ end
+
+* Added RecordIdentifier to enforce view conventions on records for dom ids, classes, and partial paths [DHH]
+
* Added map.namespace to deal with the common situation of admin sections and the like [DHH]
Before:
@@ -54,6 +54,7 @@
require 'action_controller/streaming'
require 'action_controller/session_management'
require 'action_controller/components'
+require 'action_controller/record_identifier'
require 'action_controller/macros/auto_complete'
require 'action_controller/macros/in_place_editing'
@@ -76,6 +77,7 @@
include ActionController::Streaming
include ActionController::SessionManagement
include ActionController::Components
+ include ActionController::RecordIdentifier
include ActionController::Macros::AutoComplete
include ActionController::Macros::InPlaceEditing
end
@@ -0,0 +1,91 @@
+module ActionController
+ # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
+ # Active Resources or pretty much any other model type that has an id. These patterns are then used to try elevate
+ # the view actions to a higher logical level. Example:
+ #
+ # # routes
+ # map.resources :posts
+ #
+ # # view
+ # <% div_for(post) do %> <div id="post_45" class="post">
+ # <%= post.body %> What a wonderful world!
+ # <% end %> </div>
+ #
+ # # controller
+ # def destroy
+ # post = Post.find(params[:id])
+ # post.destroy
+ #
+ # respond_to do |format|
+ # format.html { redirect_to(post) } # Calls polymorphic_url(post) which in turn calls post_url(post)
+ # format.js do
+ # # Calls: new Effect.fade('post_45');
+ # render(:update) { |page| page[post].visual_effect(:fade) }
+ # end
+ # end
+ # end
+ #
+ # As the example above shows, you can stop caring to a large extend what the actual id of the post is. You just know
+ # that one is being assigned and that the subsequent calls in redirect_to and the RJS expect that same naming
+ # convention and allows you to write less code if you follow it.
+ module RecordIdentifier
+ extend self
+
+ # Returns plural/singular for a record or class. Example:
+ #
+ # partial_path(post) # => "posts/post"
+ # partial_path(Person) # => "people/person"
+ def partial_path(record_or_class)
+ klass = class_from_record_or_class(record_or_class)
+ "#{klass.name.tableize}/#{klass.name.demodulize.underscore}"
+ end
+
+ # The DOM class convention is to use the singular form of an object or class. Examples:
+ #
+ # dom_class(post) # => "post"
+ # dom_class(Person) # => "person"
+ #
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
+ #
+ # dom_class(post, :edit) # => "edit_post"
+ # dom_class(Person, :edit) # => "edit_person"
+ def dom_class(record_or_class, prefix = nil)
+ [ prefix, singular_class_name(record_or_class) ].compact * '_'
+ end
+
+ # The DOM class convention is to use the singular form of an object or class with the id following an underscore.
+ # If no id is found, prefix with "new_" instead. Examples:
+ #
+ # dom_class(Post.new(:id => 45)) # => "post_45"
+ # dom_class(Post.new) # => "new_post"
+ #
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
+ #
+ # dom_class(Post.new(:id => 45), :edit) # => "edit_post_45"
+ def dom_id(record, prefix = nil)
+ prefix ||= 'new' unless record.id
+ [ prefix, singular_class_name(record), record.id ].compact * '_'
+ end
+
+ # Returns the plural class name of a record or class. Examples:
+ #
+ # plural_class_name(post) # => "posts"
+ # plural_class_name(Highrise::Person) # => "highrise_people"
+ def plural_class_name(record_or_class)
+ singular_class_name(record_or_class).pluralize
+ end
+
+ # Returns the singular class name of a record or class. Examples:
+ #
+ # singular_class_name(post) # => "post"
+ # singular_class_name(Highrise::Person) # => "highrise_person"
+ def singular_class_name(record_or_class)
+ class_from_record_or_class(record_or_class).name.underscore.tr('/', '_')
+ end
+
+ private
+ def class_from_record_or_class(record_or_class)
+ record_or_class.is_a?(Class) ? record_or_class : record_or_class.class
+ end
+ end
+end
@@ -384,8 +384,19 @@ def to_s #:nodoc:
# page['blank_slate'] # => $('blank_slate');
# page['blank_slate'].show # => $('blank_slate').show();
# page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
+ #
+ # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
+ # the correct id:
+ #
+ # page[@post] # => $('post_45')
+ # page[Post.new] # => $('new_post')
def [](id)
- JavaScriptElementProxy.new(self, id)
+ case id
+ when String, Symbol, NilClass
+ JavaScriptElementProxy.new(self, id)
+ else
+ JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
+ end
end
# Returns an object whose <tt>#to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
@@ -0,0 +1,16 @@
+module RecordIdentificationHelper
+ # See ActionController::RecordIdentifier.partial_path -- this is just a delegate to that for convenient access in the view.
+ def partial_path(*args, &block)
+ ActionController::RecordIdentifier.partial_path(*args, &block)
+ end
+
+ # See ActionController::RecordIdentifier.dom_class -- this is just a delegate to that for convenient access in the view.
+ def dom_class(*args, &block)
+ ActionController::RecordIdentifier.dom_class(*args, &block)
+ end
+
+ # See ActionController::RecordIdentifier.dom_id -- this is just a delegate to that for convenient access in the view.
+ def dom_id(*args, &block)
+ ActionController::RecordIdentifier.dom_id(*args, &block)
+ end
+end
@@ -0,0 +1,55 @@
+module RecordTagHelper
+ # Produces a wrapper DIV element with id and class parameters that
+ # relate to the specified ActiveRecord object. Usage example:
+ #
+ # <% div_for(@person, :class => "foo") do %>
+ # <%=h @person.name %>
+ # <% end %>
+ #
+ # produces:
+ #
+ # <div id="person_123" class="person foo"> Joe Bloggs </div>
+ #
+ def div_for(record, *args, &block)
+ content_tag_for(:div, record, *args, &block)
+ end
+
+ # content_tag_for creates an HTML element with id and class parameters
+ # that relate to the specified ActiveRecord object. For example:
+ #
+ # <% content_tag_for(:tr, @person) do %>
+ # <td><%=h @person.first_name %></td>
+ # <td><%=h @person.last_name %></td>
+ # <% end %>
+ #
+ # would produce hthe following HTML (assuming @person is an instance of
+ # a Person object, with an id value of 123):
+ #
+ # <tr id="person_123" class="person">....</tr>
+ #
+ # If you require the HTML id attribute to have a prefix, you can specify it:
+ #
+ # <% content_tag_for(:tr, @person, :foo) do %> ...
+ #
+ # produces:
+ #
+ # <tr id="foo_person_123" class="person">...
+ #
+ # content_tag_for also accepts a hash of options, which will be converted to
+ # additional HTML attributes. If you specify a +:class+ value, it will be combined
+ # with the default class name for your object. For example:
+ #
+ # <% content_tag_for(:li, @person, :class => "bar") %>...
+ #
+ # produces:
+ #
+ # <li id="person_123" class="person bar">...
+ #
+ def content_tag_for(tag_name, record, *args, &block)
+ prefix = args.first.is_a?(Hash) ? nil : args.shift
+ options = args.first.is_a?(Hash) ? args.shift : {}
+ concat content_tag(tag_name, capture(&block),
+ options.merge({ :class => "#{dom_class(record)} #{options[:class]}".strip, :id => dom_id(record, prefix) })),
+ block.binding
+ end
+end
@@ -0,0 +1,102 @@
+require File.dirname(__FILE__) + '/../abstract_unit'
+
+class Post
+ attr_reader :id
+ def save; @id = 1 end
+ def new_record?; @id.nil? end
+ def name
+ @id.nil? ? 'new post' : "post ##{@id}"
+ end
+ class Nested < Post; end
+end
+
+class Test::Unit::TestCase
+ protected
+ def posts_url
+ 'http://www.example.com/posts'
+ end
+
+ def post_url(post)
+ "http://www.example.com/posts/#{post.id}"
+ end
+end
+
+
+class RecordIdentifierTest < Test::Unit::TestCase
+ include ActionController::RecordIdentifier
+
+ def setup
+ @klass = Post
+ @record = @klass.new
+ @singular = 'post'
+ @plural = 'posts'
+ end
+
+ def test_dom_id_with_new_record
+ assert_equal "new_#{@singular}", dom_id(@record)
+ end
+
+ def test_dom_id_with_new_record_and_prefix
+ assert_equal "custom_prefix_#{@singular}", dom_id(@record, :custom_prefix)
+ end
+
+ def test_dom_id_with_saved_record
+ @record.save
+ assert_equal "#{@singular}_1", dom_id(@record)
+ end
+
+ def test_dom_id_with_prefix
+ @record.save
+ assert_equal "edit_#{@singular}_1", dom_id(@record, :edit)
+ end
+
+ def test_partial_path
+ expected = "#{@plural}/#{@singular}"
+ assert_equal expected, partial_path(@record)
+ assert_equal expected, partial_path(Post)
+ end
+
+ def test_dom_class
+ assert_equal @singular, dom_class(@record)
+ end
+
+ def test_dom_class_with_prefix
+ assert_equal "custom_prefix_#{@singular}", dom_class(@record, :custom_prefix)
+ end
+
+ def test_singular_class_name
+ assert_equal @singular, singular_class_name(@record)
+ end
+
+ def test_singular_class_name_for_class
+ assert_equal @singular, singular_class_name(@klass)
+ end
+
+ def test_plural_class_name
+ assert_equal @plural, plural_class_name(@record)
+ end
+
+ def test_plural_class_name_for_class
+ assert_equal @plural, plural_class_name(@klass)
+ end
+
+ private
+ def method_missing(method, *args)
+ RecordIdentifier.send(method, *args)
+ end
+end
+
+class NestedRecordIdentifierTest < RecordIdentifierTest
+ def setup
+ @klass = Post::Nested
+ @record = @klass.new
+ @singular = 'post_nested'
+ @plural = 'post_nesteds'
+ end
+
+ def test_partial_path
+ expected = "post/nesteds/nested"
+ assert_equal expected, partial_path(@record)
+ assert_equal expected, partial_path(Post::Nested)
+ end
+end
@@ -1,5 +1,7 @@
require "#{File.dirname(__FILE__)}/../abstract_unit"
+Bunny = Struct.new(:Bunny, :id)
+
module BaseTest
include ActionView::Helpers::JavaScriptHelper
include ActionView::Helpers::PrototypeHelper
@@ -253,6 +255,11 @@ def test_element_access
assert_equal %($("hello");), @generator['hello']
end
+ def test_element_access_on_records
+ assert_equal %($("bunny_5");), @generator[Bunny.new(:id => 5)]
+ assert_equal %($("new_bunny");), @generator[Bunny.new]
+ end
+
def test_element_proxy_one_deep
@generator['hello'].hide
assert_equal %($("hello").hide();), @generator.to_s

0 comments on commit da257eb

Please sign in to comment.