Permalink
Browse files

Support cell/column names that conflict with existing method names on…

… the Row/Header classes

Avoid string evaluation for dynamic methods
  • Loading branch information...
1 parent 112e5a6 commit ff61e5a73c3a2b39ef810e5caeb821d2c074e15a @obrie obrie committed Jun 15, 2008
View
@@ -1,5 +1,11 @@
*SVN*
+*0.0.4* (June 15th, 2008)
+
+* Support cell/column names that conflict with existing method names on the Row/Header classes
+
+* Avoid string evaluation for dynamic methods
+
*0.0.3* (June 1st, 2008)
* Remove dependency on set_or_append
View
@@ -4,7 +4,7 @@ require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
PKG_NAME = 'table_helper'
-PKG_VERSION = '0.0.3'
+PKG_VERSION = '0.0.4'
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
RUBY_FORGE_PROJECT = 'pluginaweek'
@@ -94,7 +94,7 @@ def build_row(object, index = @collection.index(object), &block)
row = BodyRow.new(object, @header)
row.alternate = alternate_rows ? index.send("#{@alternate_rows}?") : false
- yield row, object, index if block_given?
+ yield row.builder, object, index if block_given?
row.html
end
@@ -44,7 +44,7 @@ def initialize(object, header) #:nodoc:
if object.respond_to?(column)
cell(column, object.send(column))
else
- define_cell_accessor(column)
+ builder.define_cell(column)
end
end
end
@@ -34,7 +34,7 @@ def initialize(class_name, content = class_name.to_s.titleize, html_options = {}
# Indicates what type of content will be stored in this cell. This can
# either be set to either :data or :header.
def content_type=(value)
- raise ArgumentError, "content_type must be set to :data or :header, was: #{value.inspect}" if ![:data, :header].include?(value)
+ raise ArgumentError, "content_type must be set to :data or :header, was: #{value.inspect}" unless [:data, :header].include?(value)
@content_type = value
end
@@ -36,7 +36,7 @@ def build(&block)
@body.build # Build with the defaults
elements = []
- elements << @header if @options[:header]
+ elements << @header.builder if @options[:header]
elements << @body
elements << @footer if @options[:footer]
@@ -2,11 +2,56 @@
module PluginAWeek #:nodoc:
module TableHelper
+ # Provides a blank class that can be used to build the columns for a header
+ class HeaderBuilder < BlankSlate
+ reveal :respond_to?
+
+ attr_reader :header
+
+ # Creates a builder for the given header
+ def initialize(header)
+ @header = header
+ end
+
+ # Proxies all missed methods to the header
+ def method_missing(*args)
+ header.send(*args)
+ end
+
+ # Defines the accessor method for the given column name. For example, if
+ # a column with the name :title was defined, then the column would be able
+ # to be read and written like so:
+ #
+ # header.title #=> Accesses the title
+ # header.title "Page Title" #=> Creates a new column with "Page Title" as the content
+ def define_column(name)
+ method_name = name.to_s.gsub('-', '_')
+
+ klass = class << self; self; end
+ klass.class_eval do
+ define_method(method_name) do |*args|
+ header.row.builder.__send__(method_name, *args)
+ end
+ end unless klass.method_defined?(method_name)
+ end
+
+ # Removes the definition for the given cp;i,m
+ def undef_column(name)
+ klass = class << self; self; end
+ klass.class_eval do
+ remove_method(name.gsub('-', '_'))
+ end
+ end
+ end
+
# Represents the header of the table. In HTML, you can think of this as
# the <thead> tag of the table.
class Header < HtmlElement
# The actual header row
- attr_reader :row
+ attr_reader :row
+
+ # The proxy class used externally to build the actual columns
+ attr_reader :builder
# Whether or not the header should be hidden when the collection is
# empty. Default is true.
@@ -23,6 +68,7 @@ def initialize(collection, klass = nil)
@collection = collection
@row = Row.new
+ @builder = HeaderBuilder.new(self)
@hide_when_empty = true
@customized = true
@@ -38,11 +84,23 @@ def initialize(collection, klass = nil)
end
end
+ # The current columns in this header, in the order in which they will be built
+ def columns
+ row.cells
+ end
+
# Gets the names of all of the columns being displayed in the table
def column_names
row.cell_names
end
+ # Clears all of the current columns from the header
+ def clear
+ # Remove all of the shortcut methods
+ column_names.each {|name| builder.undef_column(name)}
+ row.clear
+ end
+
# Creates a new column with the specified caption. Columns must be
# defined in the order in which they will be rendered.
#
@@ -61,37 +119,16 @@ def column_names
# header.column :title, 'The Title', :class => 'pretty'
def column(name, *args)
# Clear the header row if this is being customized by the user
- if !@customized
+ unless @customized
@customized = true
-
- # Remove all of the shortcut methods
- column_names.each do |column|
- klass = class << self; self; end
- klass.class_eval do
- remove_method(column)
- end
- end
-
- @row.clear
+ clear
end
- column = @row.cell(name, *args)
+ column = row.cell(name, *args)
column.content_type = :header
column[:scope] ||= 'col'
- # Define a shortcut method to the cell
- name = name.to_s.gsub('-', '_')
- unless respond_to?(name)
- instance_eval <<-end_eval
- def #{name}(*args)
- if args.empty?
- @row.#{name}
- else
- @row.#{name}(*args)
- end
- end
- end_eval
- end
+ builder.define_column(name)
column
end
@@ -105,21 +142,22 @@ def html
end
private
- def tag_name
- 'thead'
- end
-
- def content
- @row.html
- end
-
+ # Finds the class representing the objects within the collection
def class_for_collection(collection)
if collection.respond_to?(:proxy_reflection)
collection.proxy_reflection.klass
elsif !collection.empty?
collection.first.class
end
end
+
+ def tag_name
+ 'thead'
+ end
+
+ def content
+ @row.html
+ end
end
end
end
@@ -2,13 +2,68 @@
module PluginAWeek #:nodoc:
module TableHelper
+ # Provides a blank class that can be used to build the cells for a row
+ class RowBuilder < BlankSlate
+ reveal :respond_to?
+
+ attr_reader :row
+
+ # Creates a builder for the given row
+ def initialize(row)
+ @row = row
+ end
+
+ # Proxies all missed methods to the row
+ def method_missing(*args)
+ row.send(*args)
+ end
+
+ # Defines the builder method for the given cell name. For example, if
+ # a cell with the name :title was defined, then the cell would be able
+ # to be read and written like so:
+ #
+ # row.title #=> Accesses the title
+ # row.title "Page Title" #=> Creates a new cell with "Page Title" as the content
+ def define_cell(name)
+ method_name = name.gsub('-', '_')
+
+ klass = class << self; self; end
+ klass.class_eval do
+ define_method(method_name) do |*args|
+ if args.empty?
+ row.cells[name]
+ else
+ row.cell(name, *args)
+ end
+ end
+ end unless klass.method_defined?(method_name)
+ end
+
+ # Removes the definition for the given cell
+ def undef_cell(name)
+ method_name = name.gsub('-', '_')
+
+ klass = class << self; self; end
+ klass.class_eval do
+ remove_method(method_name)
+ end
+ end
+ end
+
# Represents a single row within a table. A row can consist of either
# data cells or header cells.
class Row < HtmlElement
+ # The proxy class used externally to build the actual cells
+ attr_reader :builder
+
+ # The current cells in this row, in the order in which they will be built
+ attr_reader :cells
+
def initialize #:nodoc:
super
@cells = ActiveSupport::OrderedHash.new
+ @builder = RowBuilder.new(self)
end
# Creates a new cell with the given name and generates shortcut
@@ -17,55 +72,31 @@ def cell(name, *args)
name = name.to_s if name
cell = Cell.new(name, *args)
- @cells[name] = cell
-
- define_cell_accessor(name) if name && !respond_to?(name)
+ cells[name] = cell
+ builder.define_cell(name) if name
cell
end
# The names of all cells in this row
def cell_names
- @cells.keys
+ cells.keys
end
# Clears all of the current cells from the row
def clear
- cell_names.each do |name|
- klass = class << self; self; end
- klass.class_eval do
- remove_method(name)
- end
- end
-
- @cells.clear
+ # Remove all of the shortcut methods
+ cell_names.each {|name| builder.undef_cell(name)}
+ cells.clear
end
private
- # Defines the accessor method for the given cell name. For example, if
- # a cell with the name :title was defined, then the cell would be able
- # to be read and written like so:
- #
- # row.title #=> Accesses the title
- # row.title "Page Title" #=> Creates a new cell with "Page Title" as the content
- def define_cell_accessor(name)
- instance_eval <<-end_eval
- def #{name.gsub('-', '_')}(*args)
- if args.empty?
- @cells[#{name.inspect}]
- else
- cell(#{name.inspect}, *args)
- end
- end
- end_eval
- end
-
def tag_name
'tr'
end
def content
- @cells.values.map(&:html).join
+ cells.values.map(&:html).join
end
end
end
View
@@ -29,11 +29,11 @@ def setup
end
def test_should_generate_cell_accessors
- assert @row.respond_to?(:title)
+ assert_nothing_raised {@row.builder.title}
end
def test_should_override_default_cell_content_if_cell_specified
- @row.title 'Hello World'
+ @row.builder.title 'Hello World'
assert_equal '<tr class="row"><td class="title">Hello World</td></tr>', @row.html
end
end
@@ -64,7 +64,7 @@ def setup
end
def test_should_use_attribute_values_as_cell_content
- @row.author_name 'John Doe'
+ @row.builder.author_name 'John Doe'
assert_equal '<tr class="row"><td class="title">Default Value</td><td class="author_name">John Doe</td></tr>', @row.html
end
end
@@ -82,12 +82,12 @@ def test_should_build_missing_cells_if_cells_not_specified
end
def test_should_skip_missing_cells_if_colspan_replaces_missing_cells
- @row.title 'Hello World', :colspan => 2
+ @row.builder.title 'Hello World', :colspan => 2
assert_equal '<tr class="row"><td class="title" colspan="2">Hello World</td></tr>', @row.html
end
def test_should_not_skip_missing_cells_if_colspan_doesnt_replace_missing_cells
- @row.title 'Hello World'
+ @row.builder.title 'Hello World'
assert_equal '<tr class="row"><td class="title">Hello World</td><td class="author_name empty"></td></tr>', @row.html
end
end
Oops, something went wrong.

0 comments on commit ff61e5a

Please sign in to comment.