katalyst-tables 2.6.0 → 3.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/README.md +57 -213
  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/_ordinal.scss +38 -0
  10. data/app/assets/stylesheets/katalyst/tables/_table.scss +123 -0
  11. data/app/assets/stylesheets/katalyst/tables/typed-columns/_boolean.scss +4 -0
  12. data/app/assets/stylesheets/katalyst/tables/typed-columns/_currency.scss +5 -0
  13. data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +4 -0
  14. data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +4 -0
  15. data/app/assets/stylesheets/katalyst/tables/typed-columns/_index.scss +5 -0
  16. data/app/assets/stylesheets/katalyst/tables/typed-columns/_number.scss +5 -0
  17. data/app/components/concerns/katalyst/tables/has_table_content.rb +17 -8
  18. data/app/components/concerns/katalyst/tables/identifiable.rb +51 -0
  19. data/app/components/concerns/katalyst/tables/orderable.rb +35 -105
  20. data/app/components/concerns/katalyst/tables/selectable.rb +18 -74
  21. data/app/components/concerns/katalyst/tables/sortable.rb +51 -17
  22. data/app/components/katalyst/table_component.html.erb +4 -4
  23. data/app/components/katalyst/table_component.rb +277 -47
  24. data/app/components/katalyst/tables/body_row_component.html.erb +5 -0
  25. data/app/components/katalyst/tables/body_row_component.rb +4 -24
  26. data/app/components/katalyst/tables/cell_component.rb +85 -0
  27. data/app/components/katalyst/tables/cells/boolean_component.rb +20 -0
  28. data/app/components/katalyst/tables/cells/currency_component.rb +29 -0
  29. data/app/components/katalyst/tables/cells/date_component.rb +67 -0
  30. data/app/components/katalyst/tables/cells/date_time_component.rb +60 -0
  31. data/app/components/katalyst/tables/cells/number_component.rb +22 -0
  32. data/app/components/katalyst/tables/cells/ordinal_component.rb +44 -0
  33. data/app/components/katalyst/tables/cells/rich_text_component.rb +21 -0
  34. data/app/components/katalyst/tables/cells/select_component.rb +39 -0
  35. data/app/components/katalyst/tables/data.rb +30 -0
  36. data/app/components/katalyst/tables/empty_caption_component.rb +1 -1
  37. data/app/components/katalyst/tables/header_row_component.html.erb +5 -0
  38. data/app/components/katalyst/tables/header_row_component.rb +4 -24
  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 +6 -4
  42. data/app/components/katalyst/tables/selectable/form_component.rb +8 -11
  43. data/app/controllers/concerns/katalyst/tables/backend.rb +2 -28
  44. data/app/helpers/katalyst/tables/frontend.rb +48 -2
  45. data/app/javascript/tables/application.js +0 -5
  46. data/app/javascript/tables/orderable/form_controller.js +8 -6
  47. data/app/javascript/tables/orderable/item_controller.js +9 -0
  48. data/app/models/concerns/katalyst/tables/collection/core.rb +6 -1
  49. data/app/models/concerns/katalyst/tables/collection/pagination.rb +8 -1
  50. data/app/models/concerns/katalyst/tables/collection/sorting.rb +85 -17
  51. data/app/models/katalyst/tables/collection/array.rb +38 -0
  52. data/app/models/katalyst/tables/collection/base.rb +4 -0
  53. data/lib/katalyst/tables/config.rb +23 -0
  54. data/lib/katalyst/tables.rb +9 -0
  55. metadata +32 -15
  56. data/app/components/concerns/katalyst/tables/configurable_component.rb +0 -52
  57. data/app/components/concerns/katalyst/tables/turbo_replaceable.rb +0 -79
  58. data/app/components/katalyst/tables/body_cell_component.rb +0 -47
  59. data/app/components/katalyst/tables/header_cell_component.rb +0 -65
  60. data/app/components/katalyst/turbo/pagy_nav_component.rb +0 -23
  61. data/app/components/katalyst/turbo/table_component.rb +0 -45
  62. data/app/helpers/katalyst/tables/frontend/helper.rb +0 -31
  63. data/app/javascript/tables/turbo/collection_controller.js +0 -38
  64. data/app/models/katalyst/tables/collection/sort_form.rb +0 -102
@@ -10,82 +10,26 @@ module Katalyst
10
10
  FORM_CONTROLLER = "tables--selection--form"
11
11
  ITEM_CONTROLLER = "tables--selection--item"
12
12
 
13
- using Katalyst::HtmlAttributes::HasHtmlAttributes
14
-
15
- # Support for inclusion in a table component class
16
- # Adds an `selectable` slot and component configuration
17
- included do
18
- # Add `selectable` slot to table component
19
- config_component :selection, default: "Katalyst::Tables::Selectable::FormComponent"
20
- renders_one(:selection, lambda do |**attrs|
21
- selection_component.new(table: self, **attrs)
22
- end)
23
- end
24
-
25
- # Support for extending a table component instance
26
- # Adds methods to the table component instance
27
- def self.extended(table)
28
- table.extend(TableMethods)
29
-
30
- # ensure row components support selectable column calls
31
- table.send(:add_selectable_columns)
32
- end
33
-
34
- def initialize(**attributes)
35
- super
36
-
37
- # ensure row components support selectable column calls
38
- add_selectable_columns
39
- end
40
-
41
- private
42
-
43
- # Add `selectable` columns to row components
44
- def add_selectable_columns
45
- header_row_component.include(HeaderRow)
46
- body_row_component.include(BodyRow)
47
- end
48
-
49
- # Methods required to emulate a slot when extending an existing table.
50
- module TableMethods
51
- def with_selection(**attrs)
52
- @selection = FormComponent.new(table: self, **attrs)
53
-
54
- self
55
- end
56
-
57
- def selectable?
58
- @selection.present?
59
- end
60
-
61
- def selection
62
- @selection ||= FormComponent.new(table: self)
63
- end
64
- end
65
-
66
- module HeaderRow # :nodoc:
67
- def selection
68
- cell(:_selection, class: "selection", label: "")
69
- end
13
+ # Returns the default dom id for the selection form, uses the table's
14
+ # default id with '_selection' appended.
15
+ def self.default_form_id(collection)
16
+ "#{Identifiable::Defaults.default_table_id(collection)}_selection_form"
70
17
  end
71
18
 
72
- module BodyRow # :nodoc:
73
- def selection
74
- id = @record.public_send(@table.selection.primary_key)
75
- params = {
76
- id:,
77
- }
78
- cell(:_selection,
79
- class: "selection",
80
- data: {
81
- controller: ITEM_CONTROLLER,
82
- "#{ITEM_CONTROLLER}-params-value" => params.to_json,
83
- "#{ITEM_CONTROLLER}-#{FORM_CONTROLLER}-outlet" => "##{@table.selection.id}",
84
- action: "change->#{ITEM_CONTROLLER}#change",
85
- }) do
86
- tag.input(type: :checkbox)
87
- end
88
- end
19
+ # Adds the selection column to the table
20
+ #
21
+ # @param params [Hash] params to pass to the controller for selected rows
22
+ # @param form_id [String] id of the form element that will submit the selected row params
23
+ # @param ** [Hash] HTML attributes to be added to column cells
24
+ # @param & [Proc] optional block to alter the cell content
25
+ # @return [void]
26
+ #
27
+ # @example Render a select column
28
+ # <% row.select %> # => <td><input type="checkbox" ...></td>
29
+ def select(params: { id: record&.id }, form_id: Selectable.default_form_id(collection), **, &)
30
+ with_cell(Cells::SelectComponent.new(
31
+ collection:, row:, column: :_select, record:, label: "", heading: false, params:, form_id:, **,
32
+ ), &)
89
33
  end
90
34
  end
91
35
  end
@@ -7,25 +7,59 @@ module Katalyst
7
7
  module Sortable
8
8
  extend ActiveSupport::Concern
9
9
 
10
- # Returns true when the given attribute is sortable.
11
- def sortable?(attribute)
12
- sorting&.supports?(collection, attribute)
10
+ def initialize(**)
11
+ super(**)
12
+
13
+ @header_row_cell_callbacks << method(:add_sorting_to_cell) if collection.sortable?
14
+ end
15
+
16
+ private
17
+
18
+ def add_sorting_to_cell(cell)
19
+ if collection.sortable?(cell.column)
20
+ cell.update_html_attributes(data: { sort: collection.sort_status(cell.column) })
21
+ cell.with_content_wrapper(SortableHeaderComponent.new(collection:, cell:))
22
+ end
13
23
  end
14
24
 
15
- # Generates a url for applying/toggling sort for the given column.
16
- def sort_url(attribute) # rubocop:disable Metrics/AbcSize
17
- # Implementation inspired by pagy's `pagy_url_for` helper.
18
- # Preserve any existing GET parameters
19
- # CAUTION: these parameters are not sanitised
20
- sort = attribute && sorting.toggle(attribute)
21
- params = if sort && !sort.eql?(sorting.default)
22
- request.GET.merge("sort" => sort).except("page")
23
- else
24
- request.GET.except("page", "sort")
25
- end
26
- query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
27
-
28
- "#{request.path}#{query_string}"
25
+ class SortableHeaderComponent < ViewComponent::Base
26
+ include Katalyst::HtmlAttributes
27
+
28
+ attr_reader :collection, :cell
29
+
30
+ delegate :column, to: :cell
31
+
32
+ def initialize(collection:, cell:, **)
33
+ super(**)
34
+
35
+ @collection = collection
36
+ @cell = cell
37
+ end
38
+
39
+ def call
40
+ link_to(content, sort_url, **html_attributes)
41
+ end
42
+
43
+ # Generates a url for applying/toggling sort for the given column.
44
+ def sort_url
45
+ # rubocop:disable Metrics/AbcSize
46
+ # Implementation inspired by pagy's `pagy_url_for` helper.
47
+ # Preserve any existing GET parameters
48
+ # CAUTION: these parameters are not sanitised
49
+ sort = column && collection.toggle_sort(column)
50
+ params = if sort && !sort.eql?(collection.default_sort)
51
+ request.GET.merge("sort" => sort).except("page")
52
+ else
53
+ request.GET.except("page", "sort")
54
+ end
55
+ query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
56
+
57
+ "#{request.path}#{query_string}"
58
+ end
59
+
60
+ def default_html_attributes
61
+ { data: { turbo_action: "replace" } }
62
+ end
29
63
  end
30
64
  end
31
65
  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,91 +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
- # - `object_name`: the name of the object to use for partial rendering (defaults to collection.model_name.i18n_key)
33
- # - `partial`: the name of the partial to use for rendering each row (defaults to to_partial_path on the object)
34
- # - `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
+ #
35
47
  # In addition to these options, standard HTML attributes can be passed which will be added to the table tag.
36
48
  def initialize(collection:,
37
- sorting: nil,
38
49
  header: true,
39
- caption: false,
40
- **html_attributes)
41
- @collection = collection
42
-
43
- # sorting: instance of Katalyst::Tables::Collection::SortForm.
44
- # If not provided will be inferred from the collection.
45
- @sorting = sorting || html_attributes.delete(:sort) # backwards compatibility with old `sort` option
50
+ caption: true,
51
+ generate_ids: false,
52
+ object_name: nil,
53
+ partial: nil,
54
+ as: nil,
55
+ **)
56
+ @collection = normalize_collection(collection)
46
57
 
47
58
  # header: true means render the header row, header: false means no header row, if a hash, passes as options
48
- @header = header
49
- @header_options = (header if header.is_a?(Hash)) || {}
59
+ @header_options = header
50
60
 
51
61
  # caption: true means render the caption, caption: false means no caption, if a hash, passes as options
52
- @caption = caption
53
- @caption_options = (caption if caption.is_a?(Hash)) || {}
62
+ @caption_options = caption
63
+
64
+ @header_row_callbacks = []
65
+ @body_row_callbacks = []
66
+ @header_row_cell_callbacks = []
67
+ @body_row_cell_callbacks = []
54
68
 
55
- super(**html_attributes)
69
+ super(generate_ids:, object_name:, partial:, as:, **)
56
70
  end
57
71
 
58
- def caption?
59
- @caption.present?
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
60
94
  end
61
95
 
62
- def caption
63
- caption_component&.new(self)
96
+ def inspect
97
+ "#<#{self.class.name} collection: #{collection.inspect}>"
64
98
  end
65
99
 
66
- def header?
67
- @header.present?
100
+ delegate :header?, :body?, to: :@current_row
101
+
102
+ def row
103
+ @current_row
68
104
  end
69
105
 
70
- def header_row
71
- header_row_component.new(self, **@header_options)
106
+ def record
107
+ @current_record
72
108
  end
73
109
 
74
- def body_row(record)
75
- body_row_component.new(self, record)
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
76
120
  end
77
121
 
78
- def sorting
79
- return @sorting if @sorting.present?
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
+ ), &)
141
+ end
142
+ alias cell text
80
143
 
81
- collection.sorting if collection.respond_to?(:sorting)
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
+ ), &)
82
163
  end
83
164
 
84
- def inspect
85
- "#<#{self.class.name} collection: #{collection.inspect}>"
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
+ ), &)
86
186
  end
87
187
 
88
- define_html_attribute_methods(:thead_attributes)
89
- define_html_attribute_methods(:tbody_attributes)
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
+ ), &)
210
+ end
90
211
 
91
- # Backwards compatibility with tables 1.0
92
- alias_method :options, :html_attributes=
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
+ ), &)
231
+ end
232
+
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
312
+
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
93
323
  end
94
324
  end
@@ -0,0 +1,5 @@
1
+ <%= tag.tr(**html_attributes) do %>
2
+ <% cells.each do |cell| %>
3
+ <%= cell %>
4
+ <% end %>
5
+ <% end %>
@@ -5,27 +5,10 @@ module Katalyst
5
5
  class BodyRowComponent < ViewComponent::Base # :nodoc:
6
6
  include Katalyst::HtmlAttributes
7
7
 
8
- renders_many :columns, ->(component) { component }
8
+ renders_many :cells, ->(cell) { cell }
9
9
 
10
- def initialize(table, record)
11
- super()
12
-
13
- @table = table
14
- @record = record
15
- end
16
-
17
- def call
18
- content # generate content before rendering
19
-
20
- tag.tr(**html_attributes) do
21
- columns.each do |column|
22
- concat(column.to_s)
23
- end
24
- end
25
- end
26
-
27
- def cell(attribute, **, &)
28
- 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
29
12
  end
30
13
 
31
14
  def header?
@@ -37,11 +20,8 @@ module Katalyst
37
20
  end
38
21
 
39
22
  def inspect
40
- "#<#{self.class.name} record: #{record.inspect}>"
23
+ "#<#{self.class.name}>"
41
24
  end
42
-
43
- # Backwards compatibility with tables 1.0
44
- alias_method :options, :html_attributes=
45
25
  end
46
26
  end
47
27
  end