katalyst-tables 3.0.0.beta1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -2
  3. data/README.md +65 -187
  4. data/app/assets/builds/katalyst/tables.esm.js +17 -47
  5. data/app/assets/builds/katalyst/tables.js +17 -47
  6. data/app/assets/builds/katalyst/tables.min.js +1 -1
  7. data/app/assets/builds/katalyst/tables.min.js.map +1 -1
  8. data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -0
  9. data/app/assets/stylesheets/katalyst/tables/_summary.scss +14 -0
  10. data/app/assets/stylesheets/katalyst/tables/typed-columns/_boolean.scss +1 -1
  11. data/app/assets/stylesheets/katalyst/tables/typed-columns/_currency.scss +3 -0
  12. data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +1 -1
  13. data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +1 -1
  14. data/app/assets/stylesheets/katalyst/tables/typed-columns/_number.scss +3 -0
  15. data/app/components/concerns/katalyst/tables/has_table_content.rb +19 -10
  16. data/app/components/concerns/katalyst/tables/identifiable.rb +51 -0
  17. data/app/components/concerns/katalyst/tables/orderable.rb +35 -105
  18. data/app/components/concerns/katalyst/tables/row_renderer.rb +1 -1
  19. data/app/components/concerns/katalyst/tables/selectable.rb +18 -75
  20. data/app/components/concerns/katalyst/tables/sortable.rb +51 -17
  21. data/app/components/katalyst/summary_table_component.html.erb +15 -0
  22. data/app/components/katalyst/summary_table_component.rb +44 -0
  23. data/app/components/katalyst/table_component.html.erb +4 -4
  24. data/app/components/katalyst/table_component.rb +271 -53
  25. data/app/components/katalyst/tables/body_row_component.html.erb +5 -0
  26. data/app/components/katalyst/tables/body_row_component.rb +4 -31
  27. data/app/components/katalyst/tables/cell_component.rb +85 -0
  28. data/app/components/katalyst/tables/{body → cells}/boolean_component.rb +8 -2
  29. data/app/components/katalyst/tables/{body → cells}/currency_component.rb +7 -7
  30. data/app/components/katalyst/tables/{body → cells}/date_component.rb +12 -9
  31. data/app/components/katalyst/tables/{body → cells}/date_time_component.rb +13 -10
  32. data/app/components/katalyst/tables/{body → cells}/number_component.rb +5 -5
  33. data/app/components/katalyst/tables/cells/ordinal_component.rb +44 -0
  34. data/app/components/katalyst/tables/{body → cells}/rich_text_component.rb +8 -5
  35. data/app/components/katalyst/tables/cells/select_component.rb +39 -0
  36. data/app/components/katalyst/tables/data.rb +30 -0
  37. data/app/components/katalyst/tables/header_row_component.html.erb +5 -0
  38. data/app/components/katalyst/tables/header_row_component.rb +4 -25
  39. data/app/components/katalyst/tables/label.rb +37 -0
  40. data/app/components/katalyst/tables/orderable/form_component.rb +38 -0
  41. data/app/components/katalyst/tables/selectable/form_component.html.erb +3 -3
  42. data/app/components/katalyst/tables/selectable/form_component.rb +8 -11
  43. data/app/components/katalyst/tables/summary/body_component.html.erb +3 -0
  44. data/app/components/katalyst/tables/summary/body_component.rb +10 -0
  45. data/app/components/katalyst/tables/summary/header_component.html.erb +3 -0
  46. data/app/components/katalyst/tables/summary/header_component.rb +10 -0
  47. data/app/components/katalyst/tables/summary/row_component.html.erb +4 -0
  48. data/app/components/katalyst/tables/summary/row_component.rb +12 -0
  49. data/app/controllers/concerns/katalyst/tables/backend.rb +17 -28
  50. data/app/helpers/katalyst/tables/frontend.rb +67 -2
  51. data/app/javascript/tables/application.js +0 -5
  52. data/app/javascript/tables/orderable/form_controller.js +8 -6
  53. data/app/javascript/tables/orderable/item_controller.js +9 -0
  54. data/app/models/concerns/katalyst/tables/collection/core.rb +6 -1
  55. data/app/models/concerns/katalyst/tables/collection/pagination.rb +2 -2
  56. data/app/models/concerns/katalyst/tables/collection/sorting.rb +86 -18
  57. data/app/models/katalyst/tables/collection/array.rb +38 -0
  58. data/app/models/katalyst/tables/collection/base.rb +4 -0
  59. data/app/models/katalyst/tables/collection/filter.rb +2 -2
  60. data/config/importmap.rb +1 -0
  61. data/config/locales/tables.en.yml +0 -6
  62. data/lib/katalyst/tables/config.rb +23 -0
  63. data/lib/katalyst/tables.rb +9 -0
  64. metadata +32 -30
  65. data/app/components/concerns/katalyst/tables/body/typed_columns.rb +0 -132
  66. data/app/components/concerns/katalyst/tables/configurable_component.rb +0 -52
  67. data/app/components/concerns/katalyst/tables/header/typed_columns.rb +0 -179
  68. data/app/components/katalyst/tables/body/attachment_component.rb +0 -58
  69. data/app/components/katalyst/tables/body/link_component.rb +0 -40
  70. data/app/components/katalyst/tables/body_cell_component.rb +0 -55
  71. data/app/components/katalyst/tables/header/attachment_component.rb +0 -15
  72. data/app/components/katalyst/tables/header/boolean_component.rb +0 -15
  73. data/app/components/katalyst/tables/header/currency_component.rb +0 -15
  74. data/app/components/katalyst/tables/header/date_component.rb +0 -15
  75. data/app/components/katalyst/tables/header/date_time_component.rb +0 -15
  76. data/app/components/katalyst/tables/header/link_component.rb +0 -15
  77. data/app/components/katalyst/tables/header/number_component.rb +0 -15
  78. data/app/components/katalyst/tables/header/rich_text_component.rb +0 -15
  79. data/app/components/katalyst/tables/header_cell_component.rb +0 -97
  80. data/app/helpers/katalyst/tables/frontend/helper.rb +0 -31
  81. data/app/javascript/tables/turbo/collection_controller.js +0 -38
  82. data/app/models/katalyst/tables/collection/sort_form.rb +0 -120
@@ -0,0 +1,15 @@
1
+ <%# Simulating a normal table render for the side effects %>
2
+ <% capture do %>
3
+ <%= header_row %>
4
+ <% body_rows.each do |body| %>
5
+ <%= body %>
6
+ <% end %>
7
+ <% end %>
8
+
9
+ <%= tag.table(**html_attributes) do %>
10
+ <%= tag.tbody(**tbody_attributes) do %>
11
+ <% summary_rows.each do |summary_row| %>
12
+ <%= summary_row %>
13
+ <% end %>
14
+ <% end %>
15
+ <% end %>
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ # A component for rendering a summary table for a model.
5
+ # @example
6
+ # <%= Katalyst::SummaryTableComponent.new(model: @person) do |row, person| %>
7
+ # <%= row.text :name do |cell| %>
8
+ # <%= link_to cell.value, person %>
9
+ # <% end %>
10
+ # <%= row.text :email %>
11
+ # <% end %>
12
+ # Generates:
13
+ # <table>
14
+ # <tr><th>Name</th><td><a href="/people/1">Aaron</a></td></tr>
15
+ # <tr><th>Email</th><td>aaron@example.com</td></tr>
16
+ # </table>
17
+ class SummaryTableComponent < TableComponent
18
+ renders_many :summary_rows, Tables::Summary::RowComponent
19
+
20
+ def initialize(model:, **)
21
+ super(collection: [model], **)
22
+
23
+ @summary_rows = []
24
+
25
+ update_html_attributes(class: "summary-table")
26
+ end
27
+
28
+ def with_cell(cell, &)
29
+ if row.header?
30
+ @summary_rows << with_summary_row do |row|
31
+ row.with_header do |header|
32
+ header.with_cell(cell)
33
+ end
34
+ end
35
+ @index = 0
36
+ else
37
+ @summary_rows[@index].with_body do |body|
38
+ body.with_cell(cell, &)
39
+ end
40
+ @index += 1
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,13 +1,13 @@
1
1
  <%= tag.table(**html_attributes) do %>
2
2
  <%= render caption if caption? %>
3
- <% if header? %>
3
+ <% if header_row? %>
4
4
  <%= tag.thead(**thead_attributes) do %>
5
- <%= header_row.render_in(view_context, &row_proc) %>
5
+ <%= header_row %>
6
6
  <% end %>
7
7
  <% end %>
8
8
  <%= tag.tbody(**tbody_attributes) do %>
9
- <% collection.each do |record| %>
10
- <%= body_row(record).render_in(view_context) { |r| row_proc.call(r, record) } %>
9
+ <% body_rows.each do |body_row| %>
10
+ <%= body_row %>
11
11
  <% end %>
12
12
  <% end %>
13
13
  <% end %>
@@ -4,103 +4,321 @@ module Katalyst
4
4
  # A component for rendering a table from a collection, with a header row.
5
5
  # ```erb
6
6
  # <%= Katalyst::TableComponent.new(collection: @people) do |row, person| %>
7
- # <%= row.cell :name do |cell| %>
7
+ # <%= row.text :name do |cell| %>
8
8
  # <%= link_to cell.value, person %>
9
9
  # <% end %>
10
- # <%= row.cell :email %>
10
+ # <%= row.text :email %>
11
11
  # <% end %>
12
12
  # ```
13
13
  class TableComponent < ViewComponent::Base
14
14
  include Katalyst::HtmlAttributes
15
- include Tables::ConfigurableComponent
16
15
  include Tables::HasTableContent
17
16
 
17
+ # Load table extensions. This allows users to disable specific extensions
18
+ # if they want to implement alternatives, e.g. a different sorting UI.
19
+ Katalyst::Tables.config.component_extensions.each do |extension|
20
+ include extension.constantize
21
+ end
22
+
18
23
  attr_reader :collection, :object_name
19
24
 
20
- config_component :header_row, default: "Katalyst::Tables::HeaderRowComponent"
21
- config_component :header_cell, default: "Katalyst::Tables::HeaderCellComponent"
22
- config_component :body_row, default: "Katalyst::Tables::BodyRowComponent"
23
- config_component :body_cell, default: "Katalyst::Tables::BodyCellComponent"
24
- config_component :caption, default: "Katalyst::Tables::EmptyCaptionComponent"
25
+ renders_one :caption, Katalyst::Tables::EmptyCaptionComponent
26
+ renders_one :header_row, Katalyst::Tables::HeaderRowComponent
27
+ renders_many :body_rows, Katalyst::Tables::BodyRowComponent
28
+
29
+ define_html_attribute_methods(:thead_attributes)
30
+ define_html_attribute_methods(:tbody_attributes)
25
31
 
26
32
  # Construct a new table component. This entry point supports a large number
27
33
  # of options for customizing the table. The most common options are:
28
- # - `collection`: the collection to render
29
- # - `sorting`: the sorting to apply to the collection (defaults to collection.storing if available)
30
- # - `header`: whether to render the header row (defaults to true, supports options)
31
- # - `caption`: whether to render the caption (defaults to true, supports options)
32
- # - `generate_ids`: whether to generate ids for each row (defaults to true)
33
- # - `object_name`: the name of the object to use for partial rendering (defaults to collection.model_name.i18n_key)
34
- # - `partial`: the name of the partial to use for rendering each row (defaults to to_partial_path on the object)
35
- # - `as`: the name of the local variable to use for rendering each row (defaults to collection.model_name.param_key)
34
+ # @param collection [Katalyst::Tables::Collection::Core] the collection to render
35
+ # @param header [Boolean] whether to render the header row (defaults to true, supports options)
36
+ # @param caption [Boolean,Hash] whether to render the caption (defaults to true, supports options)
37
+ # @param generate_ids [Boolean] whether to generate dom ids for the table and rows
38
+ #
39
+ # If no block is provided when the table is rendered then the table will look for a row partial:
40
+ # @param object_name [Symbol] the name of the object to use for partial rendering
41
+ # (defaults to collection.model_name.i18n_key)
42
+ # @param partial [String] the name of the partial to use for rendering each row
43
+ # (defaults to to_partial_path on the object)
44
+ # @param as [Symbol] the name of the local variable to use for rendering each row
45
+ # (defaults to collection.model_name.param_key)
46
+ #
36
47
  # In addition to these options, standard HTML attributes can be passed which will be added to the table tag.
37
48
  def initialize(collection:,
38
- sorting: nil,
39
49
  header: true,
40
50
  caption: true,
41
- generate_ids: true,
42
- **html_attributes)
43
- @collection = collection
44
-
45
- # sorting: instance of Katalyst::Tables::Collection::SortForm.
46
- # If not provided will be inferred from the collection.
47
- @sorting = sorting || html_attributes.delete(:sort) # backwards compatibility with old `sort` option
51
+ generate_ids: false,
52
+ object_name: nil,
53
+ partial: nil,
54
+ as: nil,
55
+ **)
56
+ @collection = normalize_collection(collection)
48
57
 
49
58
  # header: true means render the header row, header: false means no header row, if a hash, passes as options
50
- @header = header
51
- @header_options = (header if header.is_a?(Hash)) || {}
59
+ @header_options = header
52
60
 
53
61
  # caption: true means render the caption, caption: false means no caption, if a hash, passes as options
54
- @caption = caption
55
- @caption_options = (caption if caption.is_a?(Hash)) || {}
62
+ @caption_options = caption
56
63
 
57
- @generate_ids = generate_ids
64
+ @header_row_callbacks = []
65
+ @body_row_callbacks = []
66
+ @header_row_cell_callbacks = []
67
+ @body_row_cell_callbacks = []
58
68
 
59
- super(**html_attributes)
69
+ super(generate_ids:, object_name:, partial:, as:, **)
60
70
  end
61
71
 
62
- def id
63
- html_attributes[:id]
72
+ def before_render
73
+ super
74
+
75
+ if @caption_options
76
+ options = (@caption_options.is_a?(Hash) ? @caption_options : {})
77
+ with_caption(self, **options)
78
+ end
79
+
80
+ if @header_options
81
+ options = @header_options.is_a?(Hash) ? @header_options : {}
82
+ with_header_row(**options) do |row|
83
+ @header_row_callbacks.each { |callback| callback.call(row, record) }
84
+ row_content(row, nil)
85
+ end
86
+ end
87
+
88
+ collection.each do |record|
89
+ with_body_row do |row|
90
+ @body_row_callbacks.each { |callback| callback.call(row, record) }
91
+ row_content(row, record)
92
+ end
93
+ end
64
94
  end
65
95
 
66
- def caption?
67
- @caption.present?
96
+ def inspect
97
+ "#<#{self.class.name} collection: #{collection.inspect}>"
68
98
  end
69
99
 
70
- def caption
71
- caption_component&.new(self)
100
+ delegate :header?, :body?, to: :@current_row
101
+
102
+ def row
103
+ @current_row
72
104
  end
73
105
 
74
- def header?
75
- @header.present?
106
+ def record
107
+ @current_record
76
108
  end
77
109
 
78
- def header_row
79
- header_row_component.new(self, **@header_options)
110
+ # When rendering a row we pass the table to the row instead of the row itself. This lets the table define the
111
+ # column entry points so it's easy to define column extensions in subclasses. When a user wants to set html
112
+ # attributes on the row, they will call `row.html_attributes = { ... }`, so we need to proxy that call to the
113
+ # current row (if set).
114
+ def html_attributes=(attributes)
115
+ if row.present?
116
+ row.html_attributes = attributes
117
+ else
118
+ @html_attributes = HtmlAttributes.options_to_html_attributes(attributes)
119
+ end
80
120
  end
81
121
 
82
- def body_row(record)
83
- body_row_component.new(self, record)
122
+ # Generates a column from values rendered as text.
123
+ #
124
+ # @param column [Symbol] the column's name, called as a method on the record
125
+ # @param label [String|nil] the label to use for the column header
126
+ # @param heading [boolean] if true, data cells will use `th` tags
127
+ # @param ** [Hash] HTML attributes to be added to column cells
128
+ # @param & [Proc] optional block to wrap the cell content
129
+ #
130
+ # If a block is provided, it will be called with the cell component as an argument.
131
+ # @yieldparam cell [Katalyst::Tables::CellComponent] the cell component
132
+ #
133
+ # @return [void]
134
+ #
135
+ # @example Render a generic text column for any value that supports `to_s`
136
+ # <% row.text :name %> # label => <th>Name</th>, data => <td>John Doe</td>
137
+ def text(column, label: nil, heading: false, **, &)
138
+ with_cell(Tables::CellComponent.new(
139
+ collection:, row:, column:, record:, label:, heading:, **,
140
+ ), &)
84
141
  end
142
+ alias cell text
85
143
 
86
- def sorting
87
- return @sorting if @sorting.present?
144
+ # Generates a column from boolean values rendered as "Yes" or "No".
145
+ #
146
+ # @param column [Symbol] the column's name, called as a method on the record
147
+ # @param label [String|nil] the label to use for the column header
148
+ # @param heading [boolean] if true, data cells will use `th` tags
149
+ # @param ** [Hash] HTML attributes to be added to column cells
150
+ # @param & [Proc] optional block to alter the cell content
151
+ #
152
+ # If a block is provided, it will be called with the boolean cell component as an argument.
153
+ # @yieldparam cell [Katalyst::Tables::Cells::BooleanComponent] the cell component
154
+ #
155
+ # @return [void]
156
+ #
157
+ # @example Render a boolean column indicating whether the record is active
158
+ # <% row.boolean :active %> # => <td>Yes</td>
159
+ def boolean(column, label: nil, heading: false, **, &)
160
+ with_cell(Tables::Cells::BooleanComponent.new(
161
+ collection:, row:, column:, record:, label:, heading:, **,
162
+ ), &)
163
+ end
88
164
 
89
- collection.sorting if collection.respond_to?(:sorting)
165
+ # Generates a column from date values rendered using I18n.l.
166
+ # The default format is :default, can be configured or overridden.
167
+ #
168
+ # @param column [Symbol] the column's name, called as a method on the record
169
+ # @param label [String|nil] the label to use for the column header
170
+ # @param heading [boolean] if true, data cells will use `th` tags
171
+ # @param format [Symbol] the I18n date format to use when rendering
172
+ # @param relative [Boolean] if true, the date may be shown as a relative date (if within 5 days)
173
+ # @param ** [Hash] HTML attributes to be added to column cells
174
+ #
175
+ # If a block is provided, it will be called with the date cell component as an argument.
176
+ # @yieldparam cell [Katalyst::Tables::Cells::DateComponent] the cell component
177
+ #
178
+ # @return [void]
179
+ #
180
+ # @example Render a date column describing when the record was created
181
+ # <% row.date :created_at %> # => <td>29 Feb 2024</td>
182
+ def date(column, label: nil, heading: false, format: Tables.config.date_format, relative: true, **, &)
183
+ with_cell(Tables::Cells::DateComponent.new(
184
+ collection:, row:, column:, record:, label:, heading:, format:, relative:, **,
185
+ ), &)
90
186
  end
91
187
 
92
- def generate_ids?
93
- @generate_ids.present?
188
+ # Generates a column from datetime values rendered using I18n.l.
189
+ # The default format is :default, can be configured or overridden.
190
+ #
191
+ # @param column [Symbol] the column's name, called as a method on the record
192
+ # @param label [String|nil] the label to use for the column header
193
+ # @param heading [boolean] if true, data cells will use `th` tags
194
+ # @param format [Symbol] the I18n datetime format to use when rendering
195
+ # @param relative [Boolean] if true, the datetime may be(if today) shown as a relative date/time
196
+ # @param ** [Hash] HTML attributes to be added to column cells
197
+ # @param & [Proc] optional block to alter the cell content
198
+ #
199
+ # If a block is provided, it will be called with the date time cell component as an argument.
200
+ # @yieldparam cell [Katalyst::Tables::Cells::DateTimeComponent] the cell component
201
+ #
202
+ # @return [void]
203
+ #
204
+ # @example Render a datetime column describing when the record was created
205
+ # <% row.datetime :created_at %> # => <td>29 Feb 2024, 5:00pm</td>
206
+ def datetime(column, label: nil, heading: false, format: Tables.config.datetime_format, relative: true, **, &)
207
+ with_cell(Tables::Cells::DateTimeComponent.new(
208
+ collection:, row:, column:, record:, label:, heading:, format:, relative:, **,
209
+ ), &)
94
210
  end
95
211
 
96
- def inspect
97
- "#<#{self.class.name} collection: #{collection.inspect}>"
212
+ # Generates a column from numeric values formatted appropriately.
213
+ #
214
+ # @param column [Symbol] the column's name, called as a method on the record
215
+ # @param label [String|nil] the label to use for the column header
216
+ # @param heading [boolean] if true, data cells will use `th` tags
217
+ # @param ** [Hash] HTML attributes to be added to column cells
218
+ # @param & [Proc] optional block to alter the cell content
219
+ #
220
+ # If a block is provided, it will be called with the number cell component as an argument.
221
+ # @yieldparam cell [Katalyst::Tables::Cells::NumberComponent] the cell component
222
+ #
223
+ # @return [void]
224
+ #
225
+ # @example Render the number of comments on a post
226
+ # <% row.number :comment_count %> # => <td>0</td>
227
+ def number(column, label: nil, heading: false, **, &)
228
+ with_cell(Tables::Cells::NumberComponent.new(
229
+ collection:, row:, column:, record:, label:, heading:, **,
230
+ ), &)
98
231
  end
99
232
 
100
- define_html_attribute_methods(:thead_attributes)
101
- define_html_attribute_methods(:tbody_attributes)
233
+ # Generates a column from numeric values rendered using `number_to_currency`.
234
+ #
235
+ # @param column [Symbol] the column's name, called as a method on the record
236
+ # @param label [String|nil] the label to use for the column header
237
+ # @param heading [boolean] if true, data cells will use `th` tags
238
+ # @param options [Hash] options to be passed to `number_to_currency`
239
+ # @param ** [Hash] HTML attributes to be added to column cells
240
+ # @param & [Proc] optional block to alter the cell content
241
+ #
242
+ # If a block is provided, it will be called with the currency cell component as an argument.
243
+ # @yieldparam cell [Katalyst::Tables::Cells::CurrencyComponent] the cell component
244
+ #
245
+ # @return [void]
246
+ #
247
+ # @example Render a currency column for the price of a product
248
+ # <% row.currency :price %> # => <td>$3.50</td>
249
+ def currency(column, label: nil, heading: false, options: {}, **, &)
250
+ with_cell(Tables::Cells::CurrencyComponent.new(
251
+ collection:, row:, column:, record:, label:, heading:, options:, **,
252
+ ), &)
253
+ end
254
+
255
+ # Generates a column containing HTML markup.
256
+ #
257
+ # @param column [Symbol] the column's name, called as a method on the record
258
+ # @param label [String|nil] the label to use for the column header
259
+ # @param heading [boolean] if true, data cells will use `th` tags
260
+ # @param ** [Hash] HTML attributes to be added to column cells
261
+ # @param & [Proc] optional block to alter the cell content
262
+ #
263
+ # If a block is provided, it will be called with the rich text cell component as an argument.
264
+ # @yieldparam cell [Katalyst::Tables::Cells::RichTextComponent] the cell component
265
+ #
266
+ # @return [void]
267
+ #
268
+ # @note This method assumes that the method returns HTML-safe content.
269
+ # If the content is not HTML-safe, it will be escaped.
270
+ #
271
+ # @example Render a description column containing HTML markup
272
+ # <% row.rich_text :description %> # => <td><em>Emphasis</em></td>
273
+ def rich_text(column, label: nil, heading: false, options: {}, **, &)
274
+ with_cell(Tables::Cells::RichTextComponent.new(
275
+ collection:, row:, column:, record:, label:, heading:, options:, **,
276
+ ), &)
277
+ end
278
+
279
+ private
280
+
281
+ # Extension point for subclasses and extensions to customize header row rendering.
282
+ def add_header_row_callback(&block)
283
+ @header_row_callbacks << block
284
+ end
285
+
286
+ # Extension point for subclasses and extensions to customize body row rendering.
287
+ def add_body_row_callback(&block)
288
+ @body_row_callbacks << block
289
+ end
290
+
291
+ # Extension point for subclasses and extensions to customize header row cell rendering.
292
+ def add_header_row_cell_callback(&block)
293
+ @header_row_cell_callbacks << block
294
+ end
295
+
296
+ # Extension point for subclasses and extensions to customize body row cell rendering.
297
+ def add_body_row_cell_callback(&block)
298
+ @body_row_cell_callbacks << block
299
+ end
300
+
301
+ # @internal proxy calls to row.with_cell and apply callbacks
302
+ def with_cell(cell, &)
303
+ if row.header?
304
+ @header_row_cell_callbacks.each { |callback| callback.call(cell) }
305
+ # note, block is silently dropped, it's not used for headers
306
+ @current_row.with_cell(cell)
307
+ else
308
+ @body_row_cell_callbacks.each { |callback| callback.call(cell) }
309
+ @current_row.with_cell(cell, &)
310
+ end
311
+ end
102
312
 
103
- # Backwards compatibility with tables 1.0
104
- alias_method :options, :html_attributes=
313
+ def normalize_collection(collection)
314
+ case collection
315
+ when Array
316
+ Tables::Collection::Array.new.apply(collection)
317
+ when ActiveRecord::Relation
318
+ Tables::Collection::Base.new.apply(collection)
319
+ else
320
+ collection
321
+ end
322
+ end
105
323
  end
106
324
  end
@@ -0,0 +1,5 @@
1
+ <%= tag.tr(**html_attributes) do %>
2
+ <% cells.each do |cell| %>
3
+ <%= cell %>
4
+ <% end %>
5
+ <% end %>
@@ -4,29 +4,11 @@ module Katalyst
4
4
  module Tables
5
5
  class BodyRowComponent < ViewComponent::Base # :nodoc:
6
6
  include Katalyst::HtmlAttributes
7
- include Body::TypedColumns
8
7
 
9
- renders_many :columns, ->(component) { component }
8
+ renders_many :cells, ->(cell) { cell }
10
9
 
11
- def initialize(table, record)
12
- super()
13
-
14
- @table = table
15
- @record = record
16
- end
17
-
18
- def call
19
- content # generate content before rendering
20
-
21
- tag.tr(**html_attributes) do
22
- columns.each do |column|
23
- concat(column.to_s)
24
- end
25
- end
26
- end
27
-
28
- def cell(attribute, **, &)
29
- with_column(@table.body_cell_component.new(@table, @record, attribute, **), &)
10
+ def before_render
11
+ content # ensure content is rendered so html_attributes can be set
30
12
  end
31
13
 
32
14
  def header?
@@ -37,18 +19,9 @@ module Katalyst
37
19
  true
38
20
  end
39
21
 
40
- def default_html_attributes
41
- return {} unless @table.generate_ids?
42
-
43
- { id: dom_id(@record) }
44
- end
45
-
46
22
  def inspect
47
- "#<#{self.class.name} record: #{record.inspect}>"
23
+ "#<#{self.class.name}>"
48
24
  end
49
-
50
- # Backwards compatibility with tables 1.0
51
- alias_method :options, :html_attributes=
52
25
  end
53
26
  end
54
27
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ class CellComponent < ViewComponent::Base # :nodoc:
6
+ include Katalyst::HtmlAttributes
7
+
8
+ attr_reader :collection, :row, :column, :record
9
+
10
+ def initialize(collection:, row:, column:, record:, label:, heading:, **)
11
+ super(**)
12
+
13
+ @collection = collection
14
+ @row = row
15
+ @column = column
16
+ @record = record
17
+ @heading = heading
18
+
19
+ if @row.header?
20
+ @label = Label.new(collection:, column:, label:)
21
+ else
22
+ @data = Data.new(record:, column:)
23
+ end
24
+ end
25
+
26
+ # @return true if the cell is a heading cell (th).
27
+ def heading?
28
+ @row.header? || @heading
29
+ end
30
+
31
+ # Adds a component to wrap the content of the cell, similar to a layout in Rails views.
32
+ def with_content_wrapper(component)
33
+ @content_wrapper = component
34
+
35
+ self
36
+ end
37
+
38
+ def call
39
+ content = if content?
40
+ self.content
41
+ elsif @row.header?
42
+ label
43
+ else
44
+ rendered_value
45
+ end
46
+
47
+ content = @content_wrapper.with_content(content).render_in(view_context) if @content_wrapper
48
+
49
+ concat(content_tag(cell_tag, content, **html_attributes))
50
+ end
51
+
52
+ # Return the rendered and sanitized label for the column.
53
+ def label
54
+ @label&.to_s
55
+ end
56
+
57
+ # Return the raw value of the cell (i.e. the value of the data read from the record)
58
+ def value
59
+ @data&.value
60
+ end
61
+
62
+ # Return the serialized and sanitised data value for rendering in the cell.
63
+ def rendered_value
64
+ @data&.to_s
65
+ end
66
+
67
+ # Serialize data for use in blocks, i.e.
68
+ # row.text(:name) { |cell| tag.span(cell) }
69
+ def to_s
70
+ # Note, this can't be `content` because the block is evaluated in order to produce content.
71
+ rendered_value
72
+ end
73
+
74
+ def inspect
75
+ "#<#{self.class.name} method: #{@method.inspect}>"
76
+ end
77
+
78
+ private
79
+
80
+ def cell_tag
81
+ heading? ? :th : :td
82
+ end
83
+ end
84
+ end
85
+ end
@@ -2,12 +2,18 @@
2
2
 
3
3
  module Katalyst
4
4
  module Tables
5
- module Body
5
+ module Cells
6
6
  # Shows Yes/No for boolean values
7
- class BooleanComponent < BodyCellComponent
7
+ class BooleanComponent < CellComponent
8
8
  def rendered_value
9
9
  value ? "Yes" : "No"
10
10
  end
11
+
12
+ private
13
+
14
+ def default_html_attributes
15
+ { class: "type-boolean" }
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using Katalyst::HtmlAttributes::HasHtmlAttributes
4
-
5
3
  module Katalyst
6
4
  module Tables
7
- module Body
5
+ module Cells
8
6
  # Formats the value as a money value
9
7
  #
10
8
  # The value is expected to be in cents.
11
9
  # Adds a class to the cell to allow for custom styling
12
- class CurrencyComponent < BodyCellComponent
13
- def initialize(table, record, attribute, options: {}, **html_attributes)
14
- super(table, record, attribute, **html_attributes)
10
+ class CurrencyComponent < CellComponent
11
+ def initialize(options:, **)
12
+ super(**)
15
13
 
16
14
  @options = options
17
15
  end
@@ -20,8 +18,10 @@ module Katalyst
20
18
  value.present? ? number_to_currency(value / 100.0, @options) : ""
21
19
  end
22
20
 
21
+ private
22
+
23
23
  def default_html_attributes
24
- super.merge_html(class: "type-currency")
24
+ { class: "type-currency" }
25
25
  end
26
26
  end
27
27
  end