katalyst-tables 2.6.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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