katalyst-tables 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|