table_helper 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +10 -0
- data/LICENSE +1 -1
- data/README.rdoc +78 -67
- data/Rakefile +1 -1
- data/lib/table_helper.rb +200 -169
- data/lib/table_helper/body.rb +46 -49
- data/lib/table_helper/body_row.rb +18 -14
- data/lib/table_helper/cell.rb +33 -22
- data/lib/table_helper/collection_table.rb +111 -37
- data/lib/table_helper/footer.rb +9 -7
- data/lib/table_helper/header.rb +31 -93
- data/lib/table_helper/html_element.rb +1 -3
- data/lib/table_helper/row.rb +16 -7
- data/test/helpers/table_helper_test.rb +20 -12
- data/test/unit/body_row_test.rb +66 -16
- data/test/unit/body_test.rb +159 -131
- data/test/unit/cell_test.rb +90 -16
- data/test/unit/collection_table_test.rb +166 -192
- data/test/unit/footer_test.rb +33 -8
- data/test/unit/header_test.rb +136 -114
- data/test/unit/row_builder_test.rb +16 -8
- data/test/unit/row_test.rb +106 -45
- metadata +3 -5
- data/test/unit/header_builder_test.rb +0 -48
data/lib/table_helper/body.rb
CHANGED
@@ -4,34 +4,40 @@ module TableHelper
|
|
4
4
|
# Represents the body of the table. In HTML, you can think of this as
|
5
5
|
# the <tbody> tag of the table.
|
6
6
|
class Body < HtmlElement
|
7
|
+
# The css class to apply for all rows in the body
|
8
|
+
cattr_accessor :empty_caption_class
|
9
|
+
@@empty_caption_class = 'ui-collection-empty'
|
10
|
+
|
11
|
+
# The table this body is a part of
|
12
|
+
attr_reader :table
|
13
|
+
|
7
14
|
# If set to :odd or :even, every odd or even-numbered row will have the
|
8
|
-
# class
|
9
|
-
attr_accessor :
|
15
|
+
# alternate class appended to its html attributes. Default is nil.
|
16
|
+
attr_accessor :alternate
|
10
17
|
|
11
18
|
# The caption to display in the collection is empty
|
12
19
|
attr_accessor :empty_caption
|
13
20
|
|
14
|
-
def initialize(
|
21
|
+
def initialize(table) #:nodoc:
|
15
22
|
super()
|
16
23
|
|
17
|
-
@
|
24
|
+
@table = table
|
18
25
|
@empty_caption = 'No matches found.'
|
19
26
|
end
|
20
27
|
|
21
|
-
def
|
22
|
-
raise ArgumentError, '
|
23
|
-
@
|
28
|
+
def alternate=(value) #:nodoc:
|
29
|
+
raise ArgumentError, 'alternate must be set to :odd or :even' if value && ![:odd, :even].include?(value)
|
30
|
+
@alternate = value
|
24
31
|
end
|
25
32
|
|
26
33
|
# Builds the body of the table. This includes the actual data that is
|
27
34
|
# generated for each object in the collection.
|
28
35
|
#
|
29
|
-
# +
|
30
|
-
# iteration of the block will provide the
|
31
|
-
#
|
32
|
-
# example,
|
36
|
+
# +each+ expects a block that defines the data in each cell. Each
|
37
|
+
# iteration of the block will provide the row within the table, the object
|
38
|
+
# being rendered, and the index of the object. For example,
|
33
39
|
#
|
34
|
-
#
|
40
|
+
# t.rows.each do |row, post, index|
|
35
41
|
# row.title "<div class=\"wrapped\">#{post.title}</div>"
|
36
42
|
# row.category post.category.name
|
37
43
|
# end
|
@@ -51,66 +57,57 @@ module TableHelper
|
|
51
57
|
# if a Post consists of the attribute +title+, then the cell for the
|
52
58
|
# title will be prepopulated with that attribute's value:
|
53
59
|
#
|
54
|
-
#
|
55
|
-
# row.
|
60
|
+
# t.rows.each do |row, post index|
|
61
|
+
# row.title post.title
|
56
62
|
# end
|
57
63
|
#
|
58
64
|
# <tt>row.title</tt> is already set to post.category so there's no need to
|
59
65
|
# manually set the value of that cell. However, it is always possible
|
60
66
|
# to override the default value like so:
|
61
67
|
#
|
62
|
-
#
|
68
|
+
# t.rows.each do |row, post, index|
|
63
69
|
# row.title link_to(post.title, post_url(post))
|
64
70
|
# row.category post.category.name
|
65
71
|
# end
|
66
|
-
def
|
67
|
-
@
|
68
|
-
|
69
|
-
# Display nothing if there are no objects to display
|
70
|
-
if @collection.empty? && @empty_caption
|
71
|
-
row = Row.new
|
72
|
-
row[:class] = 'no_content'
|
73
|
-
|
74
|
-
html_options = {}
|
75
|
-
html_options[:colspan] = @header.column_names.size if @header.column_names.size > 1
|
76
|
-
row.cell nil, @empty_caption, html_options
|
77
|
-
|
78
|
-
@content << row.html
|
79
|
-
else
|
80
|
-
@collection.each_with_index do |object, i|
|
81
|
-
@content << build_row(object, i, &block)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
@content
|
72
|
+
def each(&block)
|
73
|
+
@builder = block
|
86
74
|
end
|
87
75
|
|
88
76
|
# Builds a row for an object in the table.
|
89
77
|
#
|
90
78
|
# The provided block should set the values for each cell in the row.
|
91
|
-
def build_row(object, index =
|
92
|
-
row = BodyRow.new(object,
|
93
|
-
row.alternate =
|
94
|
-
|
95
|
-
yield row.builder, object, index if block_given?
|
96
|
-
|
79
|
+
def build_row(object, index = table.collection.index(object))
|
80
|
+
row = BodyRow.new(object, self)
|
81
|
+
row.alternate = alternate ? index.send("#{alternate}?") : false
|
82
|
+
@builder.call(row.builder, object, index) if @builder
|
97
83
|
row.html
|
98
84
|
end
|
99
85
|
|
100
|
-
def html #:nodoc:
|
101
|
-
html_options = @html_options.dup
|
102
|
-
html_options[:class] = (html_options[:class].to_s + ' alternate').strip if alternate_rows
|
103
|
-
|
104
|
-
content_tag(tag_name, content, html_options)
|
105
|
-
end
|
106
|
-
|
107
86
|
private
|
108
87
|
def tag_name
|
109
88
|
'tbody'
|
110
89
|
end
|
111
90
|
|
112
91
|
def content
|
113
|
-
|
92
|
+
content = ''
|
93
|
+
|
94
|
+
if table.empty? && @empty_caption
|
95
|
+
# No objects to display
|
96
|
+
row = Row.new(self)
|
97
|
+
row[:class] = empty_caption_class
|
98
|
+
|
99
|
+
html_options = {}
|
100
|
+
html_options[:colspan] = table.header.columns.size if table.header.columns.size > 1
|
101
|
+
row.cell nil, @empty_caption, html_options
|
102
|
+
|
103
|
+
content << row.html
|
104
|
+
else
|
105
|
+
table.collection.each_with_index do |object, i|
|
106
|
+
content << build_row(object, i)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
content
|
114
111
|
end
|
115
112
|
end
|
116
113
|
end
|
@@ -9,28 +9,32 @@ module TableHelper
|
|
9
9
|
# Alternating rows can be automated by setting the +alternate+ property.
|
10
10
|
# For example,
|
11
11
|
#
|
12
|
-
# r = BodyRow.new
|
12
|
+
# r = BodyRow.new(object, parent)
|
13
13
|
# r.alternate = true
|
14
14
|
class BodyRow < Row
|
15
|
+
# The css class to apply for all rows in the body
|
16
|
+
cattr_accessor :result_class
|
17
|
+
@@result_class = 'ui-collection-result'
|
18
|
+
|
19
|
+
# The css class to apply for rows that are marked as "alternate"
|
20
|
+
cattr_accessor :alternate_class
|
21
|
+
@@alternate_class = 'ui-state-alternate'
|
22
|
+
|
15
23
|
# True if this is an alternating row, otherwise false. Default is false.
|
16
24
|
attr_accessor :alternate
|
17
25
|
|
18
|
-
def initialize(object,
|
19
|
-
super()
|
26
|
+
def initialize(object, parent) #:nodoc:
|
27
|
+
super(parent)
|
20
28
|
|
21
|
-
@header = header
|
22
29
|
@alternate = false
|
23
|
-
|
30
|
+
self[:class] = "#{self[:class]} #{table.object_name} #{result_class}".strip
|
24
31
|
|
25
32
|
# For each column defined in the table, see if we can prepopulate the
|
26
33
|
# cell based on the data in the object. If not, we can at least
|
27
34
|
# provide shortcut accessors to the cell
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
else
|
32
|
-
builder.define_cell(column)
|
33
|
-
end
|
35
|
+
table.header.column_names.each do |column|
|
36
|
+
value = object.respond_to?(column) ? object.send(column) : nil
|
37
|
+
cell(column, value)
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
@@ -38,7 +42,7 @@ module TableHelper
|
|
38
42
|
# (if specified)
|
39
43
|
def html
|
40
44
|
original_options = @html_options.dup
|
41
|
-
|
45
|
+
self[:class] = "#{self[:class]} #{alternate_class}".strip if alternate
|
42
46
|
html = super
|
43
47
|
@html_options = original_options
|
44
48
|
html
|
@@ -52,13 +56,13 @@ module TableHelper
|
|
52
56
|
number_to_skip = 0 # Keeps track of the # of columns to skip
|
53
57
|
|
54
58
|
html = ''
|
55
|
-
|
59
|
+
table.header.column_names.each do |column|
|
56
60
|
number_to_skip -= 1 and next if number_to_skip > 0
|
57
61
|
|
58
62
|
if cell = @cells[column]
|
59
63
|
number_to_skip = (cell[:colspan] || 1) - 1
|
60
64
|
else
|
61
|
-
cell = Cell.new(column,
|
65
|
+
cell = Cell.new(column, nil)
|
62
66
|
end
|
63
67
|
|
64
68
|
html << cell.html
|
data/lib/table_helper/cell.rb
CHANGED
@@ -1,31 +1,46 @@
|
|
1
1
|
module TableHelper
|
2
2
|
# Represents a single cell within a table. This can either be a regular
|
3
3
|
# data cell (td) or a header cell (th). By default, all cells will have
|
4
|
-
# their
|
4
|
+
# their name appended to the cell's class attribute.
|
5
5
|
#
|
6
|
-
# ==
|
7
|
-
#
|
8
|
-
# Cell.new(:author, 'John Doe').build
|
9
|
-
#
|
10
|
-
# ...would generate the following tag:
|
11
|
-
#
|
12
|
-
# <td class="author">John Doe</td>
|
13
|
-
#
|
14
|
-
# == Creating header cells
|
6
|
+
# == Examples
|
15
7
|
#
|
8
|
+
# # Data cell
|
9
|
+
# c = Cell.new(:author, 'John Doe')
|
10
|
+
# c.html # => <td class="author">John Doe</td>
|
11
|
+
#
|
12
|
+
# # Header cell
|
16
13
|
# c = Cell.new(:author, 'Author Name')
|
17
14
|
# c.content_type = :header
|
18
|
-
# c.
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
15
|
+
# c.html
|
16
|
+
#
|
17
|
+
# # With namespace
|
18
|
+
# c = Cell.new(:author, :namespace => 'post')
|
19
|
+
# c.content_type = :header
|
20
|
+
# c.html # => <td class="post-author">Author</td>
|
23
21
|
class Cell < HtmlElement
|
24
|
-
|
22
|
+
# The css class to apply to empty cells
|
23
|
+
cattr_accessor :empty_class
|
24
|
+
@@empty_class = 'ui-state-empty'
|
25
|
+
|
26
|
+
# The content to display within the cell
|
27
|
+
attr_reader :content
|
28
|
+
|
29
|
+
# The type of content this cell represents (:data or :header)
|
30
|
+
attr_reader :content_type
|
31
|
+
|
32
|
+
def initialize(name, content = name.to_s.titleize, html_options = {}) #:nodoc
|
33
|
+
html_options, content = content, name.to_s.titleize if content.is_a?(Hash)
|
34
|
+
namespace = html_options.delete(:namespace)
|
25
35
|
super(html_options)
|
26
36
|
|
27
|
-
@content = content
|
28
|
-
|
37
|
+
@content = content.to_s
|
38
|
+
|
39
|
+
if name
|
40
|
+
name = "#{namespace}-#{name}" unless namespace.blank?
|
41
|
+
self[:class] = "#{self[:class]} #{name}".strip
|
42
|
+
end
|
43
|
+
self[:class] = "#{self[:class]} #{empty_class}".strip if content.blank?
|
29
44
|
|
30
45
|
self.content_type = :data
|
31
46
|
end
|
@@ -41,9 +56,5 @@ module TableHelper
|
|
41
56
|
def tag_name
|
42
57
|
@content_type == :data ? 'td' : 'th'
|
43
58
|
end
|
44
|
-
|
45
|
-
def content
|
46
|
-
@content
|
47
|
-
end
|
48
59
|
end
|
49
60
|
end
|
@@ -7,58 +7,132 @@ module TableHelper
|
|
7
7
|
# Represents a table that displays data for multiple objects within a
|
8
8
|
# collection.
|
9
9
|
class CollectionTable < HtmlElement
|
10
|
-
#
|
10
|
+
# The css class to apply for all menu bars
|
11
|
+
cattr_accessor :collection_class
|
12
|
+
@@collection_class = 'ui-collection'
|
13
|
+
|
14
|
+
# The class of objects that will be rendered in the table
|
15
|
+
attr_reader :klass
|
16
|
+
|
17
|
+
# The name of the objects in the collection. By default, this is the
|
18
|
+
# underscored name of the class of objects being rendered. This value is
|
19
|
+
# used for generates parts of the css classes in the table, such as rows
|
20
|
+
# and cells.
|
21
|
+
#
|
22
|
+
# For example, if the class is Post, then the object name will be "post".
|
23
|
+
attr_accessor :object_name
|
24
|
+
|
25
|
+
# The actual collection of objects being rendered in the table's body.
|
26
|
+
# This collection will also be used to help automatically determine what
|
27
|
+
# the class is.
|
28
|
+
attr_reader :collection
|
29
|
+
|
30
|
+
# The body of rows that will be generated for each object in the
|
31
|
+
# collection. The actual content for each row and the html options can
|
32
|
+
# be configured as well.
|
33
|
+
#
|
34
|
+
# See TableHelper::Body for more information.
|
11
35
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
36
|
+
# == Examples
|
37
|
+
#
|
38
|
+
# # Defining rows
|
39
|
+
# t.rows.each do |row, post, index|
|
40
|
+
# row.category post.category.name
|
41
|
+
# row.author post.author.name
|
42
|
+
# ...
|
43
|
+
# row[:style] = 'margin-top: 5px;'
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # Customizing html options
|
47
|
+
# t.rows[:class] = 'pretty'
|
48
|
+
attr_reader :rows
|
49
|
+
|
50
|
+
# Creates a new table based on the objects within the given collection
|
51
|
+
def initialize(collection, klass = nil, html_options = {}) #:nodoc:
|
52
|
+
html_options, klass = klass, nil if klass.is_a?(Hash)
|
17
53
|
super(html_options)
|
18
54
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
:footer => false
|
27
|
-
)
|
55
|
+
@collection = collection
|
56
|
+
@klass = klass || default_class
|
57
|
+
@object_name = @klass ? @klass.name.split('::').last.underscore : nil
|
58
|
+
@html_options.reverse_merge!(:cellspacing => '0', :cellpadding => '0')
|
59
|
+
|
60
|
+
self[:class] = "#{self[:class]} #{@object_name.pluralize}".strip if @object_name
|
61
|
+
self[:class] = "#{self[:class]} #{collection_class}".strip
|
28
62
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
)
|
63
|
+
# Build actual content
|
64
|
+
@header = Header.new(self)
|
65
|
+
@rows = Body.new(self)
|
66
|
+
@footer = Footer.new(self)
|
33
67
|
|
34
|
-
|
35
|
-
@body = Body.new(collection, @header)
|
36
|
-
@footer = Footer.new(collection)
|
68
|
+
yield self if block_given?
|
37
69
|
end
|
38
70
|
|
39
|
-
#
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
71
|
+
# Will any rows be generated in the table's body?
|
72
|
+
def empty?
|
73
|
+
collection.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates a new header cell with the given name. Headers must be defined
|
77
|
+
# in the order in which they will be rendered.
|
78
|
+
#
|
79
|
+
# The actual caption and html options for the header can be configured
|
80
|
+
# as well. The caption determines what will be displayed in each cell.
|
81
|
+
#
|
82
|
+
# == Examples
|
83
|
+
#
|
84
|
+
# # Adding headers
|
85
|
+
# t.header :title # => <th class="post-title">Title</th>
|
86
|
+
# t.header :title, 'The Title' # => <th class="post-title">The Title</th>
|
87
|
+
# t.header :title, :class => 'pretty' # => <th class="post-title pretty">Title</th>
|
88
|
+
# t.header :title, 'The Title', :class => 'pretty' # => <th class="post-title pretty">The Title</th>
|
89
|
+
#
|
90
|
+
# # Customizing html options
|
91
|
+
# t.header[:class] = 'pretty'
|
92
|
+
def header(*args)
|
93
|
+
args.empty? ? @header : @header.column(*args)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Creates a new footer cell with the given name. Footers must be defined
|
97
|
+
# in the order in which they will be rendered.
|
98
|
+
#
|
99
|
+
# The actual caption and html options for the footer can be configured
|
100
|
+
# as well. The caption determines what will be displayed in each cell.
|
101
|
+
#
|
102
|
+
# == Examples
|
103
|
+
#
|
104
|
+
# # Adding footers
|
105
|
+
# t.footer :total # => <td class="post-total"></th>
|
106
|
+
# t.footer :total, 5 # => <td class="post-total">5</th>
|
107
|
+
# t.footer :total, :class => 'pretty' # => <td class="post-total pretty"></th>
|
108
|
+
# t.footer :total, 5, :class => 'pretty' # => <td class="post-total pretty">5</th>
|
109
|
+
#
|
110
|
+
# # Customizing html options
|
111
|
+
# t.footer[:class] = 'pretty'
|
112
|
+
def footer(*args)
|
113
|
+
args.empty? ? @footer : @footer.cell(*args)
|
53
114
|
end
|
54
115
|
|
55
116
|
private
|
117
|
+
# Finds the class representing the objects within the collection
|
118
|
+
def default_class
|
119
|
+
if collection.respond_to?(:proxy_reflection)
|
120
|
+
collection.proxy_reflection.klass
|
121
|
+
elsif !collection.empty?
|
122
|
+
collection.first.class
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
56
126
|
def tag_name
|
57
127
|
'table'
|
58
128
|
end
|
59
129
|
|
60
130
|
def content
|
61
|
-
|
131
|
+
content = ''
|
132
|
+
content << @header.html unless @header.empty?
|
133
|
+
content << @rows.html
|
134
|
+
content << @footer.html unless @footer.empty?
|
135
|
+
content
|
62
136
|
end
|
63
137
|
end
|
64
138
|
end
|