katalyst-tables 1.0.0

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