katalyst-tables 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +341 -0
- data/lib/katalyst/tables/backend/sort_form.rb +109 -0
- data/lib/katalyst/tables/backend.rb +52 -0
- data/lib/katalyst/tables/frontend/builder/base.rb +62 -0
- data/lib/katalyst/tables/frontend/builder/body_cell.rb +31 -0
- data/lib/katalyst/tables/frontend/builder/body_row.rb +29 -0
- data/lib/katalyst/tables/frontend/builder/header_cell.rb +55 -0
- data/lib/katalyst/tables/frontend/builder/header_row.rb +23 -0
- data/lib/katalyst/tables/frontend/helper.rb +16 -0
- data/lib/katalyst/tables/frontend/table_builder.rb +71 -0
- data/lib/katalyst/tables/frontend.rb +52 -0
- data/lib/katalyst/tables/version.rb +7 -0
- data/lib/katalyst/tables.rb +11 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fff2924a5584c83799ccce74292d2597083e81fd771d24022a517f38e89147ef
|
4
|
+
data.tar.gz: d0230ed5a686ef5f5cedeb54f96ff8a206955d718a66420940087550958f7997
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1655096befcbe0fcd8e11e3bf7240b6975ca3d6d1c3eef6e09d22b04aadd5697b37ebc124b9ebb0ddc7d1742e947a5d42c47f70f2e803bb9e2f04b54517cc99a
|
7
|
+
data.tar.gz: eca6c92288908ef026165bef43b8450187a3e101acfa94e67f8cf635125ade4567eab914bc1f44cae0afef80561efbd226302e9787a007e1bc5fd82e0fe6a5cf
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Katalyst Interactive
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
# Katalyst::Tables
|
2
|
+
|
3
|
+
Tools for building HTML tables from ActiveRecord collections.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "katalyst-tables", git: "https://github.com/katalyst/katalyst-tables", branch: "main"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
**Reminder:** If you have a rails server running, remember to restart the server to prevent the `uninitialized constant` error.
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
This gem provides two entry points: Frontend for use in your views, and Backend for use in your controllers. The backend
|
22
|
+
entry point is optional, as it's only required if you want to support sorting by column headers.
|
23
|
+
|
24
|
+
### Frontend
|
25
|
+
|
26
|
+
Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
|
27
|
+
|
28
|
+
```erb
|
29
|
+
<%= table_with collection: @people do |row, person| %>
|
30
|
+
<%= row.cell :name %>
|
31
|
+
<%= row.cell :email %>
|
32
|
+
<%= row.cell :actions do %>
|
33
|
+
<%= link_to "Edit", person %>
|
34
|
+
<% end %>
|
35
|
+
<% end %>
|
36
|
+
```
|
37
|
+
|
38
|
+
`table_builder` will call your block once per row and accumulate the cells you generate into rows:
|
39
|
+
|
40
|
+
```html
|
41
|
+
|
42
|
+
<table>
|
43
|
+
<thead>
|
44
|
+
<tr>
|
45
|
+
<th>Name</th>
|
46
|
+
<th>Email</th>
|
47
|
+
<th>Actions</th>
|
48
|
+
</tr>
|
49
|
+
</thead>
|
50
|
+
<tbody>
|
51
|
+
<tr>
|
52
|
+
<td>Alice</td>
|
53
|
+
<td>alice@acme.org</td>
|
54
|
+
<td><a href="/people/1/edit">Edit</a></td>
|
55
|
+
</tr>
|
56
|
+
<tr>
|
57
|
+
<td>Bob</td>
|
58
|
+
<td>bob@acme.org</td>
|
59
|
+
<td><a href="/people/2/edit">Edit</a></td>
|
60
|
+
</tr>
|
61
|
+
</tbody>
|
62
|
+
</table>
|
63
|
+
```
|
64
|
+
|
65
|
+
### Options
|
66
|
+
|
67
|
+
You can customise the options passed to the table, rows, and cells.
|
68
|
+
|
69
|
+
Tables support options via the call to `table_with`, similar to `form_with`.
|
70
|
+
|
71
|
+
```erb
|
72
|
+
<%= table_with collection: @people, id: "people-table" do |row, person| %>
|
73
|
+
...
|
74
|
+
<% end %>
|
75
|
+
```
|
76
|
+
|
77
|
+
Cells support the same approach:
|
78
|
+
|
79
|
+
```erb
|
80
|
+
<%= row.cell :name, class: "name" %>
|
81
|
+
```
|
82
|
+
|
83
|
+
Rows do not get called directly, so instead you can call `options` on the row builder to customize the row tag
|
84
|
+
generation.
|
85
|
+
|
86
|
+
```erb
|
87
|
+
<%= table_with collection: @people, id: "people-table" do |row, person| %>
|
88
|
+
<% row.options data: { id: person.id } if row.body? %>
|
89
|
+
...
|
90
|
+
<% end %>
|
91
|
+
```
|
92
|
+
|
93
|
+
Note: because the row builder gets called to generate the header row, you may need to guard calls that access the
|
94
|
+
`person` directly as shown in the previous example. You could also check whether `person` is present.
|
95
|
+
|
96
|
+
#### Headers
|
97
|
+
|
98
|
+
`table_builder` will automatically generate a header row for you by calling your block with no object. During this
|
99
|
+
iteration, `row.header?` is true, `row.body?` is false, and the object (`person`) is nil.
|
100
|
+
|
101
|
+
All cells generated in the table header iteration will automatically be header cells, but you can also make header cells
|
102
|
+
in your body rows by passing `heading: true` when you generate the cell.
|
103
|
+
|
104
|
+
```erb
|
105
|
+
<%= row.cell :id, heading: true %>
|
106
|
+
```
|
107
|
+
|
108
|
+
The table header cells default to showing the titleized column name, but you can customize this in one of two ways:
|
109
|
+
|
110
|
+
* Set the value inline
|
111
|
+
```erb
|
112
|
+
<%= row.cell :id, label: "ID" %>
|
113
|
+
```
|
114
|
+
* Define a translation for the attribute
|
115
|
+
```yml
|
116
|
+
# en.yml
|
117
|
+
activerecord:
|
118
|
+
attributes:
|
119
|
+
person:
|
120
|
+
id: "ID"
|
121
|
+
```
|
122
|
+
|
123
|
+
Note: if the cell is given a block, it is not called during the header pass. This
|
124
|
+
is because the block is assumed to be for generating data for the body, not the
|
125
|
+
header. We suggest you set `label` instead.
|
126
|
+
|
127
|
+
#### Cell values
|
128
|
+
|
129
|
+
If you do not provide a value when you call the cell builder, the attribute you
|
130
|
+
provide will be retrieved from the current item and the result will be rendered in
|
131
|
+
the table cell. This is often all you need to do, but if you do want to customise
|
132
|
+
the value you can pass a block instead:
|
133
|
+
|
134
|
+
```erb
|
135
|
+
<%= row.cell :status do %>
|
136
|
+
<%= person.password.present? ? "Active" : "Invited" %>
|
137
|
+
<% end %>
|
138
|
+
```
|
139
|
+
|
140
|
+
In the context of the block you have access the cell builder if you simply
|
141
|
+
want to extend the default behaviour:
|
142
|
+
|
143
|
+
```erb
|
144
|
+
<%= row.cell :status do |cell| %>
|
145
|
+
<%= link_to cell.value, person %>
|
146
|
+
<% end %>
|
147
|
+
```
|
148
|
+
|
149
|
+
You can also call `options` on the cell builder, similar to the row builder, but
|
150
|
+
please note that this will replace any options passed to the cell as arguments.
|
151
|
+
|
152
|
+
### Sort
|
153
|
+
|
154
|
+
The major reason why you should use this gem, apart the convenience of the
|
155
|
+
builder, is for adding efficient and simple column sorting to your tables.
|
156
|
+
|
157
|
+
Start by including the backend in your controller(s):
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
include Katalyst::Tables::Backend
|
161
|
+
```
|
162
|
+
|
163
|
+
Now, in your controller index actions, you can sort your active record
|
164
|
+
collections based on the `sort` param which is appended to the current URL as a
|
165
|
+
get parameter when a user clicks on a column header.
|
166
|
+
|
167
|
+
Building on our example from earlier:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
class PeopleController < ApplicationController
|
171
|
+
include Katalyst::Tables::Backend
|
172
|
+
|
173
|
+
def index
|
174
|
+
@people = People.all
|
175
|
+
|
176
|
+
@sort, @people = table_sort(@people) # sort
|
177
|
+
end
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
You then add the sort form object to your view so that it can add column header
|
182
|
+
links and show the current sort state:
|
183
|
+
|
184
|
+
```erb
|
185
|
+
<%= table_with collection: @people, sort: @sort do |row, person| %>
|
186
|
+
<%= row.cell :name %>
|
187
|
+
<%= row.cell :email %>
|
188
|
+
<%= row.cell :actions do %>
|
189
|
+
<%= link_to "Edit", person %>
|
190
|
+
<% end %>
|
191
|
+
<% end %>
|
192
|
+
```
|
193
|
+
|
194
|
+
That's it! Any column that corresponds to an ActiveRecord attribute will now be
|
195
|
+
automatically sortable in the frontend.
|
196
|
+
|
197
|
+
You can also add sorting to non-attribute columns by defining a scope in your
|
198
|
+
model:
|
199
|
+
|
200
|
+
```
|
201
|
+
scope :order_by_status, ->(direction) { ... }
|
202
|
+
```
|
203
|
+
|
204
|
+
Finally, you can use sort with a collection that is already ordered, but please
|
205
|
+
note that the backend will call `reorder` if the user provides a sort option. If
|
206
|
+
you want to provide a tie-breaker default ordering, the best way to do so is after
|
207
|
+
calling `table_sort`.
|
208
|
+
|
209
|
+
You may also want to whitelist the `sort` param if you encounter strong param warnings.
|
210
|
+
|
211
|
+
### Pagination
|
212
|
+
|
213
|
+
This gem designed to work with [pagy](https://github.com/ddnexus/pagy/).
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
|
217
|
+
def index
|
218
|
+
@people = People.all
|
219
|
+
|
220
|
+
@sort, @people = table_sort(@people) # sort
|
221
|
+
@pagy, @people = pagy(@people) # then paginate
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
```erb
|
226
|
+
<%= table_with collection: @people, sort: @sort do |row, person| %>
|
227
|
+
<%= row.cell :name %>
|
228
|
+
<%= row.cell :email %>
|
229
|
+
<%= row.cell :actions do %>
|
230
|
+
<%= link_to "Edit", person %>
|
231
|
+
<% end %>
|
232
|
+
<% end %>
|
233
|
+
<%== pagy_nav(@pagy) %>
|
234
|
+
```
|
235
|
+
|
236
|
+
### Customization
|
237
|
+
|
238
|
+
A common pattern we use is to have a cell at the end of the table for actions. For example:
|
239
|
+
|
240
|
+
```html
|
241
|
+
<table class="action-table">
|
242
|
+
<thead>
|
243
|
+
<tr>
|
244
|
+
<th>Name</th>
|
245
|
+
<th class="actions"></th>
|
246
|
+
</tr>
|
247
|
+
</thead>
|
248
|
+
<tbody>
|
249
|
+
<tr>
|
250
|
+
<td>Alice</td>
|
251
|
+
<td class="actions">
|
252
|
+
<a href="/people/1/edit">Edit</a>
|
253
|
+
<a href="/people/1" method="delete">Delete</a>
|
254
|
+
</td>
|
255
|
+
</tr>
|
256
|
+
</tbody>
|
257
|
+
</table>
|
258
|
+
```
|
259
|
+
|
260
|
+
You can write a custom builder that helps generate this type of table by adding the required classes and adding helpers
|
261
|
+
for generating the actions. This allows for a declarative table syntax, something like this:
|
262
|
+
|
263
|
+
```erb
|
264
|
+
<%= table_with(collection: collection, builder: Test::ActionTable) do |row| %>
|
265
|
+
<%= row.cell :name %>
|
266
|
+
<%= row.actions do |cell| %>
|
267
|
+
<%= cell.action "Edit", :edit %>
|
268
|
+
<%= cell.action "Delete", :delete, method: :delete %>
|
269
|
+
<% end %>
|
270
|
+
<% end %>
|
271
|
+
```
|
272
|
+
|
273
|
+
And the custom builder:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
class ActionTable < Katalyst::Tables::Frontend::TableBuilder
|
277
|
+
def build(&block)
|
278
|
+
(@html_options[:class] ||= []) << "action-table"
|
279
|
+
super
|
280
|
+
end
|
281
|
+
|
282
|
+
def table_header_row(builder = ActionHeaderRow, &block)
|
283
|
+
super
|
284
|
+
end
|
285
|
+
|
286
|
+
def table_header_cell(method, builder = ActionHeaderCell, **options)
|
287
|
+
super
|
288
|
+
end
|
289
|
+
|
290
|
+
def table_body_row(object, builder = ActionBodyRow, &block)
|
291
|
+
super
|
292
|
+
end
|
293
|
+
|
294
|
+
def table_body_cell(object, method, builder = ActionBodyCell, **options, &block)
|
295
|
+
super
|
296
|
+
end
|
297
|
+
|
298
|
+
class ActionHeaderRow < Katalyst::Tables::Frontend::Builder::HeaderRow
|
299
|
+
def actions(&block)
|
300
|
+
cell(:actions, class: "actions", label: "")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
class ActionHeaderCell < Katalyst::Tables::Frontend::Builder::HeaderCell
|
305
|
+
end
|
306
|
+
|
307
|
+
class ActionBodyRow < Katalyst::Tables::Frontend::Builder::BodyRow
|
308
|
+
def actions(&block)
|
309
|
+
cell(:actions, class: "actions", &block)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class ActionBodyCell < Katalyst::Tables::Frontend::Builder::BodyCell
|
314
|
+
def action(label, href, **opts)
|
315
|
+
content_tag :a, label, { href: href }.merge(opts)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
```
|
320
|
+
|
321
|
+
If you have a table builder you want to reuse, you can set it as a default for some or all of your controllers:
|
322
|
+
|
323
|
+
```html
|
324
|
+
class ApplicationController < ActiveController::Base
|
325
|
+
default_table_builder ActionTableBuilder
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
## Development
|
330
|
+
|
331
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
332
|
+
|
333
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
334
|
+
|
335
|
+
## Contributing
|
336
|
+
|
337
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/katalyst-tables.
|
338
|
+
|
339
|
+
## License
|
340
|
+
|
341
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Backend
|
6
|
+
# A FormObject (model) representing the sort state of controller for a given
|
7
|
+
# collection/parameter.
|
8
|
+
class SortForm
|
9
|
+
DIRECTIONS = %w[asc desc].freeze
|
10
|
+
|
11
|
+
attr_accessor :column, :direction
|
12
|
+
|
13
|
+
def initialize(controller, column: nil, direction: nil)
|
14
|
+
self.column = column
|
15
|
+
self.direction = direction
|
16
|
+
|
17
|
+
@controller = controller
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true if the given collection supports sorting on the given
|
21
|
+
# column. A column supports sorting if it is a database column or if
|
22
|
+
# the collection responds to `order_by_#{column}(direction)`.
|
23
|
+
#
|
24
|
+
# @param collection [ActiveRecord::Relation]
|
25
|
+
# @param column [String, Symbol]
|
26
|
+
# @return [true, false]
|
27
|
+
def supports?(collection, column)
|
28
|
+
collection.respond_to?("order_by_#{column}") ||
|
29
|
+
collection.model.has_attribute?(column.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the current sort behaviour of the given column, for use as a
|
33
|
+
# column heading class in the table view.
|
34
|
+
#
|
35
|
+
# @param column [String, Symbol] the table column as defined in table_with
|
36
|
+
# @return [String] the current sort behaviour of the given column
|
37
|
+
def status(column)
|
38
|
+
direction if column.to_s == self.column
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generates a url for applying/toggling sort for the given column.
|
42
|
+
#
|
43
|
+
# @param column [String, Symbol] the table column as defined in table_with
|
44
|
+
# @return [String] URL for use as a link in a column header
|
45
|
+
def url_for(column)
|
46
|
+
# Implementation inspired by pagy's `pagy_url_for` helper.
|
47
|
+
|
48
|
+
request = @controller.request
|
49
|
+
|
50
|
+
# Preserve any existing GET parameters
|
51
|
+
# CAUTION: these parameters are not sanitised
|
52
|
+
params = request.GET.merge("sort" => "#{column} #{toggle_direction(column)}").except("page")
|
53
|
+
query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
|
54
|
+
|
55
|
+
"#{request.path}#{query_string}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generates a url for the current page without any sorting parameters.
|
59
|
+
#
|
60
|
+
# @return [String] URL for use as a link in a column header
|
61
|
+
def unsorted_url
|
62
|
+
request = @controller.request
|
63
|
+
|
64
|
+
# Preserve any existing GET parameters but remove sort.
|
65
|
+
# CAUTION: these parameters are not sanitised
|
66
|
+
params = request.GET.except("sort", "page")
|
67
|
+
query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
|
68
|
+
|
69
|
+
"#{request.path}#{query_string}"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Apply the constructed sort ordering to the collection.
|
73
|
+
#
|
74
|
+
# @param collection [ActiveRecord::Relation]
|
75
|
+
# @return [Array(SortForm, ActiveRecord::Relation)]
|
76
|
+
def apply(collection)
|
77
|
+
return [self, collection] if column.nil?
|
78
|
+
|
79
|
+
if collection.respond_to?("order_by_#{column}")
|
80
|
+
collection = collection.reorder(nil).public_send("order_by_#{column}", direction.to_sym)
|
81
|
+
elsif collection.model.has_attribute?(column)
|
82
|
+
collection = collection.reorder(column => direction)
|
83
|
+
else
|
84
|
+
clear!
|
85
|
+
end
|
86
|
+
|
87
|
+
[self, collection]
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def clear!
|
93
|
+
self.column = self.direction = nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def toggle_direction(column)
|
97
|
+
return "asc" unless column.to_s == self.column
|
98
|
+
|
99
|
+
case direction
|
100
|
+
when "asc"
|
101
|
+
"desc"
|
102
|
+
when "desc"
|
103
|
+
"asc"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/concern"
|
5
|
+
|
6
|
+
require_relative "backend/sort_form"
|
7
|
+
|
8
|
+
module Katalyst
|
9
|
+
module Tables
|
10
|
+
# Utilities for controllers that are generating collections for visualisation
|
11
|
+
# in a table view using Katalyst::Tables::Frontend.
|
12
|
+
#
|
13
|
+
# Provides `table_sort` for sorting based on column interactions (sort param).
|
14
|
+
module Backend
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
|
17
|
+
# Sort the given collection by params[:sort], which is set when a user
|
18
|
+
# interacts with a column header in a frontend table view.
|
19
|
+
#
|
20
|
+
# @return [[SortForm, ActiveRecord::Relation]]
|
21
|
+
def table_sort(collection)
|
22
|
+
column, direction = params[:sort]&.split(" ")
|
23
|
+
direction = "asc" unless SortForm::DIRECTIONS.include?(direction)
|
24
|
+
|
25
|
+
SortForm.new(self,
|
26
|
+
column: column,
|
27
|
+
direction: direction)
|
28
|
+
.apply(collection)
|
29
|
+
end
|
30
|
+
|
31
|
+
included do
|
32
|
+
class_attribute :_default_table_builder, instance_accessor: false
|
33
|
+
end
|
34
|
+
|
35
|
+
class_methods do
|
36
|
+
# Set the table builder to be used as the default for all tables
|
37
|
+
# in the views rendered by this controller and its subclasses.
|
38
|
+
#
|
39
|
+
# ==== Parameters
|
40
|
+
# * <tt>builder</tt> - Default table builder, an instance of +Katalyst::Tables::Frontend::TableBuilder+
|
41
|
+
def default_table_builder(builder)
|
42
|
+
self._default_table_builder = builder
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Default table builder for the controller
|
47
|
+
def default_table_builder
|
48
|
+
self.class._default_table_builder
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext/module/delegation"
|
5
|
+
|
6
|
+
require_relative "../helper"
|
7
|
+
|
8
|
+
module Katalyst
|
9
|
+
module Tables
|
10
|
+
module Frontend
|
11
|
+
module Builder
|
12
|
+
class Base # :nodoc:
|
13
|
+
include Helper
|
14
|
+
|
15
|
+
attr_reader :table
|
16
|
+
|
17
|
+
delegate :sort,
|
18
|
+
:table_header_cell,
|
19
|
+
:table_header_row,
|
20
|
+
:table_body_cell,
|
21
|
+
:table_body_row,
|
22
|
+
:template,
|
23
|
+
to: :table
|
24
|
+
|
25
|
+
delegate :content_tag,
|
26
|
+
:link_to,
|
27
|
+
:render,
|
28
|
+
:translate,
|
29
|
+
:with_output_buffer,
|
30
|
+
to: :template
|
31
|
+
|
32
|
+
def initialize(table, **options)
|
33
|
+
@table = table
|
34
|
+
@header = false
|
35
|
+
self.options(**options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def header?
|
39
|
+
@header
|
40
|
+
end
|
41
|
+
|
42
|
+
def body?
|
43
|
+
!@header
|
44
|
+
end
|
45
|
+
|
46
|
+
def options(**options)
|
47
|
+
@html_options = html_options_for_table_with(**options)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def table_tag(type, value = nil, &block)
|
53
|
+
# capture output before calling tag, to allow users to modify `options` during body execution
|
54
|
+
value = with_output_buffer(&block) if block_given?
|
55
|
+
|
56
|
+
content_tag(type, value, @html_options, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Frontend
|
8
|
+
module Builder
|
9
|
+
class BodyCell < Base # :nodoc:
|
10
|
+
attr_reader :object, :method
|
11
|
+
|
12
|
+
def initialize(table, object, method, **options)
|
13
|
+
super table, **options
|
14
|
+
|
15
|
+
@type = options.fetch(:heading, false) ? :th : :td
|
16
|
+
@object = object
|
17
|
+
@method = method
|
18
|
+
end
|
19
|
+
|
20
|
+
def build
|
21
|
+
table_tag(@type) { block_given? ? yield(self).to_s : value.to_s }
|
22
|
+
end
|
23
|
+
|
24
|
+
def value
|
25
|
+
object.public_send(method)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Frontend
|
8
|
+
module Builder
|
9
|
+
class BodyRow < Base # :nodoc:
|
10
|
+
attr_reader :object
|
11
|
+
|
12
|
+
def initialize(table, object)
|
13
|
+
super table
|
14
|
+
|
15
|
+
@object = object
|
16
|
+
end
|
17
|
+
|
18
|
+
def build
|
19
|
+
table_tag(:tr) { yield self, object }
|
20
|
+
end
|
21
|
+
|
22
|
+
def cell(method, **options, &block)
|
23
|
+
table_body_cell(object, method, **options, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "body_cell"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Frontend
|
8
|
+
module Builder
|
9
|
+
class HeaderCell < BodyCell # :nodoc:
|
10
|
+
def initialize(table, method, **options)
|
11
|
+
super(table, nil, method, **options)
|
12
|
+
|
13
|
+
@value = options[:label]
|
14
|
+
@header = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def build(&_block)
|
18
|
+
# NOTE: block ignored intentionally but subclasses may consume it
|
19
|
+
if @table.sort&.supports?(@table.collection, method)
|
20
|
+
content = sort_link(value) # writes to html_options
|
21
|
+
table_tag :th, content # consumes options
|
22
|
+
else
|
23
|
+
table_tag :th, value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def value
|
28
|
+
if !@value.nil?
|
29
|
+
@value
|
30
|
+
elsif @table.object_name.present?
|
31
|
+
translation
|
32
|
+
else
|
33
|
+
default_value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def translation(key = "activerecord.attributes.#{@table.object_name}.#{method}")
|
38
|
+
translate(key, default: default_value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_value
|
42
|
+
method.to_s.humanize.titleize
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def sort_link(content)
|
48
|
+
(@html_options["data"] ||= {})["sort"] = sort.status(method)
|
49
|
+
link_to(content, @table.sort.url_for(method))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "body_row"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Frontend
|
8
|
+
module Builder
|
9
|
+
class HeaderRow < BodyRow # :nodoc:
|
10
|
+
def initialize(table)
|
11
|
+
super table, nil
|
12
|
+
|
13
|
+
@header = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def cell(method, **options, &block)
|
17
|
+
table_header_cell(method, **options, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Frontend
|
6
|
+
module Helper # :nodoc:
|
7
|
+
private
|
8
|
+
|
9
|
+
def html_options_for_table_with(html: {}, **options)
|
10
|
+
html_options = options.slice(:id, :class, :data).merge(html)
|
11
|
+
html_options.stringify_keys!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "builder/body_cell"
|
4
|
+
require_relative "builder/body_row"
|
5
|
+
require_relative "builder/header_cell"
|
6
|
+
require_relative "builder/header_row"
|
7
|
+
|
8
|
+
module Katalyst
|
9
|
+
module Tables
|
10
|
+
module Frontend
|
11
|
+
# Builder API for generating HTML tables from ActiveRecord.
|
12
|
+
# @see Frontend#table_with
|
13
|
+
class TableBuilder
|
14
|
+
attr_reader :template, :collection, :object_name, :sort
|
15
|
+
|
16
|
+
def initialize(template, collection, options, html_options)
|
17
|
+
@template = template
|
18
|
+
@collection = collection
|
19
|
+
@header = options.fetch(:header, true)
|
20
|
+
@object_name = options.fetch(:object_name, nil)
|
21
|
+
@sort = options[:sort]
|
22
|
+
@html_options = html_options
|
23
|
+
end
|
24
|
+
|
25
|
+
def table_header_row(builder = nil, &block)
|
26
|
+
@template.table_header_row(self, builder || Builder::HeaderRow, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def table_header_cell(method, builder = nil, **options, &block)
|
30
|
+
@template.table_header_cell(self, method, builder || Builder::HeaderCell, **options, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def table_body_row(object, builder = nil, &block)
|
34
|
+
@template.table_body_row(self, object, builder || Builder::BodyRow, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def table_body_cell(object, method, builder = nil, **options, &block)
|
38
|
+
@template.table_body_cell(self, object, method, builder || Builder::BodyCell, **options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def build(&block)
|
42
|
+
template.content_tag("table", @html_options) do
|
43
|
+
thead(&block) + tbody(&block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def thead(&block)
|
50
|
+
return "".html_safe unless @header
|
51
|
+
|
52
|
+
template.content_tag("thead") do
|
53
|
+
table_header_row(&block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def tbody(&block)
|
58
|
+
template.content_tag("tbody") do
|
59
|
+
buffer = ActiveSupport::SafeBuffer.new
|
60
|
+
|
61
|
+
collection.each do |object|
|
62
|
+
buffer << table_body_row(object, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
buffer
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "frontend/builder/base"
|
4
|
+
require_relative "frontend/builder/body_cell"
|
5
|
+
require_relative "frontend/builder/body_row"
|
6
|
+
require_relative "frontend/builder/header_cell"
|
7
|
+
require_relative "frontend/builder/header_row"
|
8
|
+
require_relative "frontend/helper"
|
9
|
+
require_relative "frontend/table_builder"
|
10
|
+
|
11
|
+
module Katalyst
|
12
|
+
module Tables
|
13
|
+
# View Helper for generating HTML tables. Include in your ApplicationHelper, or similar.
|
14
|
+
module Frontend
|
15
|
+
include Helper
|
16
|
+
|
17
|
+
def table_with(collection:, **options, &block)
|
18
|
+
table_options = options.slice(:header, :object_name, :sort)
|
19
|
+
|
20
|
+
table_options[:object_name] ||= collection.try(:model_name)&.param_key
|
21
|
+
|
22
|
+
html_options = html_options_for_table_with(**options)
|
23
|
+
|
24
|
+
builder = options.fetch(:builder) { default_table_builder_class }
|
25
|
+
builder.new(self, collection, table_options, html_options).build(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def table_header_row(table, builder, &block)
|
29
|
+
builder.new(table).build(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def table_header_cell(table, method, builder, **options, &block)
|
33
|
+
builder.new(table, method, **options).build(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def table_body_row(table, object, builder, &block)
|
37
|
+
builder.new(table, object).build(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def table_body_cell(table, object, method, builder, **options, &block)
|
41
|
+
builder.new(table, object, method, **options).build(&block)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def default_table_builder_class
|
47
|
+
builder = controller.try(:default_table_builder) || TableBuilder
|
48
|
+
builder.respond_to?(:constantize) ? builder.constantize : builder
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: katalyst-tables
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Katalyst Interactive
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
description: Builder-style HTML table generator for building tabular index views.
|
28
|
+
Supports sorting by columns.
|
29
|
+
email:
|
30
|
+
- devs@katalyst.com.au
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- CHANGELOG.md
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- lib/katalyst/tables.rb
|
39
|
+
- lib/katalyst/tables/backend.rb
|
40
|
+
- lib/katalyst/tables/backend/sort_form.rb
|
41
|
+
- lib/katalyst/tables/frontend.rb
|
42
|
+
- lib/katalyst/tables/frontend/builder/base.rb
|
43
|
+
- lib/katalyst/tables/frontend/builder/body_cell.rb
|
44
|
+
- lib/katalyst/tables/frontend/builder/body_row.rb
|
45
|
+
- lib/katalyst/tables/frontend/builder/header_cell.rb
|
46
|
+
- lib/katalyst/tables/frontend/builder/header_row.rb
|
47
|
+
- lib/katalyst/tables/frontend/helper.rb
|
48
|
+
- lib/katalyst/tables/frontend/table_builder.rb
|
49
|
+
- lib/katalyst/tables/version.rb
|
50
|
+
homepage: https://github.com/katalyst/katalyst-tables
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata:
|
54
|
+
allowed_push_host: https://rubygems.org
|
55
|
+
rubygems_mfa_required: 'true'
|
56
|
+
homepage_uri: https://github.com/katalyst/katalyst-tables
|
57
|
+
source_code_uri: https://github.com/katalyst/katalyst-tables
|
58
|
+
changelog_uri: https://github.com/katalyst/katalyst-tables/blobs/main/CHANGELOG.md
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 2.6.0
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.1.6
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: HTML table generator for Rails views
|
78
|
+
test_files: []
|