katalyst-tables 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +151 -103
  4. data/app/assets/config/katalyst-tables.js +1 -0
  5. data/app/assets/javascripts/controllers/tables/turbo_collection_controller.js +22 -0
  6. data/app/components/concerns/katalyst/tables/configurable_component.rb +31 -0
  7. data/app/components/concerns/katalyst/tables/has_html_attributes.rb +45 -0
  8. data/app/components/concerns/katalyst/tables/has_table_content.rb +33 -0
  9. data/app/components/concerns/katalyst/tables/sortable.rb +32 -0
  10. data/app/components/concerns/katalyst/tables/turbo_replaceable.rb +62 -0
  11. data/app/components/katalyst/table_component.rb +51 -40
  12. data/app/components/katalyst/tables/body_cell_component.rb +4 -4
  13. data/app/components/katalyst/tables/body_row_component.rb +3 -3
  14. data/app/components/katalyst/tables/empty_caption_component.html.erb +6 -0
  15. data/app/components/katalyst/tables/empty_caption_component.rb +38 -0
  16. data/app/components/katalyst/tables/header_cell_component.rb +20 -16
  17. data/app/components/katalyst/tables/header_row_component.rb +6 -5
  18. data/app/components/katalyst/tables/pagy_nav_component.rb +26 -0
  19. data/app/components/katalyst/turbo/pagy_nav_component.rb +23 -0
  20. data/app/components/katalyst/turbo/table_component.rb +48 -0
  21. data/app/models/concerns/katalyst/tables/collection/core.rb +71 -0
  22. data/app/models/concerns/katalyst/tables/collection/pagination.rb +66 -0
  23. data/app/models/concerns/katalyst/tables/collection/sorting.rb +63 -0
  24. data/app/models/katalyst/tables/collection.rb +32 -0
  25. data/config/importmap.rb +7 -0
  26. data/lib/katalyst/tables/backend/sort_form.rb +16 -2
  27. data/lib/katalyst/tables/engine.rb +13 -0
  28. data/lib/katalyst/tables/frontend/helper.rb +4 -14
  29. data/lib/katalyst/tables/version.rb +1 -1
  30. metadata +33 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c4a55f7a1b45c9f70f357eaa670debf6457aca92a8ac7ea8b10df20140d1c23
4
- data.tar.gz: 620cff7771f9de2d74c613d6b107136ea34c494862d2da4e612ccb55d8dee51d
3
+ metadata.gz: 2a572503d453b3b8b98a82942a48c5e2772cec4023417e8db71ed2cd8299331a
4
+ data.tar.gz: 4761c4fa4b21aa8c91f70c2118e27bd68dfd700ffc419ed6f7eae299b582710f
5
5
  SHA512:
6
- metadata.gz: 15cc508fdbb3ad40baec51314fde01c3889b670d4ccd6366ea49171a45b979d441a4fb3fafe65e5bd07093375615b6d298ebe091f240122edf9d68b10471f251
7
- data.tar.gz: e5f4186c070c81c4645bc3cf8a378927f502df6084d235c38416d98eb7e4ab45c775da2d22338d32bf5063774a575b4c7a92a556468f1b35d18c809db421922c
6
+ metadata.gz: aa9c1dc28479dc744c18804999d5f34ea9937e1500dbe8c01a1f2c183439077dfdd7047dbb8ce2ea0f93ef43bcfcea15eee6d08b2606f3682a00747e590cbc3b
7
+ data.tar.gz: 45c36c312e7f6efa8b11409d4822d135c961eba6ade51055b4671371b3b1a6e25a01c9fd1e059c0a89fa5da6bea3e7fe56fda267a1b3d7bfdf51e515b7b8a696
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.1.0]
4
+
5
+ - Add Collection model for building collections in a controller from params.
6
+ - See [[README.md]] for examples
7
+ - Add turbo entry points for table and pagy_nav
8
+ - See [[README.md]] for examples
9
+ - Add support for row partials when content is not provided
10
+ - See [[README.md]] for examples
11
+ - Add messages when table is empty, off by default (caption: true)
12
+ - Add PagyNavComponent for rendering `pagy_nav` from a collection.
13
+ - Replaces internal references to SortForm to use `sorting` instead
14
+ - No changes required to existing code unless you were using the internal
15
+ classes directly
16
+ - Change allows sort param and sorting model to co-exist
17
+
3
18
  ## [2.0.0]
4
19
 
5
20
  - Replaces builders with view_components
data/README.md CHANGED
@@ -16,76 +16,99 @@ And then execute:
16
16
 
17
17
  ## Usage
18
18
 
19
- This gem provides two entry points: `Frontend` for use in your views, and `Backend` for use in your controllers. The backend
20
- entry point is optional, as it's only required if you want to support sorting by column headers.
19
+ This gem provides entry points for backend and frontend concerns:
20
+ * `Katalyst::TableComponent` can be used render encapsulated tables, it calls a
21
+ partial for each row.
22
+ * `Katalyst::Tables::Frontend` provides `table_with` for inline table generation
23
+ * `Katalyst::Tables::Collection::Base` provides a default entry point for
24
+ building collections in your controller actions.
21
25
 
22
- ### Frontend
26
+ ## Frontend
23
27
 
24
- Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
28
+ Use `Katalyst::TableComponent` to build a table component from an ActiveRecord
29
+ collection, or from a `Katalyst::Tables::Collection::Base` instance.
30
+
31
+ For example, if you render `Katalyst::TableComponent.new(collection: @people)`,
32
+ the table component will look for a partial called `_person.html+row.erb` and
33
+ render it for each row (and once for the header row).
25
34
 
26
35
  ```erb
27
- <%= table_with collection: @people do |row, person| %>
28
- <%= row.cell :name %>
29
- <%= row.cell :email %>
30
- <%= row.cell :actions do %>
31
- <%= link_to "Edit", person %>
32
- <% end %>
36
+ <%# locals: { row:, person: nil } %>
37
+ <% row.cell :name do |cell| %>
38
+ <%= link_to cell.value, [:edit, person] %>
33
39
  <% end %>
40
+ <% row.cell :email %>
34
41
  ```
35
42
 
36
- The table builder will call your block once per row and accumulate the cells you generate into rows:
43
+ The table component will call your partial once per row and accumulate the cells
44
+ you generate into rows, including a header row:
37
45
 
38
46
  ```html
39
47
 
40
48
  <table>
41
- <thead>
42
- <tr>
43
- <th>Name</th>
44
- <th>Email</th>
45
- <th>Actions</th>
46
- </tr>
47
- </thead>
48
- <tbody>
49
- <tr>
50
- <td>Alice</td>
51
- <td>alice@acme.org</td>
52
- <td><a href="/people/1/edit">Edit</a></td>
53
- </tr>
54
- <tr>
55
- <td>Bob</td>
56
- <td>bob@acme.org</td>
57
- <td><a href="/people/2/edit">Edit</a></td>
58
- </tr>
59
- </tbody>
49
+ <thead>
50
+ <tr>
51
+ <th>Name</th>
52
+ <th>Email</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ <tr>
57
+ <td><a href="/people/1/edit">Alice</a></td>
58
+ <td>alice@acme.org</td>
59
+ </tr>
60
+ <tr>
61
+ <td><a href="/people/2/edit">Bob</a></td>
62
+ <td>bob@acme.org</td>
63
+ </tr>
64
+ </tbody>
60
65
  </table>
61
66
  ```
62
67
 
63
- ### Options
68
+ You can customize the partial and/or the name of the resource in a similar style
69
+ to view partials:
64
70
 
65
- You can customise the options passed to the table, rows, and cells.
71
+ ```erb
72
+ <%= render Katalyst::TableComponent.new(collection: @employees, as: :person, partial: "person") %>
73
+ ```
74
+
75
+ ### Inline tables
66
76
 
67
- Tables support options via the call to `table_with`, similar to `form_with`.
77
+ You can use the `table_with` helper to generate a table inline in your view without explicitly interacting with the
78
+ table component. This is primarily intended for backwards compatibility, but it can be useful for simple tables.
79
+
80
+ Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
68
81
 
69
82
  ```erb
70
- <%= table_with collection: @people, id: "people-table" do |row, person| %>
71
- ...
83
+ <%= table_with collection: @people do |row, person| %>
84
+ <% row.cell :name do |cell| %>
85
+ <%= link_to cell.value, [:edit, person] %>
86
+ <% end %>
87
+ <% row.cell :email %>
72
88
  <% end %>
73
89
  ```
74
90
 
91
+ ### HTML Attributes
92
+
93
+ You can add custom attributes on table, row, and cell tags.
94
+
95
+ The table tag takes attributes passed to `TableComponent` or via the call to `table_with`, similar to `form_with`:
96
+
97
+ ```erb
98
+ <%= TableComponent.new(collection: @people, id: "people-table")
99
+ ```
100
+
75
101
  Cells support the same approach:
76
102
 
77
103
  ```erb
78
104
  <%= row.cell :name, class: "name" %>
79
105
  ```
80
106
 
81
- Rows do not get called directly, so instead you can call `options` on the row builder to customize the row tag
107
+ Rows do not get called directly, so instead you can assign to `html_attributes` on the row builder to customize row tag
82
108
  generation.
83
109
 
84
110
  ```erb
85
- <%= table_with collection: @people, id: "people-table" do |row, person| %>
86
- <% row.options data: { id: person.id } if row.body? %>
87
- ...
88
- <% end %>
111
+ <% row.html_attributes = { id: person.id } if row.body? %>
89
112
  ```
90
113
 
91
114
  Note: because the row builder gets called to generate the header row, you may need to guard calls that access the
@@ -93,8 +116,8 @@ Note: because the row builder gets called to generate the header row, you may ne
93
116
 
94
117
  #### Headers
95
118
 
96
- `table_builder` will automatically generate a header row for you by calling your block with no object. During this
97
- iteration, `row.header?` is true, `row.body?` is false, and the object (`person`) is nil.
119
+ Tables will automatically generate a header row for you by calling your row partial or provided block with no object.
120
+ During this call, `row.header?` is true, `row.body?` is false, and the object (`person`) is nil.
98
121
 
99
122
  All cells generated in the table header iteration will automatically be header cells, but you can also make header cells
100
123
  in your body rows by passing `heading: true` when you generate the cell.
@@ -130,8 +153,8 @@ the table cell. This is often all you need to do, but if you do want to customis
130
153
  the value you can pass a block instead:
131
154
 
132
155
  ```erb
133
- <%= row.cell :status do %>
134
- <%= person.password.present? ? "Active" : "Invited" %>
156
+ <% row.cell :status do %>
157
+ <%= person.password.present? ? "Active" : "Invited" %>
135
158
  <% end %>
136
159
  ```
137
160
 
@@ -139,20 +162,61 @@ In the context of the block you have access the cell builder if you simply
139
162
  want to extend the default behaviour:
140
163
 
141
164
  ```erb
142
- <%= row.cell :status do |cell| %>
143
- <%= link_to cell.value, person %>
165
+ <% row.cell :status do |cell| %>
166
+ <%= link_to cell.value, person %>
144
167
  <% end %>
145
168
  ```
146
169
 
147
- You can also call `options` on the cell builder, similar to the row builder, but
148
- please note that this will replace any options passed to the cell as arguments.
170
+ You can also assign to `html_attributes` on the cell builder, similar to the row
171
+ builder, but please note that this will replace any options passed to the cell
172
+ as arguments.
149
173
 
150
- ### Sort
174
+ ## Collections
151
175
 
152
- The major reason why you should use this gem, apart the convenience of the
153
- builder, is for adding efficient and simple column sorting to your tables.
176
+ The `Katalyst::Tables::Collection::Base` class provides a convenient way to
177
+ manage collections in your controller actions. It is designed to be used with
178
+ Pagy for pagination and provides built-in sorting when used with ActiveRecord
179
+ collections. Sorting and Pagination are off by default, but you can create
180
+ a custom `ApplicationCollection` class that sets them on by default.
181
+
182
+ ```ruby
183
+ class ApplicationCollection < Katalyst::Tables::Collection::Base
184
+ config.sorting = "name" # requires models have a name attribute
185
+ config.pagination = true
186
+ end
187
+ ```
154
188
 
155
- Start by including the backend in your controller(s):
189
+ You can then use this class in your controller actions:
190
+
191
+ ```ruby
192
+ class PeopleController < ApplicationController
193
+ def index
194
+ @people = ApplicationCollection.new.with_params(params).apply(People.all)
195
+ end
196
+ end
197
+ ```
198
+
199
+ Collections can be passed directly to `TableComponent` and it will automatically
200
+ detect features such as sorting and generate the appropriate table header links.
201
+
202
+ ```erb
203
+ <%= render TableComponent.new(collection: @people) %>
204
+ ```
205
+
206
+ ## Sort
207
+
208
+ When sort is enabled, table columns will be automatically sortable in the
209
+ frontend for any column that corresponds to an attribute on the model. You can
210
+ also add sorting to non-attribute columns by defining a scope in your
211
+ model:
212
+
213
+ ```
214
+ scope :order_by_status, ->(direction) { ... }
215
+ ```
216
+
217
+ You can also use sort without using collections, this was the primary backend
218
+ interface for V1 and takes design cues from Pagy. Start by including the backend
219
+ in your controller(s):
156
220
 
157
221
  ```ruby
158
222
  include Katalyst::Tables::Backend
@@ -183,55 +247,47 @@ links and show the current sort state:
183
247
  <%= table_with collection: @people, sort: @sort do |row, person| %>
184
248
  <%= row.cell :name %>
185
249
  <%= row.cell :email %>
186
- <%= row.cell :actions do %>
187
- <%= link_to "Edit", person %>
188
- <% end %>
189
250
  <% end %>
190
251
  ```
191
252
 
192
- That's it! Any column that corresponds to an ActiveRecord attribute will now be
193
- automatically sortable in the frontend.
253
+ ## Pagination
194
254
 
195
- You can also add sorting to non-attribute columns by defining a scope in your
196
- model:
255
+ This gem designed to work with [pagy](https://github.com/ddnexus/pagy/).
197
256
 
198
- ```
199
- scope :order_by_status, ->(direction) { ... }
200
- ```
257
+ If you use collections and enable pagination then pagy will be called internally
258
+ and the pagy metadata will be available as `pagination` on the collection.
201
259
 
202
- Finally, you can use sort with a collection that is already ordered, but please
203
- note that the backend will call `reorder` if the user provides a sort option. If
204
- you want to provide a tie-breaker default ordering, the best way to do so is after
205
- calling `table_sort`.
260
+ `Katalyst::Tables::PagyNavComponent` can be used to render the pagination links
261
+ for a collection.
206
262
 
207
- You may also want to whitelist the `sort` param if you encounter strong param warnings.
263
+ ```erb
264
+ <%= render Katalyst::Tables::PagyNavComponent.new(collection: @people) %>
265
+ ```
208
266
 
209
- ### Pagination
267
+ ## Turbo streams
210
268
 
211
- This gem designed to work with [pagy](https://github.com/ddnexus/pagy/).
269
+ This gem provides turbo stream entry points for table and pagy_nav. These are
270
+ identical in the options they support, but they require ids, and they will
271
+ automatically render turbo stream replace tags when rendered as part of a turbo
272
+ stream response.
212
273
 
213
- ```ruby
274
+ To take full advantage of this feature, we suggest you build the component in
275
+ your controller and pass it to the view. This allows you to use the same
276
+ controller for both HTML and turbo responses.
214
277
 
278
+ ```ruby
215
279
  def index
216
- @people = People.all
217
-
218
- @sort, @people = table_sort(@people) # sort
219
- @pagy, @people = pagy(@people) # then paginate
280
+ collection = ApplicationCollection.new.with_params(params).apply(People.all)
281
+ table = Katalyst::Turbo::TableComponent.new(collection:, id: "people")
282
+
283
+ respond_to do |format|
284
+ format.html { render locals: { table: table } }
285
+ format.turbo_stream { render table }
286
+ end
220
287
  end
221
288
  ```
222
289
 
223
- ```erb
224
- <%= table_with collection: @people, sort: @sort do |row, person| %>
225
- <%= row.cell :name %>
226
- <%= row.cell :email %>
227
- <%= row.cell :actions do %>
228
- <%= link_to "Edit", person %>
229
- <% end %>
230
- <% end %>
231
- <%== pagy_nav(@pagy) %>
232
- ```
233
-
234
- ### Customization
290
+ ## Customization
235
291
 
236
292
  A common pattern we use is to have a cell at the end of the table for actions. For example:
237
293
 
@@ -255,11 +311,12 @@ A common pattern we use is to have a cell at the end of the table for actions. F
255
311
  </table>
256
312
  ```
257
313
 
258
- You can write a custom builder that helps generate this type of table by adding the required classes and adding helpers
259
- for generating the actions. This allows for a declarative table syntax, something like this:
314
+ You can write a custom component that helps generate this type of table by
315
+ adding the required classes and adding helpers for generating the actions.
316
+ This allows for a declarative table syntax, something like this:
260
317
 
261
318
  ```erb
262
- <%= table_with(collection: collection, component: ActionTableComponent) do |row| %>
319
+ <%= render ActionTableComponent.new(collection:) do |row| %>
263
320
  <% row.cell :name %>
264
321
  <% row.actions do |cell| %>
265
322
  <%= cell.action "Edit", :edit %>
@@ -276,10 +333,9 @@ class ActionTableComponent < Katalyst::TableComponent
276
333
  config.header_row = "ActionHeaderRow"
277
334
  config.body_row = "ActionBodyRow"
278
335
  config.body_cell = "ActionBodyCell"
279
-
280
- def call
281
- options(class: "action-table")
282
- super
336
+
337
+ def default_attributes
338
+ { class: "action-table" }
283
339
  end
284
340
 
285
341
  class ActionHeaderRow < Katalyst::Tables::HeaderRowComponent
@@ -295,21 +351,13 @@ class ActionTableComponent < Katalyst::TableComponent
295
351
  end
296
352
 
297
353
  class ActionBodyCell < Katalyst::Tables::BodyCellComponent
298
- def action(label, href, **opts)
299
- content_tag :a, label, { href: href }.merge(opts)
354
+ def action(label, href, **attrs)
355
+ content_tag(:a, label, href: href, **attrs)
300
356
  end
301
357
  end
302
358
  end
303
359
  ```
304
360
 
305
- If you have a table component you want to reuse, you can set it as a default for some or all of your controllers:
306
-
307
- ```html
308
- class ApplicationController < ActiveController::Base
309
- default_table_component ActionTableComponent
310
- end
311
- ```
312
-
313
361
  ## Development
314
362
 
315
363
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
@@ -0,0 +1 @@
1
+ //= link_tree ../javascripts
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class TurboCollectionController extends Controller {
4
+ static values = {
5
+ url: String,
6
+ sort: String,
7
+ }
8
+
9
+ urlValueChanged(url) {
10
+ window.history.replaceState({}, "", this.urlValue);
11
+ }
12
+
13
+ sortValueChanged(sort) {
14
+ document.querySelectorAll(this.#sortSelector).forEach((input) => {
15
+ if (input) input.value = sort;
16
+ });
17
+ }
18
+
19
+ get #sortSelector() {
20
+ return "input[name='sort']";
21
+ }
22
+ }
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module ConfigurableComponent # :nodoc:
6
+ extend ActiveSupport::Concern
7
+
8
+ include ActiveSupport::Configurable
9
+
10
+ included do
11
+ # Workaround: ViewComponent::Base.config is incompatible with ActiveSupport::Configurable
12
+ @_config = Class.new(ActiveSupport::Configurable::Configuration).new
13
+ end
14
+
15
+ class_methods do
16
+ # Define a configurable sub-component.
17
+ def config_component(name, component_name: "#{name}_component", default: nil)
18
+ config_accessor(name)
19
+ config.public_send("#{name}=", default)
20
+ define_method(component_name) do
21
+ instance_variable_get("@#{component_name}") if instance_variable_defined?("@#{component_name}")
22
+
23
+ klass = config.public_send(name)
24
+ component = self.class.const_get(klass) if klass
25
+ instance_variable_set("@#{component_name}", component) if component
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "html_attributes_utils"
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module HasHtmlAttributes # :nodoc:
8
+ extend ActiveSupport::Concern
9
+
10
+ using HTMLAttributesUtils
11
+
12
+ DEFAULT_MERGEABLE_ATTRIBUTES = [
13
+ *HTMLAttributesUtils::DEFAULT_MERGEABLE_ATTRIBUTES,
14
+ %i[data controller],
15
+ %i[data action]
16
+ ].freeze
17
+
18
+ def initialize(**options)
19
+ super(**options.except(:id, :aria, :class, :data, :html))
20
+
21
+ self.html_attributes = options
22
+ end
23
+
24
+ # Add HTML options to the current component.
25
+ # Public method for customizing components from within
26
+ def html_attributes=(options)
27
+ @html_attributes = options.slice(:id, :aria, :class, :data).merge(options.fetch(:html, {}))
28
+ end
29
+
30
+ # Backwards compatibility with tables 1.0
31
+ alias options html_attributes=
32
+
33
+ private
34
+
35
+ def html_attributes
36
+ default_attributes
37
+ .deep_merge_html_attributes(@html_attributes, mergeable_attributes: DEFAULT_MERGEABLE_ATTRIBUTES)
38
+ end
39
+
40
+ def default_attributes
41
+ {}
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module HasTableContent # :nodoc:
6
+ extend ActiveSupport::Concern
7
+
8
+ def initialize(object_name: nil, partial: nil, as: nil, **options)
9
+ super(**options)
10
+
11
+ @object_name = object_name || model_name&.i18n_key
12
+ @partial = partial
13
+ @as = as
14
+ end
15
+
16
+ def model_name
17
+ collection.model_name if collection.respond_to?(:model_name)
18
+ end
19
+
20
+ private
21
+
22
+ def row_proc
23
+ @row_proc ||= @__vc_render_in_block || method(:row_partial)
24
+ end
25
+
26
+ def row_partial(row, record = nil)
27
+ partial = @partial || model_name&.param_key&.to_s
28
+ as = @as || model_name&.param_key&.to_sym
29
+ render(partial: partial, variants: [:row], locals: { as => record, row: row })
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ # Extension to add sorting support to a collection.
6
+ # Assumes collection and sorting are available in the current scope.
7
+ module Sortable
8
+ extend ActiveSupport::Concern
9
+
10
+ # Returns true when the given attribute is sortable.
11
+ def sortable?(attribute)
12
+ sorting&.supports?(collection, attribute)
13
+ end
14
+
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}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ # Adds support for turbo stream replacement to ViewComponents. Components
6
+ # that are rendered from a turbo-stream-compatible response will be rendered
7
+ # using turbo stream replacement. Components must define `id`.
8
+ #
9
+ # Turbo stream replacement rendering will only be enabled if the component
10
+ # passes `turbo: true` as a constructor option.
11
+ module TurboReplaceable
12
+ extend ActiveSupport::Concern
13
+
14
+ include ::Turbo::StreamsHelper
15
+
16
+ def turbo?
17
+ @turbo
18
+ end
19
+
20
+ def initialize(turbo: true, **options)
21
+ super(**options)
22
+
23
+ @turbo = turbo
24
+ end
25
+
26
+ class_methods do
27
+ # Redefine the compiler to use our custom compiler.
28
+ # Compiler is set on `inherited` so we need to re-set it if it's not the expected type.
29
+ def compiler
30
+ @vc_compiler = @vc_compiler.is_a?(TurboCompiler) ? @vc_compiler : TurboCompiler.new(self)
31
+ end
32
+ end
33
+
34
+ included do
35
+ # ensure that our custom compiler is used, as `inherited` calls `compile` before our module is included.
36
+ compile(force: true) if compiled?
37
+ end
38
+
39
+ # Wraps the default compiler provided by ViewComponent to add turbo support.
40
+ class TurboCompiler < ViewComponent::Compiler
41
+ private
42
+
43
+ def define_render_template_for # rubocop:disable Metrics/MethodLength
44
+ super
45
+
46
+ redefinition_lock.synchronize do
47
+ component_class.alias_method(:vc_render_template_for, :render_template_for)
48
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
49
+ def render_template_for(variant = nil)
50
+ return vc_render_template_for(variant) unless turbo?
51
+ controller.respond_to do |format|
52
+ format.html { vc_render_template_for(variant) }
53
+ format.turbo_stream { turbo_stream.replace(id, vc_render_template_for(variant)) }
54
+ end
55
+ end
56
+ RUBY
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end