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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2022-03-23
4
+
5
+ - Initial release
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tables/backend"
4
+ require_relative "tables/frontend"
5
+ require_relative "tables/version"
6
+
7
+ module Katalyst
8
+ module Tables
9
+ class Error < StandardError; end
10
+ end
11
+ 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: []