dining-table 0.2.1 → 1.0.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 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