dining-table 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 67ee845d370a9b92ee759acdb3fff4fbfccdbd12
4
- data.tar.gz: 88e0481f7a7d2218cb709e36d94ccf22d22add8b
3
+ metadata.gz: 8e2bb002b4a0ab18643810cb14fb3790d285b384
4
+ data.tar.gz: 0578437ede4fe83f41814a2c4d99ee898b52a953
5
5
  SHA512:
6
- metadata.gz: adfc7a39866c15e9da27a68b262a24695a72d46951abe91658e2e71917541407cac95c9a7afdb0ede4e825dbaa4e9fee4c5f765a7bb8207d8efd1878a1bec440
7
- data.tar.gz: 382b77586358e8505d819ed50a8d999abc53f451b2b083a85ea3cc0af7c2709f92f6a8ae0f323d1100b5bf94b589411ef45cab9b35b811de5f35f620214fa927
6
+ metadata.gz: 882749ff1912bd3c9467c334f9a5abfa4eaf48e0b3a43fccb2595a74c1a50e77318720952e2a1ded192e4b75e63ccf51a7c6a307c78084bfb7cd6c791a973a6e
7
+ data.tar.gz: abffd0f5edd5d89312482265a01b3fd2cafe6a73c20bb9f405a5bf37e35a2b6b1960c087f7fee9676f643c20bcea2ea09a0de7a7f0ef38b299b4a281ef88db5a
@@ -1,3 +1,9 @@
1
+ ## 1.0.0 (17/06/2018)
2
+
3
+ * New configuration mechanism for HTML presenter
4
+ * Described new configuration mechanism in readme
5
+ * Documented skip_header and added and documented skip_footer
6
+
1
7
  ## 0.2.1 (26/05/2018)
2
8
 
3
9
  * Removed Gemfile.lock file
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # dining-table
2
2
  [![Build Status](https://travis-ci.org/mvdamme/dining-table.png)](https://travis-ci.org/mvdamme/dining-table)
3
3
 
4
+ dining-table allows you to write clean Ruby classes instead of messy view code to generate HTML tables. You can re-use the same classes to
5
+ generate csv or xlsx output as well.
6
+
4
7
  dining-table was inspired by the (now unfortunately unmaintained) [table_cloth](https://github.com/bobbytables/table_cloth) gem.
5
- This gem is definitely not a drop-in replacement for [table-cloth](https://github.com/bobbytables/table_cloth), it aims to be less dependent on Rails
6
- (no Rails required to use `dining-table`) and more flexible.
7
- In addition, it not only supports HTML output but you can output tabular data in csv or xlsx formats as well.
8
+ This gem is definitely not a drop-in replacement for [table_cloth](https://github.com/bobbytables/table_cloth), it aims to be less dependent on Rails
9
+ (no Rails required to use `dining-table`, in fact it has no dependencies (except if you chose to generate xlsx output)) and more flexible.
8
10
 
9
11
  ## Installation
10
12
 
@@ -71,6 +73,17 @@ end
71
73
 
72
74
  The custom header can be a string, but also a lambda or a proc.
73
75
 
76
+ If for some reason you don't want a header, call `skip_header`:
77
+
78
+ ```ruby
79
+ class CarTable < DiningTable::Table
80
+ def define
81
+ skip_header
82
+ column :brand
83
+ end
84
+ end
85
+ ```
86
+
74
87
  By default, `dining-table` doesn't add a footer to the table, except when at least one column explicitly specifies a footer:
75
88
 
76
89
  ```ruby
@@ -84,6 +97,17 @@ end
84
97
 
85
98
  Please note how the collection passed in when creating the table obect (`@cars` in `CarTable.new(@cars, self)`) is available as `collection`.
86
99
 
100
+ Similarly to `skip_header`, if for some reason you don't want a footer (even though at least one column defines one), call `skip_footer`:
101
+
102
+ ```ruby
103
+ class CarTable < DiningTable::Table
104
+ def define
105
+ skip_footer
106
+ column :brand, footer: 'Footer'
107
+ end
108
+ end
109
+ ```
110
+
87
111
  ### Links and view helpers
88
112
 
89
113
  When rendering the table in a view using `<%= CarTable.new(@cars, self).render %>`, the `self` parameter is the view context. It is made available through the `h`
@@ -185,6 +209,8 @@ end
185
209
 
186
210
  ### HTML
187
211
 
212
+ #### Introduction
213
+
188
214
  The default presenter is HTML (i.e. `DiningTable::Presenters::HTMLPresenter`), so `CarTable.new(@cars, self).render` will generate a table in HTML.
189
215
  When defining columns, you can specify options that apply only when using a certain presenter. For example, here we provide css classes for `td` and `th`
190
216
  elements for some columns in the html table:
@@ -193,29 +219,126 @@ elements for some columns in the html table:
193
219
  class CarTable < DiningTable::Table
194
220
  def define
195
221
  column :brand
196
- column :number_of_doors, html: { td_options: { class: 'center' }, th_options: { class: :center } }
197
- column :stock, html: { td_options: { class: 'center' }, th_options: { class: :center } }
222
+ column :number_of_doors, html: { td: { class: 'center' }, th: { class: 'center' } }
223
+ column :stock, html: { td: { class: 'center' }, th: { class: 'center' } }
198
224
  end
199
225
  end
200
226
  ```
201
227
 
202
228
  The same table class can also be used with other presenters (csv, xlsx or a custom presenter), but the options will only be in effect when using the HTML presenter.
203
229
 
204
- By instantiating the presenter yourself it is possible to specify options. For example:
230
+ #### Presenter configuration
231
+
232
+ By instantiating the presenter yourself it is possible to specify options for a specific table. Using the `:tags` key you can specify
233
+ options for all HTML tags used in the table. Example:
205
234
 
206
235
  ```ruby
207
- <%= CarTable.new(@cars, self, presenter: DiningTable::Presenters::HTMLPresenter.new( class: 'table table-bordered' )).render %>
236
+ <%= CarTable.new(@cars, self,
237
+ presenter: DiningTable::Presenters::HTMLPresenter.new(
238
+ tags: { table: { class: 'table table-bordered', id: 'car_table' },
239
+ tr: { class: 'car_table_row' } } )).render %>
208
240
  ```
241
+ In the above example, we specify the CSS class and HTML id for the table, and the CSS class to be used for all rows in the table.
242
+ The supported HTML tags are: `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`.
209
243
 
210
- It is also possible to wrap the table in another tag (a div for instance):
244
+ It is also possible to wrap the table in another tag (a div for instance), and specify options for this tag:
211
245
 
212
246
  ```ruby
213
247
  <%= CarTable.new(@cars, self,
214
- presenter: DiningTable::Presenters::HTMLPresenter.new( class: 'table table-bordered',
215
- wrap: { tag: :div, class: 'table-responsive' } )).render %>
248
+ presenter: DiningTable::Presenters::HTMLPresenter.new(
249
+ tags: { table: { class: 'table table-bordered', id: 'car_table' },
250
+ wrap: { tag: :div, class: 'table-responsive' } )).render %>
216
251
  ```
217
252
 
218
- Both of these html options are usually best set as defaults, see [Configuration](#configuration)
253
+ Most of the html options are usually best set as defaults, see [Configuration](#configuration).
254
+
255
+ Note that configuration information provided to the presenter constructor is added to the default configuration,
256
+ it doesn't replace it. This means you can have the default configuration define the CSS class for the
257
+ table tag, for instance, and add the html id attribute when initializing the presenter, or from inside the
258
+ table definition.
259
+
260
+ #### Configuration inside the table definition
261
+
262
+ It is possible to specify or modify the configuration from within the table definition. This allows you to use custom
263
+ CSS classes, ids, etc. per row or even per cell. Example:
264
+
265
+ ```ruby
266
+ class CarTableWithConfigBlocks < DiningTable::Table
267
+ def define
268
+ table_id = options[:table_id] # custom option, see 'Options' above
269
+
270
+ presenter.table_config do |config|
271
+ config.table.class = 'table-class'
272
+ config.table.id = table_id || 'table-id'
273
+ config.thead.class = 'thead-class'
274
+ end if presenter.type?(:html)
275
+
276
+ presenter.row_config do |config, index, object|
277
+ if index == :header
278
+ config.tr.class = 'header-tr'
279
+ config.th.class = 'header-th'
280
+ elsif index == :footer
281
+ config.tr.class = 'footer-tr'
282
+ else # normal row
283
+ config.tr.class = index.odd? ? 'odd' : 'even'
284
+ config.tr.class += ' lowstock' if object.stock < 10
285
+ end
286
+ end if presenter.type?(:html)
287
+
288
+ column :brand
289
+ column :stock, footer: 'Footer text'
290
+ end
291
+ end
292
+ ```
293
+ This example shows how to use `presenter.table_config` to set the configuration for (in this case) the `table` and `thead`tags. The block you use with `table_config`
294
+ is called once, when the table is being rendered. A configuration object is passed in that allows you to set any HTML attribute of the
295
+ seven supported tags.
296
+
297
+ Note that the configuration object already contains the pre-existing configuration information (coming
298
+ from either the presenter initialisation and/or from the global configuration), so you can refine the configuration in the block
299
+ instead of having to re-specify it in full. This means you can easily add CSS classes without knowledge of previously existing
300
+ configuration:
301
+ ```ruby
302
+ presenter.table_config do |config|
303
+ config.table.class += ' my-table-class'
304
+ end if presenter.type?(:html)
305
+ ```
306
+ Per row configuration can be specified with `presenter.row_config`. The block used with this method is called once for each row being
307
+ rendered, and receives three parameters: the configuration object (identical as with `table_config`), an index value, and the object
308
+ containing the data being rendered in this row.
309
+ The index value is equal to the row number of the row being rendered (starting at zero), except for the header and footer rows, in which case it
310
+ is equal to `:header` and `:footer`, respectively. `object` is the current object being rendered (`nil` for the header and footer rows).
311
+ As above, the passed in configuration object already contains the configuration which is in effect before calling the block.
312
+
313
+ #### Per cell configuration
314
+
315
+ As shown above, you can specify per column configuration using a hash:
316
+
317
+ ```ruby
318
+ class CarTable < DiningTable::Table
319
+ def define
320
+ column :number_of_doors, html: { td: { class: 'center' }, th: { class: 'center' } }
321
+ end
322
+ end
323
+ ```
324
+ For each column, the per column configuration is merged with the row configuration (see `presenter.row_config` above) before
325
+ cells from the column are rendered.
326
+
327
+ Sometimes, you might want to specify the configuration per cell, for instance to add a CSS class for cells with a certain content.
328
+ This is possible by supplying a lamba or proc instead of a hash:
329
+
330
+ ```ruby
331
+ class CarTable < DiningTable::Table
332
+ def define
333
+ number_of_doors_options = ->( config, index, object ) do
334
+ config.td.class = 'center'
335
+ config.td.class += ' five_doors' if object && object.number_of_doors == 5
336
+ end
337
+ column :number_of_doors, html: number_of_doors_options
338
+ end
339
+ end
340
+ ```
341
+ The arguments provided to the lambda or proc are the same as in the case of `presenter.row_config`.
219
342
 
220
343
  ### CSV
221
344
 
@@ -315,7 +438,8 @@ You can set default options for the different presenters in an initializer (e.g.
315
438
 
316
439
  ```ruby
317
440
  DiningTable.configure do |config|
318
- config.html_presenter.default_options = { class: 'table table-bordered table-hover',
441
+ config.html_presenter.default_options = { tags: { table: { class: 'table table-bordered' },
442
+ thead: { class: 'header' } },
319
443
  wrap: { tag: :div, class: 'table-responsive' } }
320
444
  config.csv_presenter.default_options = { csv: { col_sep: ';' } }
321
445
  end
@@ -323,4 +447,4 @@ end
323
447
 
324
448
  ## Copyright
325
449
 
326
- Copyright (c) 2016 Michaël Van Damme. See LICENSE.txt for further details.
450
+ Copyright (c) 2018 Michaël Van Damme. See LICENSE.txt for further details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 1.0.0
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: dining-table 0.2.1 ruby lib
5
+ # stub: dining-table 1.0.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "dining-table".freeze
9
- s.version = "0.2.1"
9
+ s.version = "1.0.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Micha\u{eb}l Van Damme".freeze]
14
- s.date = "2018-05-26"
14
+ s.date = "2018-06-17"
15
15
  s.description = "Easily output tabular data, be it in HTML, CSV or XLSX. Create clean table classes instead of messing with views to create nice tables.".freeze
16
16
  s.email = "michael.vandamme@vub.ac.be".freeze
17
17
  s.extra_rdoc_files = [
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
34
34
  "lib/dining-table/presenters/csv_presenter.rb",
35
35
  "lib/dining-table/presenters/excel_presenter.rb",
36
36
  "lib/dining-table/presenters/html_presenter.rb",
37
+ "lib/dining-table/presenters/html_presenter_configuration.rb",
37
38
  "lib/dining-table/presenters/presenter.rb",
38
39
  "lib/dining-table/presenters/spreadsheet_presenter.rb",
39
40
  "lib/dining-table/table.rb",
@@ -44,9 +45,12 @@ Gem::Specification.new do |s|
44
45
  "spec/spec_helper.rb",
45
46
  "spec/tables/car_table.rb",
46
47
  "spec/tables/car_table_with_actions.rb",
48
+ "spec/tables/car_table_with_config_blocks.rb",
47
49
  "spec/tables/car_table_with_footer.rb",
48
50
  "spec/tables/car_table_with_header.rb",
49
- "spec/tables/car_table_with_options.rb"
51
+ "spec/tables/car_table_with_options.rb",
52
+ "spec/tables/car_table_with_options_old_syntax.rb",
53
+ "spec/tables/car_table_without_header.rb"
50
54
  ]
51
55
  s.homepage = "http://github.com/mvdamme/dining-table".freeze
52
56
  s.licenses = ["MIT".freeze]
@@ -6,6 +6,7 @@ require 'dining-table/columns/column'
6
6
  require 'dining-table/columns/actions_column'
7
7
 
8
8
  require 'dining-table/presenters/presenter'
9
+ require 'dining-table/presenters/html_presenter_configuration'
9
10
  require 'dining-table/presenters/html_presenter'
10
11
  require 'dining-table/presenters/spreadsheet_presenter'
11
12
  require 'dining-table/presenters/csv_presenter'
@@ -16,11 +16,11 @@ module DiningTable
16
16
  private
17
17
 
18
18
  def action(&block)
19
- action_value = yield(@current_object)
19
+ action_value = table.instance_exec(@current_object, &block)
20
20
  @incremental_value += action_value.to_s if action_value && action_value.respond_to?(:to_s)
21
21
  end
22
22
 
23
- # offer methods normally available on Table that could be used by the action blocks
23
+ # offer methods normally available on Table that could be used by the action-column block
24
24
  [ :h, :helpers, :collection, :index, :presenter ].each do |method|
25
25
  self.class_eval <<-eos, __FILE__, __LINE__+1
26
26
  def #{method}(*args)
@@ -28,7 +28,7 @@ module DiningTable
28
28
  end
29
29
  eos
30
30
  end
31
-
31
+
32
32
  end
33
33
 
34
34
  end
@@ -5,7 +5,11 @@ module DiningTable
5
5
  module Presenters
6
6
 
7
7
  class CSVPresenter < SpreadsheetPresenter
8
-
8
+
9
+ attr_writer :output
10
+ attr_accessor :stringio
11
+ private :output, :stringio, :output=, :stringio=
12
+
9
13
  def initialize( *args )
10
14
  super
11
15
  self.output = ''
@@ -21,9 +25,6 @@ module DiningTable
21
25
 
22
26
  private
23
27
 
24
- attr_writer :output
25
- attr_accessor :stringio
26
-
27
28
  def csv
28
29
  @csv ||= begin
29
30
  self.stringio = StringIO.new
@@ -3,7 +3,10 @@ module DiningTable
3
3
  module Presenters
4
4
 
5
5
  class ExcelPresenter < SpreadsheetPresenter
6
-
6
+
7
+ attr_accessor :worksheet
8
+ private :worksheet, :worksheet=
9
+
7
10
  def initialize( worksheet, *args )
8
11
  super( *args )
9
12
  self.worksheet = worksheet
@@ -15,8 +18,6 @@ module DiningTable
15
18
 
16
19
  private
17
20
 
18
- attr_accessor :worksheet
19
-
20
21
  def add_row(array)
21
22
  worksheet.add_row( array )
22
23
  end
@@ -3,9 +3,16 @@ module DiningTable
3
3
  module Presenters
4
4
 
5
5
  class HTMLPresenter < Presenter
6
-
7
- def initialize( *args )
6
+
7
+ attr_accessor :tags_configuration, :table_tags_configuration, :base_tags_configuration, :table_config_block, :row_config_block
8
+
9
+ attr_writer :output
10
+ private :output, :output=
11
+
12
+ def initialize( options = {} )
8
13
  super
14
+ self.base_tags_configuration = HTMLPresenterConfiguration::TagsConfiguration.from_hash( default_options )
15
+ base_tags_configuration.merge_hash( options )
9
16
  self.output = ''
10
17
  end
11
18
 
@@ -14,10 +21,11 @@ module DiningTable
14
21
  end
15
22
 
16
23
  def start_table
24
+ set_up_configuration
17
25
  if options[:wrap]
18
26
  add_tag(:start, wrap_tag, wrap_options )
19
27
  end
20
- add_tag(:start, :table, options )
28
+ add_tag(:start, :table, table_options )
21
29
  end
22
30
 
23
31
  def end_table
@@ -28,7 +36,7 @@ module DiningTable
28
36
  end
29
37
 
30
38
  def start_body
31
- add_tag(:start, :tbody)
39
+ add_tag(:start, :tbody, tag_options(:tbody))
32
40
  end
33
41
 
34
42
  def end_body
@@ -36,33 +44,42 @@ module DiningTable
36
44
  end
37
45
 
38
46
  def render_row( object )
39
- add_tag(:start, :tr)
47
+ set_up_row_configuration( table.index, object )
48
+ add_tag(:start, :tr, row_options)
40
49
  columns.each do |column|
41
50
  value = column.value( object )
42
- render_cell( value, column.options_for( identifier ) )
51
+ configuration = cell_configuration( tags_configuration, column, table.index, object )
52
+ #render_cell( value, column.options_for( identifier ) )
53
+ render_cell( value, configuration )
43
54
  end
44
55
  add_tag(:end, :tr)
45
56
  end
46
57
 
47
58
  def render_header
48
- add_tag(:start, :thead)
49
- add_tag(:start, :tr)
59
+ set_up_row_configuration( :header, nil )
60
+ add_tag(:start, :thead, tag_options(:thead))
61
+ add_tag(:start, :tr, row_options)
50
62
  columns.each do |column|
51
63
  value = column.header
52
- render_header_cell( value, column.options_for( identifier ) )
64
+ configuration = cell_configuration( tags_configuration, column, :header, nil )
65
+ #render_header_cell( value, column.options_for( identifier ) )
66
+ render_header_cell( value, configuration )
53
67
  end
54
68
  add_tag(:end, :tr)
55
69
  add_tag(:end, :thead)
56
70
  end
57
71
 
58
72
  def render_footer
73
+ set_up_row_configuration( :footer, nil )
59
74
  footers = columns.each.map(&:footer)
60
75
  if footers.map { |s| blank?(s) }.uniq != [ true ]
61
- add_tag(:start, :tfoot)
62
- add_tag(:start, :tr)
76
+ add_tag(:start, :tfoot, tag_options(:tfoot))
77
+ add_tag(:start, :tr, row_options)
63
78
  columns.each_with_index do |column, index|
64
79
  value = footers[index]
65
- render_footer_cell( value, column.options_for( identifier ) )
80
+ configuration = cell_configuration( tags_configuration, column, :header, nil )
81
+ #render_footer_cell( value, column.options_for( identifier ) )
82
+ render_footer_cell( value, configuration )
66
83
  end
67
84
  add_tag(:end, :tr)
68
85
  add_tag(:end, :tfoot)
@@ -72,11 +89,17 @@ module DiningTable
72
89
  def output
73
90
  @output.respond_to?(:html_safe) ? @output.html_safe : @output
74
91
  end
75
-
92
+
93
+ def table_config(&block)
94
+ self.table_config_block = block
95
+ end
96
+
97
+ def row_config(&block)
98
+ self.row_config_block = block
99
+ end
100
+
76
101
  private
77
102
 
78
- attr_writer :output
79
-
80
103
  def output_
81
104
  @output
82
105
  end
@@ -93,21 +116,21 @@ module DiningTable
93
116
  def end_tag(tag, options = {})
94
117
  "</#{ tag.to_s }>"
95
118
  end
96
-
97
- def render_cell( string, options )
98
- render_general_cell( string, options, :td, :td_options )
119
+
120
+ def render_cell( string, configuration )
121
+ render_general_cell( string, configuration, :td)
99
122
  end
100
123
 
101
- def render_header_cell( string, options )
102
- render_general_cell( string, options, :th, :th_options )
124
+ def render_header_cell( string, configuration )
125
+ render_general_cell( string, configuration, :th)
103
126
  end
104
-
105
- def render_footer_cell( string, options )
106
- render_general_cell( string, options, :td, :footer_options )
127
+
128
+ def render_footer_cell( string, configuration )
129
+ render_cell( string, configuration )
107
130
  end
108
-
109
- def render_general_cell( string, options, cell_tag, options_identifier )
110
- add_tag(:start, cell_tag, options[ options_identifier ] )
131
+
132
+ def render_general_cell( string, configuration, cell_tag )
133
+ add_tag(:start, cell_tag, tag_options(cell_tag, configuration) )
111
134
  output_ << string.to_s
112
135
  add_tag(:end, cell_tag)
113
136
  end
@@ -134,7 +157,67 @@ module DiningTable
134
157
  options_.delete(:tag)
135
158
  options_
136
159
  end
137
-
160
+
161
+ def table_options
162
+ options_ = tag_options(:table)
163
+ return options_ unless options_.empty?
164
+ if options[:class]
165
+ warn "[DEPRECATION] dining-table: option \"class\" is deprecated, please use \"tags: { table: { class: 'my_class' } }\" instead."
166
+ { :class => options[:class] }
167
+ else
168
+ { }
169
+ end
170
+ end
171
+
172
+ def row_options
173
+ tag_options(:tr)
174
+ end
175
+
176
+ def column_options_cache( column )
177
+ @column_options_cache ||= { }
178
+ @column_options_cache[ column ] ||= begin
179
+ column_options = column.options_for( identifier )
180
+ if column_options.is_a?(Hash)
181
+ if column_options[:th_options] || column_options[:td_options]
182
+ warn "[DEPRECATION] dining-table: options \"th_options\" and \"td_options\" are deprecated, please use \"th\" and \"td\" instead. Example: \"{ td: { class: 'my_class' } }\"."
183
+ column_options[:th] = column_options.delete(:th_options)
184
+ column_options[:td] = column_options.delete(:td_options)
185
+ end
186
+ column_options[:tags] ? column_options : { :tags => column_options }
187
+ elsif column_options.respond_to?(:call)
188
+ column_options
189
+ end
190
+ end
191
+ end
192
+
193
+ def cell_configuration( start_configuration, column, index, object )
194
+ column_options = column_options_cache( column )
195
+ return start_configuration if !column_options
196
+ new_configuration = start_configuration.dup
197
+ if column_options.is_a?(Hash)
198
+ new_configuration.merge_hash( column_options )
199
+ else # callable
200
+ column_options.call( new_configuration, index, object )
201
+ new_configuration
202
+ end
203
+ end
204
+
205
+ def tag_options( tag, configuration = nil )
206
+ configuration ||= tags_configuration
207
+ configuration.send( tag ).to_h
208
+ end
209
+
210
+ def set_up_configuration
211
+ self.table_tags_configuration = base_tags_configuration.dup
212
+ table_config_block.call( table_tags_configuration ) if table_config_block
213
+ self.tags_configuration = table_tags_configuration.dup
214
+ end
215
+
216
+ def set_up_row_configuration( index, object )
217
+ self.tags_configuration = table_tags_configuration.dup
218
+ row_config_block.call( tags_configuration, index, object ) if row_config_block
219
+ end
220
+
138
221
  end
139
222
 
140
223
  end
@@ -0,0 +1,103 @@
1
+ module DiningTable
2
+
3
+ module Presenters
4
+
5
+ module HTMLPresenterConfiguration
6
+
7
+ # configuration classes that allow us to avoid implementing a deep-merge for the config hash,
8
+ # and are more user friendly for use in config blocks in the table definition
9
+ class TagConfiguration
10
+
11
+ attr_accessor :__data_hash
12
+
13
+ def initialize
14
+ self.__data_hash = {}
15
+ end
16
+
17
+ def method_missing(name, *args, &block)
18
+ if name.to_s[-1] == '='
19
+ key = name.to_s[0..-2] # strip away '='
20
+ __data_hash[ key.to_sym ] = args.first
21
+ else
22
+ __data_hash.key?( name ) ? __data_hash[ name ] : super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(method_name, *args)
27
+ return true if method_name.to_s[-1] == '='
28
+ __data_hash.key?( method_name ) ? true : super
29
+ end
30
+
31
+ # override class method (since it is a very common html attribute), and the method missing approach doesn't
32
+ # work here, as it returns the Ruby class by default.
33
+ def class
34
+ __data_hash.key?( :class ) ? __data_hash[ :class ] : super
35
+ end
36
+
37
+ def to_h
38
+ __data_hash
39
+ end
40
+
41
+ def merge_hash( hash )
42
+ return self if !hash
43
+ hash.each do |key, value|
44
+ self.send("#{ key }=", value)
45
+ end
46
+ self
47
+ end
48
+
49
+ def self.from_hash( hash )
50
+ new.merge_hash( hash )
51
+ end
52
+
53
+ # for deep dup
54
+ def initialize_copy( source )
55
+ self.__data_hash = source.__data_hash.dup
56
+ end
57
+
58
+ end
59
+
60
+ class TagsConfiguration
61
+ TAGS = [ :table, :thead, :tbody, :tfoot, :tr, :th, :td ]
62
+ attr_accessor(*TAGS)
63
+
64
+ def initialize
65
+ TAGS.each do |tag|
66
+ self.send("#{ tag }=", TagConfiguration.new)
67
+ end
68
+ end
69
+
70
+ def to_h
71
+ hashes = TAGS.map do |identifier|
72
+ self.send(identifier).to_h
73
+ end
74
+ { :tags => Hash[ TAGS.zip( hashes ) ] }
75
+ end
76
+
77
+ def merge_hash( hash )
78
+ return self if !hash
79
+ tags = hash[ :tags ]
80
+ TAGS.each do |tag|
81
+ self.send("#{ tag }").merge_hash( tags[ tag ] )
82
+ end if tags
83
+ self
84
+ end
85
+
86
+ def self.from_hash( hash )
87
+ new.merge_hash( hash )
88
+ end
89
+
90
+ # for deep dup
91
+ def initialize_copy( source )
92
+ TAGS.each do |tag|
93
+ self.send("#{ tag }=", source.send( tag ).dup)
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -22,7 +22,7 @@ module DiningTable
22
22
  identifier == identifier_
23
23
  end
24
24
 
25
- [ :start_table, :end_table, :render_header, :start_body, :end_body, :row, :render_footer, :output ].each do |method|
25
+ [ :start_table, :end_table, :render_header, :start_body, :end_body, :render_row, :render_footer, :output ].each do |method|
26
26
  self.class_eval <<-eos, __FILE__, __LINE__+1
27
27
  def #{method}(*args)
28
28
  end
@@ -3,7 +3,10 @@ module DiningTable
3
3
  class Table
4
4
 
5
5
  attr_accessor :collection, :presenter, :options, :index, :columns, :action_columns, :view_context
6
-
6
+
7
+ attr_accessor :no_header, :no_footer
8
+ private :no_header, :no_footer, :no_header=, :no_footer=
9
+
7
10
  def initialize( collection, view_context, options = {} )
8
11
  self.collection = collection
9
12
  self.view_context = view_context
@@ -27,7 +30,7 @@ module DiningTable
27
30
  presenter.render_row( object )
28
31
  end
29
32
  presenter.end_body
30
- presenter.render_footer
33
+ presenter.render_footer unless no_footer
31
34
  presenter.end_table
32
35
  presenter.output
33
36
  end
@@ -36,11 +39,17 @@ module DiningTable
36
39
  view_context
37
40
  end
38
41
  alias_method :h, :helpers
39
-
42
+
43
+ def skip_header
44
+ self.no_header = true
45
+ end
46
+
47
+ def skip_footer
48
+ self.no_footer = true
49
+ end
50
+
40
51
  private
41
52
 
42
- attr_accessor :no_header
43
-
44
53
  # auxiliary function
45
54
  def column(name, options = {}, &block)
46
55
  klass = options[:class]
@@ -62,10 +71,6 @@ module DiningTable
62
71
  Presenters::HTMLPresenter
63
72
  end
64
73
 
65
- def skip_header
66
- self.no_header = true
67
- end
68
-
69
74
  end
70
75
 
71
76
  end
@@ -63,7 +63,8 @@ describe 'HTMLTableSpec' do
63
63
  xpath = "/table/tfoot/tr[1]/td[#{ col_index + 1 }]"
64
64
  check_not_empty(doc.elements, xpath)
65
65
  doc.elements.each(xpath) do |element|
66
- element.text.must_equal footer
66
+ element.text.must_equal footer if footer
67
+ element.text.must_be_nil if !footer # avoid minitest deprecation warning
67
68
  end
68
69
  end
69
70
  # last footer has link
@@ -73,6 +74,15 @@ describe 'HTMLTableSpec' do
73
74
  end
74
75
  end
75
76
 
77
+ it "allows skipping header and footer" do
78
+ @cars = CarWithHumanAttributeName.collection
79
+ html = CarTableWithoutHeader.new(@cars, @view_context).render
80
+ doc = REXML::Document.new( html )
81
+ table = doc.elements.first
82
+ table.elements.size.must_equal 1 # only body
83
+ table.elements.first.name.must_equal 'tbody'
84
+ end
85
+
76
86
  it "correctly renders a table with column options and column blocks" do
77
87
  html = CarTableWithOptions.new(@cars, nil).render
78
88
  doc = document( html )
@@ -96,6 +106,28 @@ describe 'HTMLTableSpec' do
96
106
  end
97
107
  end
98
108
 
109
+ it "still supports deprecated syntax for html column options" do
110
+ html = CarTableWithOptionsOldSyntax.new(@cars, nil).render
111
+ doc = document( html )
112
+ @cars.each_with_index do |car, index|
113
+ [ :brand, :stock ].each_with_index do |column, col_index|
114
+ xpath = "/table/tbody/tr[#{ index + 1 }]/td[#{ col_index + 1 }]"
115
+ check_not_empty(doc.elements, xpath)
116
+ doc.elements.each(xpath) do |element|
117
+ class_ = col_index == 0 ? 'center' : 'left'
118
+ element.attributes.get_attribute('class').value.must_equal class_
119
+ end
120
+ end
121
+ end
122
+ # also check header
123
+ [ 1, 2 ].each do |index|
124
+ doc.elements.each("/table/thead/tr[1]/th[#{ index }]") do |element|
125
+ class_ = index == 1 ? 'center' : 'left'
126
+ element.attributes.get_attribute('class').value.must_equal class_
127
+ end
128
+ end
129
+ end
130
+
99
131
  it "correctly renders a table with actions" do
100
132
  html = CarTableWithActions.new(@cars, @view_context).render
101
133
  doc = document( html )
@@ -140,6 +172,38 @@ describe 'HTMLTableSpec' do
140
172
  end
141
173
 
142
174
  it "respects presenter options" do
175
+ html = CarTableWithFooter.new(@cars, @view_context,
176
+ :presenter => DiningTable::Presenters::HTMLPresenter.new(
177
+ :tags => { :table => { :class => 'table table-bordered', :id => 'my_table_id', :'data-custom' => 'custom1!' },
178
+ :thead => { :class => 'mythead', :id => 'my_thead_id', :'data-custom' => 'custom2!' },
179
+ :tbody => { :class => 'mytbody', :id => 'my_tbody_id', :'data-custom' => 'custom3!' },
180
+ :tfoot => { :class => 'mytfoot', :id => 'my_tfoot_id', :'data-custom' => 'custom4!' },
181
+ :tr => { :class => 'mytr', :'data-custom' => 'custom5!' },
182
+ :th => { :class => 'myth', :'data-custom' => 'custom6!' },
183
+ :td => { :class => 'mytd', :'data-custom' => 'custom7!' }
184
+ } ) ).render
185
+ doc = document( html )
186
+ table = doc.elements.first
187
+ check_attributes( table, ['class', 'id', 'data-custom'], ['table table-bordered', 'my_table_id', 'custom1!'])
188
+ header = table.elements[1] # 1 = first element (not second) in REXML
189
+ check_attributes( header, ['class', 'id', 'data-custom'], ['mythead', 'my_thead_id', 'custom2!'])
190
+ body = table.elements[2] # 2 = second element (not third) in REXML
191
+ check_attributes( body, ['class', 'id', 'data-custom'], ['mytbody', 'my_tbody_id', 'custom3!'])
192
+ footer = table.elements[3] # 3 = third element (not fourth) in REXML
193
+ check_attributes( footer, ['class', 'id', 'data-custom'], ['mytfoot', 'my_tfoot_id', 'custom4!'])
194
+ row = header.elements.first
195
+ check_attributes( row, ['class', 'data-custom'], ['mytr', 'custom5!'])
196
+ row.elements.each do |header_cell|
197
+ check_attributes( header_cell, ['class', 'data-custom'], ['myth', 'custom6!'])
198
+ end
199
+ body.elements.each do |row_|
200
+ check_attributes( row_, ['class', 'data-custom'], ['mytr', 'custom5!'])
201
+ end
202
+ row = footer.elements.first
203
+ check_attributes( row, ['class', 'data-custom'], ['mytr', 'custom5!'])
204
+ end
205
+
206
+ it "still supports old (deprecated) way of specifying the table class" do
143
207
  html = CarTable.new(@cars, nil,
144
208
  :presenter => DiningTable::Presenters::HTMLPresenter.new( :class => 'table table-bordered' ) ).render
145
209
  doc = document( html )
@@ -156,7 +220,7 @@ describe 'HTMLTableSpec' do
156
220
 
157
221
  it "respects global html options" do
158
222
  DiningTable.configure do |config|
159
- config.html_presenter.default_options = { :class => 'table-hover',
223
+ config.html_presenter.default_options = { :tags => { :table => { :class => 'table-hover' }, :tr => { :class => 'rowrow' } },
160
224
  :wrap => { :tag => :div, :class => 'table-responsive' } }
161
225
  end
162
226
  html = CarTable.new(@cars, nil).render
@@ -165,12 +229,45 @@ describe 'HTMLTableSpec' do
165
229
  doc.elements.first.attributes.get_attribute('class').value.must_equal 'table-responsive'
166
230
  table = doc.elements.first.elements.first
167
231
  table.attributes.get_attribute('class').value.must_equal 'table-hover'
232
+ body = table.elements[2]
233
+ body.elements.each do |row|
234
+ row.attributes.get_attribute('class').value.must_equal 'rowrow'
235
+ end
168
236
  # reset configuration for other specs
169
237
  DiningTable.configure do |config|
170
238
  config.html_presenter.default_options = { }
171
239
  end
172
240
  end
173
241
 
242
+ it "respects in-table presenter config blocks" do
243
+ html = CarTableWithConfigBlocks.new(@cars, @view_context).render
244
+ doc = REXML::Document.new( html )
245
+ table = doc.elements.first
246
+ table.attributes.get_attribute('class').value.must_equal 'my-table-class'
247
+ header = table.elements.first
248
+ header.attributes.get_attribute('class').value.must_equal 'my-thead-class'
249
+ row = header.elements.first
250
+ row.attributes.get_attribute('class').value.must_equal 'header-tr'
251
+ row.elements.each do |cell|
252
+ cell.attributes.get_attribute('class').value.must_equal 'header-th'
253
+ end
254
+ body = table.elements[2]
255
+ body.elements.each_with_index do |row_, index|
256
+ row_.attributes.get_attribute('class').value.must_match( index.odd? ? /odd/ : /even/ )
257
+ row_.attributes.get_attribute('class').value.must_match( /lowstock/ ) if @cars[index].stock < 10
258
+ row_.elements.each_with_index do |td, td_index|
259
+ if td_index == 0
260
+ td.attributes.get_attribute('class').value.must_equal 'left'
261
+ elsif td_index == 1
262
+ td.attributes.get_attribute('class').value.must_match( /center/ )
263
+ td.attributes.get_attribute('class').value.must_match( /five_doors/ ) if @cars[index].number_of_doors == 5
264
+ else
265
+ td.attributes.get_attribute('class').must_be_nil if td_index != 1
266
+ end
267
+ end
268
+ end
269
+ end
270
+
174
271
  def document( html )
175
272
  doc = REXML::Document.new( html )
176
273
  check_table_structure( doc )
@@ -203,6 +300,13 @@ describe 'HTMLTableSpec' do
203
300
  not_empty?(node, xpath).must_equal true
204
301
  end
205
302
 
303
+ def check_attributes( element, attributes, values )
304
+ attributes.each_with_index do |attribute, index|
305
+ value = values[ index ]
306
+ element.attributes.get_attribute( attribute ).value.must_equal value
307
+ end
308
+ end
309
+
206
310
  class ViewContext
207
311
  def link_to(text, url)
208
312
  "<a href=\"#{ url }\">#{ text }</a>"
@@ -2,9 +2,10 @@ class CarTableWithActions < DiningTable::Table
2
2
  def define
3
3
  column :brand
4
4
  column :number_of_doors
5
- actions :header => 'Action', :html => { :td_options => { class: 'left' }, :th_options => { class: :left } } do |object|
6
- action { |object| h.link_to( 'Show', '#show' ) }
7
- action { |object| h.link_to( 'Edit', '#edit' ) }
5
+ actions :header => 'Action', :html => { :td => { class: 'left' }, :th => { class: :left } } do |object|
6
+ h.link_to( 'Show', '#show' ) # doesn't do anything, simply verify that h helper is available
7
+ action { |object_| h.link_to( 'Show', '#show' ) }
8
+ action { |object_| h.link_to( 'Edit', '#edit' ) }
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,31 @@
1
+ class CarTableWithConfigBlocks < DiningTable::Table
2
+ def define
3
+
4
+ presenter.table_config do |config|
5
+ config.table.class = 'my-table-class'
6
+ config.thead.class = 'my-thead-class'
7
+ end if presenter.type?(:html)
8
+
9
+ presenter.row_config do |config, index, object|
10
+ if index == :header
11
+ config.tr.class = 'header-tr'
12
+ config.th.class = 'header-th'
13
+ elsif index == :footer
14
+ config.tr.class = 'header-tr'
15
+ else
16
+ config.tr.class = index.odd? ? 'odd' : 'even'
17
+ config.tr.class += ' lowstock' if object.stock < 10
18
+ end
19
+ end if presenter.type?(:html)
20
+
21
+ column :brand, :html => { :td => { :class => 'left' } }
22
+
23
+ number_of_doors_options = ->( config, index, object ) do
24
+ config.td.class = 'center'
25
+ config.td.class += ' five_doors' if object && object.number_of_doors == 5
26
+ end
27
+ column :number_of_doors, :footer => 'Total', :html => number_of_doors_options
28
+
29
+ column :stock, :footer => lambda { h.link_to("Total: #{ collection.map(&:stock).inject(&:+) }", '#') }
30
+ end
31
+ end
@@ -1,9 +1,9 @@
1
1
  class CarTableWithOptions < DiningTable::Table
2
2
  def define
3
- column :brand, :html => { :td_options => { class: 'center' }, :th_options => { class: :center } } do |object|
3
+ column :brand, :html => { :td => { class: 'center' }, :th => { class: :center } } do |object|
4
4
  object.brand.upcase
5
5
  end
6
- column :stock, :html => { :td_options => { class: 'left' }, :th_options => { class: :left } }
6
+ column :stock, :html => { :td => { class: 'left' }, :th => { class: :left } }
7
7
  column :launch_date if options[:with_normal_launch_date]
8
8
  column :launch_date, :class => DateColumn if options[:with_date_column_launch_date]
9
9
  end
@@ -0,0 +1,10 @@
1
+ class CarTableWithOptionsOldSyntax < DiningTable::Table
2
+ def define
3
+ column :brand, :html => { :td => { class: 'center' }, :th => { class: :center } } do |object|
4
+ object.brand.upcase
5
+ end
6
+ column :stock, :html => { :td_options => { class: 'left' }, :th_options => { class: :left } } # deprecated options syntax
7
+ column :launch_date if options[:with_normal_launch_date]
8
+ column :launch_date, :class => DateColumn if options[:with_date_column_launch_date]
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ class CarTableWithoutHeader < DiningTable::Table
2
+ def define
3
+ skip_header
4
+ skip_footer
5
+
6
+ column :brand
7
+ column :number_of_doors, :footer => 'Total'
8
+ end
9
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dining-table
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michaël Van Damme
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-26 00:00:00.000000000 Z
11
+ date: 2018-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -62,6 +62,7 @@ files:
62
62
  - lib/dining-table/presenters/csv_presenter.rb
63
63
  - lib/dining-table/presenters/excel_presenter.rb
64
64
  - lib/dining-table/presenters/html_presenter.rb
65
+ - lib/dining-table/presenters/html_presenter_configuration.rb
65
66
  - lib/dining-table/presenters/presenter.rb
66
67
  - lib/dining-table/presenters/spreadsheet_presenter.rb
67
68
  - lib/dining-table/table.rb
@@ -72,9 +73,12 @@ files:
72
73
  - spec/spec_helper.rb
73
74
  - spec/tables/car_table.rb
74
75
  - spec/tables/car_table_with_actions.rb
76
+ - spec/tables/car_table_with_config_blocks.rb
75
77
  - spec/tables/car_table_with_footer.rb
76
78
  - spec/tables/car_table_with_header.rb
77
79
  - spec/tables/car_table_with_options.rb
80
+ - spec/tables/car_table_with_options_old_syntax.rb
81
+ - spec/tables/car_table_without_header.rb
78
82
  homepage: http://github.com/mvdamme/dining-table
79
83
  licenses:
80
84
  - MIT