tablette 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +169 -0
- data/Rakefile +5 -0
- data/lib/tablette/columns.rb +84 -0
- data/lib/tablette/element/configuration.rb +14 -0
- data/lib/tablette/element/nesting.rb +88 -0
- data/lib/tablette/element/rendering.rb +58 -0
- data/lib/tablette/element.rb +51 -0
- data/lib/tablette/html_renderer.rb +44 -0
- data/lib/tablette/rows.rb +70 -0
- data/lib/tablette/sections.rb +69 -0
- data/lib/tablette/table.rb +49 -0
- data/lib/tablette/version.rb +3 -0
- data/lib/tablette.rb +16 -0
- data/spec/grid_spec.rb +76 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/sample_table.rb +103 -0
- data/tablette.gemspec +25 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d1ea4083f7a84daf2dc1cdd21a1fddce42ab0e08
|
4
|
+
data.tar.gz: a194c4045ce19b05f0ec69c022c239cb7cef32a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91950838b5802f5c25485d8bc1cb872d835501115cbc2b457105c12821898a3efa5dd157c2bd2900ad66e1f13bec45d1592fc1b4db9d1411945b78a5970e00fc
|
7
|
+
data.tar.gz: 723deac30f5dcf8f06a6f9580ea607dd1c99eb4a7016f1b6005a31a1bb6f7311c1d301f094583932f0b2a80f17933ffe8c54f00f787cba3672661cbbf86451e0
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Victor Sokolov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
# Tablette
|
2
|
+
|
3
|
+
Inspired by discussion at: https://github.com/evilmartians/slashadmin/issues/3.
|
4
|
+
Rails table renderer that tries to be flexible.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'tablette'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install tablette
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Somwhere in your app:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
short_table = Tablette::Table.new do
|
26
|
+
column :id
|
27
|
+
column :name
|
28
|
+
end
|
29
|
+
|
30
|
+
puts short_table.to_html(collection, User)
|
31
|
+
```
|
32
|
+
|
33
|
+
You will see following:
|
34
|
+
|
35
|
+
```html
|
36
|
+
# <table>
|
37
|
+
# <thead><tr><th>Id</th><th>User name</th></tr></thead>
|
38
|
+
# <tbody>
|
39
|
+
# <tr><td>1</td><td>John Doe</td></tr>
|
40
|
+
# <tr>...</tr>
|
41
|
+
# </tbody>
|
42
|
+
# </table>
|
43
|
+
```
|
44
|
+
|
45
|
+
## Full definition
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
table = Tablette::Table.new do
|
49
|
+
html_options class: 'table'
|
50
|
+
|
51
|
+
header do
|
52
|
+
row do
|
53
|
+
column 'Id', html_options: { colspan: 5 }
|
54
|
+
column do
|
55
|
+
'Doctor strangelove'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
body do
|
61
|
+
html_options class: 'sortable'
|
62
|
+
row do
|
63
|
+
html_options do |member, index|
|
64
|
+
{ data: { id: member.id, index: index } }
|
65
|
+
end
|
66
|
+
|
67
|
+
column html_options: ->(member, _) { { data: { value: member.id } } } do |_, index|
|
68
|
+
index
|
69
|
+
end
|
70
|
+
column :id
|
71
|
+
column :age do |member, _|
|
72
|
+
"Dead at #{member.age}"
|
73
|
+
end
|
74
|
+
column do |_, index|
|
75
|
+
sample_helper_function(index)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
row html_options: { class: 'small' } do
|
80
|
+
tag 'overriden_tr'
|
81
|
+
|
82
|
+
column :test do
|
83
|
+
"test"
|
84
|
+
end
|
85
|
+
|
86
|
+
column :age, formatter: :sample_formatter
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
footer do
|
91
|
+
row do
|
92
|
+
column html_options: { rowspan: 3 } do
|
93
|
+
"On foot"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
puts table.to_html(collection)
|
100
|
+
```
|
101
|
+
|
102
|
+
Every element accepts:
|
103
|
+
* html_options - to customize default options.
|
104
|
+
* override_html_options - to completely override default html options.
|
105
|
+
* tag - to change tag name.
|
106
|
+
|
107
|
+
Default HTML options are:
|
108
|
+
* data-id - for tbody/tr.
|
109
|
+
* data-key - for tbody/tr/td.
|
110
|
+
|
111
|
+
Options which are set by blocks accepts:
|
112
|
+
* |member, index| - for row and column inside body element.
|
113
|
+
* |collection, klass = nil| - for table, header and footer (and all nested elements)
|
114
|
+
* Same for body.
|
115
|
+
|
116
|
+
Method called with :formatter option accepts value, member and index.
|
117
|
+
|
118
|
+
You can override default html options for an element with :override_html_options
|
119
|
+
option.
|
120
|
+
|
121
|
+
You can specify two or more rows in body section. All of this rows will be
|
122
|
+
rendered for every collection item.
|
123
|
+
|
124
|
+
## Global configuration
|
125
|
+
|
126
|
+
Table elements can be customized at application level.
|
127
|
+
|
128
|
+
Somewhere in initializer:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
Tablette::Table.config.html_options = { class: 'table' }
|
132
|
+
Tablette::HeaderRow.config.html_options = proc { |_, resource_class = nil|
|
133
|
+
{ class: resource_class.name.underscore }
|
134
|
+
}
|
135
|
+
```
|
136
|
+
|
137
|
+
You can use: Table, Header, Body, Footer, HeaderRow, BodyRow, FooterRow,
|
138
|
+
HeaderColumn, BodyColumn, FooterColumn.
|
139
|
+
|
140
|
+
So, you can replace table with ordered list or something you need.
|
141
|
+
|
142
|
+
## Partial rendering
|
143
|
+
|
144
|
+
Could be useful for twitter-style pagination:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
table.element_to_html(:header, collection, User)
|
148
|
+
table.element_to_html(:body, collection, User)
|
149
|
+
table.element_to_html(:footer, collection, User)
|
150
|
+
```
|
151
|
+
|
152
|
+
## TODO
|
153
|
+
|
154
|
+
1. Think about sorting.
|
155
|
+
2. Formatted output.
|
156
|
+
3. Data attrs for everything.
|
157
|
+
4. Authospan.
|
158
|
+
5. :row as parameter.
|
159
|
+
6. Reusable columns
|
160
|
+
7. Shortened column definition
|
161
|
+
8. Tablette.render() do
|
162
|
+
|
163
|
+
## Contributing
|
164
|
+
|
165
|
+
1. Fork it
|
166
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
167
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
168
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
169
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Column < Element
|
3
|
+
config.tag = 'td'
|
4
|
+
|
5
|
+
def initialize(*args, &block)
|
6
|
+
self.value = block
|
7
|
+
self.key = args.first if args.first.is_a?(String) or args.first.is_a?(Symbol)
|
8
|
+
|
9
|
+
config.builtin_html_options = ->(member, index = nil) do
|
10
|
+
classes = []
|
11
|
+
|
12
|
+
if self.key.present?
|
13
|
+
classes << self.key.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
if index.kind_of? Numeric
|
17
|
+
if index.odd?
|
18
|
+
classes << "odd"
|
19
|
+
else
|
20
|
+
classes << "even"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if classes.any?
|
25
|
+
{ :class => classes.join(" ") }
|
26
|
+
else
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Bypass block evaling: in this case it's not a config but a value formatter
|
32
|
+
super(*args, &nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
def colspan(args)
|
36
|
+
override_html_options, html_options =
|
37
|
+
get_options([:override_html_options, :html_options], *args)
|
38
|
+
|
39
|
+
options = override_html_options.merge html_options
|
40
|
+
options[:colspan]
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
attr_accessor :key, :value
|
45
|
+
end
|
46
|
+
|
47
|
+
class BodyColumn < Column
|
48
|
+
protected
|
49
|
+
def html_content(member, index)
|
50
|
+
value = @renderer.wrap_content(self.value).call(member, index) if self.value.present?
|
51
|
+
value ||= member.send(key) if key.present? and member.respond_to?(key)
|
52
|
+
|
53
|
+
if config.formatter.present?
|
54
|
+
value ||= send(config.formatter, key, member, index)
|
55
|
+
end
|
56
|
+
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class HeaderColumn < Column
|
62
|
+
config.tag = 'th'
|
63
|
+
|
64
|
+
protected
|
65
|
+
def html_content(collection, resource_class = nil)
|
66
|
+
return @renderer.wrap_content(value).call(collection, resource_class) if value.is_a?(Proc)
|
67
|
+
if resource_class.respond_to?(:human_attribute_name) && key.present?
|
68
|
+
resource_class.human_attribute_name(key)
|
69
|
+
else
|
70
|
+
key
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class FooterColumn < Column
|
76
|
+
config.tag = 'td'
|
77
|
+
|
78
|
+
protected
|
79
|
+
def html_content(*args)
|
80
|
+
return @renderer.wrap_content(value).call(*args) if value.is_a?(Proc)
|
81
|
+
key
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Element
|
3
|
+
protected
|
4
|
+
|
5
|
+
config.allowed_configuration_options = %w(
|
6
|
+
tag html_options override_html_options
|
7
|
+
)
|
8
|
+
config.html_options = {}
|
9
|
+
config.builtin_html_options = {}
|
10
|
+
config.override_html_options = {}
|
11
|
+
config.render_nested_elements = []
|
12
|
+
config.tag = nil
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Element
|
3
|
+
class << self
|
4
|
+
protected
|
5
|
+
# Defines DSL method for configuring nested element.
|
6
|
+
# If no args/block passed - returns currently defined elements as array.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
# class Table < Element
|
10
|
+
# nest :body, Body
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# table = Table.new do
|
14
|
+
# body do
|
15
|
+
# (...)
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# table.body.first.to_html # <tbody>...</tbody>
|
20
|
+
def nest(accessor_name, klass, opts = {})
|
21
|
+
ivar_name = opts[:ivar_name] || accessor_name
|
22
|
+
merge_children = opts[:merge_children] || false
|
23
|
+
define_method accessor_name do |*args, &block|
|
24
|
+
items = instance_variable_get("@#{ivar_name}") || []
|
25
|
+
return items if args.blank? && block.blank?
|
26
|
+
|
27
|
+
inherited_config = {
|
28
|
+
:renderer => @renderer,
|
29
|
+
:helper => @helper
|
30
|
+
}
|
31
|
+
|
32
|
+
if args.last.kind_of? Hash
|
33
|
+
last = args.pop
|
34
|
+
args << last.merge(inherited_config)
|
35
|
+
else
|
36
|
+
args << inherited_config
|
37
|
+
end
|
38
|
+
|
39
|
+
value = klass.new(*args, &block)
|
40
|
+
if merge_children && items.any?
|
41
|
+
items.first.merge_with! value
|
42
|
+
else
|
43
|
+
items.push(value)
|
44
|
+
end
|
45
|
+
instance_variable_set("@#{ivar_name}", items)
|
46
|
+
value
|
47
|
+
end
|
48
|
+
protected accessor_name
|
49
|
+
end
|
50
|
+
|
51
|
+
# Defines top-level shortcut DSL method.
|
52
|
+
#
|
53
|
+
# Example:
|
54
|
+
# class Body < Element
|
55
|
+
# nest :row, BodyRow
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# class Table < Element
|
59
|
+
# nest_through :body, :row, :column # Table#column calls .body.row.column
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# table = Table.new do
|
63
|
+
# column :id
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# table.body.first.row.first.column.first.to_html # <td data-name="id">...</td>
|
67
|
+
def nest_through(*chain)
|
68
|
+
nested_method = chain.last
|
69
|
+
|
70
|
+
define_method nested_method do |*args, &block|
|
71
|
+
_get_chained(self, chain.dup, *args, &block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def _get_chained(context, chain, *args, &block)
|
78
|
+
key = chain.shift
|
79
|
+
if chain.empty?
|
80
|
+
context.send(key, *args, &block)
|
81
|
+
else
|
82
|
+
# Get last defined element, or define new blank
|
83
|
+
nested_item = context.send(key).last || context.send(key) { }
|
84
|
+
_get_chained(nested_item, chain, *args, &block)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Element
|
3
|
+
# Translates element to html tag.
|
4
|
+
def to_html(*args)
|
5
|
+
tag, override_html_options, user_html_options, builtin_html_options =
|
6
|
+
get_options([:tag, :override_html_options, :html_options, :builtin_html_options], *args)
|
7
|
+
|
8
|
+
raise "Set tag option for #{self.class.name}" if tag.blank?
|
9
|
+
|
10
|
+
html_options = override_html_options.merge user_html_options
|
11
|
+
|
12
|
+
builtin_html_options.each do |key, value|
|
13
|
+
if html_options.include? key
|
14
|
+
html_options[key] = "#{value} #{html_options[key]}"
|
15
|
+
else
|
16
|
+
html_options[key] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@renderer.element = self
|
21
|
+
begin
|
22
|
+
@renderer.produce_element tag, html_options, html_content(*args)
|
23
|
+
ensure
|
24
|
+
@renderer.element = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def element_to_html(element, *args)
|
29
|
+
send(element).map do |item|
|
30
|
+
item.to_html(*args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
# HTML content for element. Renders elements set by :render_nested_elements
|
36
|
+
# wrapped by :tag.
|
37
|
+
def html_content(*args)
|
38
|
+
nested = get_options(:render_nested_elements, *args).first
|
39
|
+
|
40
|
+
if nested.blank?
|
41
|
+
raise "Set render_nested_elements options or override #html_content/#to_html for #{self.class.name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
nested.map { |e| element_to_html(e, *args) }.flatten!(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Gets given option values. If an option is a block - yields it and
|
50
|
+
# returns value.
|
51
|
+
def get_options(keys, *args)
|
52
|
+
keys = Array.wrap(keys)
|
53
|
+
keys.map do |name|
|
54
|
+
config[name].is_a?(Proc) ? config[name].call(*args) : config[name]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Element
|
3
|
+
include ActiveSupport::Configurable
|
4
|
+
|
5
|
+
attr_accessor :renderer, :helper
|
6
|
+
|
7
|
+
def initialize(*args, &definition)
|
8
|
+
options = args.extract_options!
|
9
|
+
if options.include? :renderer
|
10
|
+
@renderer = options.delete :renderer
|
11
|
+
end
|
12
|
+
|
13
|
+
if options.include? :helper
|
14
|
+
@helper = options.delete :helper
|
15
|
+
end
|
16
|
+
|
17
|
+
instance_exec(&definition) if block_given?
|
18
|
+
config.merge!(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond_to?(method)
|
22
|
+
super || @helper.respond_to?(method) || config.allowed_configuration_options.include?(method.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method, *args, &block)
|
26
|
+
if @helper.respond_to? method
|
27
|
+
@helper.send method, *args, &block
|
28
|
+
elsif config.allowed_configuration_options.include?(method.to_s)
|
29
|
+
# Catches a call to configuration option setter.
|
30
|
+
#
|
31
|
+
# Example:
|
32
|
+
# body do
|
33
|
+
# html_options { class: 'test' } # Holds such calls
|
34
|
+
# end
|
35
|
+
|
36
|
+
config[method] = args.first || block
|
37
|
+
|
38
|
+
class_eval do
|
39
|
+
define_method method do |value|
|
40
|
+
config[method] = value
|
41
|
+
end
|
42
|
+
protected method
|
43
|
+
end
|
44
|
+
|
45
|
+
config[method]
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Tablette
|
2
|
+
module HTMLRenderer
|
3
|
+
def self.element=(element)
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.produce_element(tag, attributes, content)
|
8
|
+
content = content.join if content.kind_of? Array
|
9
|
+
|
10
|
+
"<#{tag} #{_to_html_args(attributes)}>#{content}</#{tag}>"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.to_html(root)
|
14
|
+
if root.kind_of? Array
|
15
|
+
root.join
|
16
|
+
else
|
17
|
+
root
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.wrap_content(proc)
|
22
|
+
proc
|
23
|
+
end
|
24
|
+
|
25
|
+
# Translates html_options to HTML attributes string. Accepts nested
|
26
|
+
# data-attributes.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
# _to_html_args(ref: true, data: { id: 1 }) # ref="true" data-id="1"
|
30
|
+
def self._to_html_args(options, prepend = nil)
|
31
|
+
options = options || {}
|
32
|
+
html_args = options.map do |key, value|
|
33
|
+
if value.is_a?(Hash)
|
34
|
+
_to_html_args(value, key)
|
35
|
+
else
|
36
|
+
key = "#{prepend}-#{key}" if prepend.present?
|
37
|
+
%{#{key}="#{value}"}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
html_args.join(' ')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Row < Element
|
3
|
+
attr_reader :columns
|
4
|
+
|
5
|
+
config.tag = 'tr'
|
6
|
+
config.render_nested_elements = %w(column)
|
7
|
+
|
8
|
+
def columns_for_row(args)
|
9
|
+
columns = 0
|
10
|
+
|
11
|
+
self.column.each do |column|
|
12
|
+
colspan = column.colspan(args)
|
13
|
+
|
14
|
+
if colspan.nil? || colspan == "auto"
|
15
|
+
columns += 1
|
16
|
+
else
|
17
|
+
columns += colspan
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
columns
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_auto_colspan!(columns, args)
|
25
|
+
auto_column = nil
|
26
|
+
|
27
|
+
self.column.each do |column|
|
28
|
+
next unless column.colspan(args) == "auto"
|
29
|
+
|
30
|
+
if auto_column.nil?
|
31
|
+
auto_column = column
|
32
|
+
else
|
33
|
+
raise "multiple columns with colspan=auto are defined"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
row_columns = columns_for_row(args)
|
38
|
+
if auto_column.nil?
|
39
|
+
if row_columns != columns
|
40
|
+
raise "mismatched number of columns in table: #{columns} in table, #{row_columns} defined in row"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
auto_column.config[:html_options][:colspan] = columns - row_columns + 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class BodyRow < Row
|
49
|
+
config.override_html_options = proc { |member, index|
|
50
|
+
{ data: { id: member.try(:id) } }
|
51
|
+
}
|
52
|
+
|
53
|
+
nest :column, BodyColumn
|
54
|
+
|
55
|
+
protected
|
56
|
+
def html_content(member, index)
|
57
|
+
column.map do |column|
|
58
|
+
column.to_html(member, index)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class HeaderRow < Row
|
64
|
+
nest :column, HeaderColumn
|
65
|
+
end
|
66
|
+
|
67
|
+
class FooterRow < Row
|
68
|
+
nest :column, FooterColumn
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Section < Element
|
3
|
+
config.render_nested_elements = %w(row)
|
4
|
+
|
5
|
+
def columns_for_section(args)
|
6
|
+
columns = 0
|
7
|
+
|
8
|
+
self.row.each do |row|
|
9
|
+
row_columns = row.columns_for_row args
|
10
|
+
columns = row_columns if row_columns > columns
|
11
|
+
end
|
12
|
+
|
13
|
+
columns
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_auto_colspan!(columns, args)
|
17
|
+
self.row.each do |row|
|
18
|
+
row.update_auto_colspan! columns, args
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Body < Section
|
24
|
+
config.tag = 'tbody'
|
25
|
+
|
26
|
+
nest :row, BodyRow
|
27
|
+
nest_through :row, :column
|
28
|
+
|
29
|
+
def columns_for_section(args)
|
30
|
+
collection, resource_class = args
|
31
|
+
return 0 if collection.empty?
|
32
|
+
|
33
|
+
super([collection.first, 0])
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_auto_colspan!(columns, args)
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
def html_content(collection, resource_class = nil)
|
42
|
+
collection.map.with_index do |member, index|
|
43
|
+
row.map { |row| row.to_html(member, index) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Header < Section
|
49
|
+
config.tag = 'thead'
|
50
|
+
|
51
|
+
nest :row, HeaderRow
|
52
|
+
nest_through :row, :column
|
53
|
+
|
54
|
+
def merge_with!(header)
|
55
|
+
self.row.concat header.row
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Footer < Section
|
60
|
+
config.tag = 'tfoot'
|
61
|
+
|
62
|
+
nest :row, FooterRow
|
63
|
+
nest_through :row, :column
|
64
|
+
|
65
|
+
def merge_with!(footer)
|
66
|
+
self.row.concat footer.row
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tablette
|
2
|
+
class Table < Element
|
3
|
+
config.tag = 'table'
|
4
|
+
config.render_nested_elements = %w(header! body footer!)
|
5
|
+
config.allowed_configuration_options = %w(tag html_options)
|
6
|
+
config.builtin_html_options = { :class => "tablette" }
|
7
|
+
|
8
|
+
nest :header!, Header, :ivar_name => :header, :merge_children => true
|
9
|
+
nest :body, Body
|
10
|
+
nest :footer!, Footer, :ivar_name => :footer, :merge_children => true
|
11
|
+
|
12
|
+
nest_through :body, :row, :column
|
13
|
+
|
14
|
+
def initialize(*args, &block)
|
15
|
+
@renderer = HTMLRenderer
|
16
|
+
@helper = nil
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def header(&block)
|
22
|
+
header! do
|
23
|
+
column(html_options: { colspan: 'auto'}, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def footer(&block)
|
28
|
+
footer! do
|
29
|
+
column(html_options: { colspan: 'auto'}, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_html(*args)
|
34
|
+
columns = 0
|
35
|
+
|
36
|
+
all_sections = [ self.header!, self.body, self.footer! ].flatten
|
37
|
+
all_sections.each do |section|
|
38
|
+
section_columns = section.columns_for_section args
|
39
|
+
columns = section_columns if section_columns > columns
|
40
|
+
end
|
41
|
+
|
42
|
+
all_sections.each do |section|
|
43
|
+
section.update_auto_colspan! columns, args
|
44
|
+
end
|
45
|
+
|
46
|
+
@renderer.to_html(super)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/tablette.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
require 'active_support/core_ext/object/inclusion'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'active_support/core_ext/object/try'
|
5
|
+
require 'active_support/configurable'
|
6
|
+
|
7
|
+
require 'tablette/version'
|
8
|
+
require 'tablette/element'
|
9
|
+
require 'tablette/element/configuration'
|
10
|
+
require 'tablette/element/rendering'
|
11
|
+
require 'tablette/element/nesting'
|
12
|
+
require 'tablette/columns'
|
13
|
+
require 'tablette/rows'
|
14
|
+
require 'tablette/sections'
|
15
|
+
require 'tablette/table'
|
16
|
+
require 'tablette/html_renderer'
|
data/spec/grid_spec.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Grid' do
|
4
|
+
context 'defined fully' do
|
5
|
+
subject { sample_table_full_described }
|
6
|
+
|
7
|
+
it 'should render correctly' do
|
8
|
+
subject.should have_tag 'table', with: { class: 'table' }, count: 1 do
|
9
|
+
with_tag 'thead', count: 1 do
|
10
|
+
with_tag 'th', text: 'Doctor strangelove', count: 1
|
11
|
+
with_tag 'th', text: 'Id', count: 1
|
12
|
+
end
|
13
|
+
|
14
|
+
with_tag 'tbody', with: { class: 'sortable' }, count: 1 do
|
15
|
+
sample_collection.each_with_index do |member, index|
|
16
|
+
with_tag "tr[data-id='#{member.id}'][data-index='#{index}']"
|
17
|
+
|
18
|
+
with_tag 'td', with: { 'data-value' => member.id }, text: index, count: 1
|
19
|
+
with_tag 'td', text: member.id, count: 1
|
20
|
+
with_tag 'td', text: "Dead at #{member.age}", count: 1
|
21
|
+
with_tag 'td', text: "I hope this helps #{index}", count: 1
|
22
|
+
|
23
|
+
with_tag 'overriden_tr'
|
24
|
+
|
25
|
+
with_tag 'td', text: member.age
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
with_tag 'tfoot', count: 1 do
|
30
|
+
with_tag 'td', text: 'On foot', count: 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'partial rendering' do
|
37
|
+
subject do
|
38
|
+
sample_table_full_described_definition.element_to_html(:body, sample_collection).join
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should render header, footer, body separately' do
|
42
|
+
|
43
|
+
subject.should_not have_tag 'thead'
|
44
|
+
subject.should_not have_tag 'tfoot'
|
45
|
+
|
46
|
+
subject.should have_tag 'tbody'
|
47
|
+
subject.should have_tag 'tr'
|
48
|
+
subject.should have_tag 'td'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'defined shortly' do
|
53
|
+
subject { sample_table_short }
|
54
|
+
|
55
|
+
it 'should render correctly' do
|
56
|
+
subject.should have_tag 'table', count: 1 do
|
57
|
+
with_tag 'tbody', count: 1
|
58
|
+
with_tag 'thead', count: 1
|
59
|
+
with_tag 'tr', count: 4
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with active record objects' do
|
65
|
+
subject { sample_table_active_record }
|
66
|
+
|
67
|
+
it 'should get right headings from active record' do
|
68
|
+
subject.should have_tag 'thead', count: 1 do
|
69
|
+
with_tag 'th', text: 'Humanized id', count: 1
|
70
|
+
with_tag 'th', text: 'Humanized age', count: 1
|
71
|
+
end
|
72
|
+
|
73
|
+
subject.should_not have_tag 'tfoot'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
class ActiveRecordMock
|
4
|
+
def self.human_attribute_name(name)
|
5
|
+
"Humanized #{name.to_s}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def sample_collection
|
10
|
+
[
|
11
|
+
OpenStruct.new(id: 10, age: 27, value: 'Jim Morrison'),
|
12
|
+
OpenStruct.new(id: 20, age: 70, value: 'William Blake'),
|
13
|
+
OpenStruct.new(id: 30, age: 89, value: 'Robert Lee Frost')
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def sample_helper_function(arg)
|
18
|
+
"I hope this helps #{arg}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def sample_formatter(key, member, index)
|
22
|
+
"Formatter for #{member[key]}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def sample_table_full_described_definition
|
26
|
+
Tablette::Table.new do
|
27
|
+
html_options class: 'table'
|
28
|
+
|
29
|
+
header! do
|
30
|
+
column 'Id', html_options: { colspan: 5 }
|
31
|
+
column do
|
32
|
+
'Doctor strangelove'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
body do
|
37
|
+
html_options class: 'sortable'
|
38
|
+
row do
|
39
|
+
html_options do |member, index|
|
40
|
+
{ data: { id: member.id, index: index } }
|
41
|
+
end
|
42
|
+
|
43
|
+
column html_options: ->(member, _) { { data: { value: member.id } } } do |_, index|
|
44
|
+
index
|
45
|
+
end
|
46
|
+
column :id
|
47
|
+
column :age do |member, _|
|
48
|
+
"Dead at #{member.age}"
|
49
|
+
end
|
50
|
+
column do |_, index|
|
51
|
+
sample_helper_function(index)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
row html_options: { class: 'small' } do
|
56
|
+
tag 'overriden_tr'
|
57
|
+
|
58
|
+
column :test do
|
59
|
+
"test"
|
60
|
+
end
|
61
|
+
|
62
|
+
column :age, formatter: :sample_formatter
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
footer do
|
67
|
+
"On foot"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def sample_table_full_described
|
73
|
+
sample_table_full_described_definition.to_html(sample_collection)
|
74
|
+
end
|
75
|
+
|
76
|
+
def sample_table_short
|
77
|
+
table = Tablette::Table.new do
|
78
|
+
header do
|
79
|
+
column 'Id'
|
80
|
+
column 'Age'
|
81
|
+
end
|
82
|
+
|
83
|
+
column :id
|
84
|
+
column :age
|
85
|
+
end
|
86
|
+
table.to_html(sample_collection)
|
87
|
+
end
|
88
|
+
|
89
|
+
def sample_table_active_record
|
90
|
+
table = Tablette::Table.new do
|
91
|
+
header! do
|
92
|
+
column :id
|
93
|
+
column :age
|
94
|
+
column "Custom string"
|
95
|
+
column do
|
96
|
+
"Custom block"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
column :id
|
100
|
+
column :age
|
101
|
+
end
|
102
|
+
table.to_html(sample_collection, ActiveRecordMock)
|
103
|
+
end
|
data/tablette.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tablette/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "tablette"
|
8
|
+
gem.version = Tablette::VERSION
|
9
|
+
gem.authors = ["Victor Sokolov", "Sergey Gridasov"]
|
10
|
+
gem.email = ["gzigzigzeo@gmail.com", "grindars@gmail.com"]
|
11
|
+
gem.description = %q{HTML table generator}
|
12
|
+
gem.summary = %q{HTML table generator}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'activesupport', '>= 3'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
gem.add_development_dependency 'rake'
|
24
|
+
gem.add_development_dependency 'rspec-html-matchers'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tablette
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Sokolov
|
8
|
+
- Sergey Gridasov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '3'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec-html-matchers
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: HTML table generator
|
71
|
+
email:
|
72
|
+
- gzigzigzeo@gmail.com
|
73
|
+
- grindars@gmail.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- .gitignore
|
79
|
+
- .rspec
|
80
|
+
- .travis.yml
|
81
|
+
- Gemfile
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- lib/tablette.rb
|
86
|
+
- lib/tablette/columns.rb
|
87
|
+
- lib/tablette/element.rb
|
88
|
+
- lib/tablette/element/configuration.rb
|
89
|
+
- lib/tablette/element/nesting.rb
|
90
|
+
- lib/tablette/element/rendering.rb
|
91
|
+
- lib/tablette/html_renderer.rb
|
92
|
+
- lib/tablette/rows.rb
|
93
|
+
- lib/tablette/sections.rb
|
94
|
+
- lib/tablette/table.rb
|
95
|
+
- lib/tablette/version.rb
|
96
|
+
- spec/grid_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
- spec/support/sample_table.rb
|
99
|
+
- tablette.gemspec
|
100
|
+
homepage: ''
|
101
|
+
licenses: []
|
102
|
+
metadata: {}
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.0.0
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: HTML table generator
|
123
|
+
test_files:
|
124
|
+
- spec/grid_spec.rb
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/support/sample_table.rb
|