katalyst-tables 1.1.0 → 2.1.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +166 -134
  4. data/app/assets/config/katalyst-tables.js +1 -0
  5. data/app/assets/javascripts/controllers/tables/turbo_collection_controller.js +22 -0
  6. data/app/components/concerns/katalyst/tables/configurable_component.rb +31 -0
  7. data/app/components/concerns/katalyst/tables/has_html_attributes.rb +45 -0
  8. data/app/components/concerns/katalyst/tables/has_table_content.rb +33 -0
  9. data/app/components/concerns/katalyst/tables/sortable.rb +32 -0
  10. data/app/components/concerns/katalyst/tables/turbo_replaceable.rb +62 -0
  11. data/app/components/katalyst/table_component.rb +102 -0
  12. data/app/components/katalyst/tables/body_cell_component.rb +40 -0
  13. data/app/components/katalyst/tables/body_row_component.rb +40 -0
  14. data/app/components/katalyst/tables/empty_caption_component.html.erb +6 -0
  15. data/app/components/katalyst/tables/empty_caption_component.rb +38 -0
  16. data/app/components/katalyst/tables/header_cell_component.rb +58 -0
  17. data/app/components/katalyst/tables/header_row_component.rb +40 -0
  18. data/app/components/katalyst/tables/pagy_nav_component.rb +26 -0
  19. data/app/components/katalyst/turbo/pagy_nav_component.rb +23 -0
  20. data/app/components/katalyst/turbo/table_component.rb +48 -0
  21. data/app/models/concerns/katalyst/tables/collection/core.rb +71 -0
  22. data/app/models/concerns/katalyst/tables/collection/pagination.rb +66 -0
  23. data/app/models/concerns/katalyst/tables/collection/sorting.rb +63 -0
  24. data/app/models/katalyst/tables/collection.rb +32 -0
  25. data/config/importmap.rb +7 -0
  26. data/lib/katalyst/tables/backend/sort_form.rb +16 -2
  27. data/lib/katalyst/tables/backend.rb +8 -8
  28. data/lib/katalyst/tables/engine.rb +24 -0
  29. data/lib/katalyst/tables/frontend/helper.rb +8 -9
  30. data/lib/katalyst/tables/frontend.rb +6 -36
  31. data/lib/katalyst/tables/version.rb +1 -1
  32. data/lib/katalyst/tables.rb +5 -0
  33. metadata +54 -9
  34. data/lib/katalyst/tables/frontend/builder/base.rb +0 -63
  35. data/lib/katalyst/tables/frontend/builder/body_cell.rb +0 -31
  36. data/lib/katalyst/tables/frontend/builder/body_row.rb +0 -29
  37. data/lib/katalyst/tables/frontend/builder/header_cell.rb +0 -55
  38. data/lib/katalyst/tables/frontend/builder/header_row.rb +0 -23
  39. data/lib/katalyst/tables/frontend/table_builder.rb +0 -71
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Collection
6
+ # Entry point for creating a collection for use with table components.
7
+ # This class is intended to be subclassed, i.e.:
8
+ #
9
+ # class ApplicationController < ActionController::Base
10
+ # class Collection < Katalyst::Tables::Collection::Base
11
+ # ...
12
+ # end
13
+ # end
14
+ #
15
+ # In the context of a controller action, construct a collection, apply it
16
+ # to a model, then pass the result to the view component:
17
+ # ```
18
+ # collection = Collection.new.with_params(params).apply(People.all)
19
+ # table = Katalyst::TableComponent.new(collection: collection)
20
+ # render table
21
+ # ````
22
+ class Base
23
+ include Core
24
+ include Pagination
25
+ include Sorting
26
+
27
+ use(Pagination::Paginate)
28
+ use(Sorting::Sort)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
4
+
5
+ pin_all_from Katalyst::Tables::Engine.root.join("app/assets/javascripts"),
6
+ # preload in tests so that we don't start clicking before controllers load
7
+ preload: Rails.env.test?
@@ -8,11 +8,25 @@ module Katalyst
8
8
  class SortForm
9
9
  DIRECTIONS = %w[asc desc].freeze
10
10
 
11
- attr_accessor :column, :direction
11
+ attr_accessor :column, :direction, :default
12
12
 
13
- def initialize(column: nil, direction: nil)
13
+ def self.parse(param, default: nil)
14
+ column, direction = param.to_s.split
15
+ direction = "asc" unless DIRECTIONS.include?(direction)
16
+
17
+ default = SortForm.parse(default).to_param if default.present?
18
+
19
+ SortForm.new(column: column, direction: direction, default: default)
20
+ end
21
+
22
+ def initialize(column: nil, direction: nil, default: nil)
14
23
  self.column = column
15
24
  self.direction = direction
25
+ self.default = default
26
+ end
27
+
28
+ def to_param
29
+ "#{column} #{direction}"
16
30
  end
17
31
 
18
32
  # Returns true if the given collection supports sorting on the given
@@ -28,23 +28,23 @@ module Katalyst
28
28
  end
29
29
 
30
30
  included do
31
- class_attribute :_default_table_builder, instance_accessor: false
31
+ class_attribute :_default_table_component, instance_accessor: false
32
32
  end
33
33
 
34
34
  class_methods do
35
- # Set the table builder to be used as the default for all tables
35
+ # Set the table component to be used as the default for all tables
36
36
  # in the views rendered by this controller and its subclasses.
37
37
  #
38
38
  # ==== Parameters
39
- # * <tt>builder</tt> - Default table builder, an instance of +Katalyst::Tables::Frontend::TableBuilder+
40
- def default_table_builder(builder)
41
- self._default_table_builder = builder
39
+ # * <tt>component</tt> - Default table component, an instance of +Katalyst::TableComponent+
40
+ def default_table_component(component)
41
+ self._default_table_component = component
42
42
  end
43
43
  end
44
44
 
45
- # Default table builder for the controller
46
- def default_table_builder
47
- self.class._default_table_builder
45
+ # Default table component for this controller
46
+ def default_table_component
47
+ self.class._default_table_component
48
48
  end
49
49
  end
50
50
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module Katalyst
6
+ module Tables
7
+ class Engine < ::Rails::Engine # :nodoc:
8
+ isolate_namespace Katalyst::Tables
9
+
10
+ initializer "katalyst-tables.asset" do
11
+ config.after_initialize do |app|
12
+ app.config.assets.precompile += %w[katalyst-tables.js] if app.config.respond_to?(:assets)
13
+ end
14
+ end
15
+
16
+ initializer "katalyst-tables.importmap", before: "importmap" do |app|
17
+ if app.config.respond_to?(:importmap)
18
+ app.config.importmap.paths << root.join("config/importmap.rb")
19
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,6 +3,7 @@
3
3
  module Katalyst
4
4
  module Tables
5
5
  module Frontend
6
+ # @deprecated Use {Katalyst::TableComponent} instead.
6
7
  module Helper # :nodoc:
7
8
  extend ActiveSupport::Concern
8
9
 
@@ -10,22 +11,20 @@ module Katalyst
10
11
  #
11
12
  # @param sort [String, nil] sort parameter to apply, or nil to remove sorting
12
13
  # @return [String] URL for toggling column sorting
13
- def sort_url_for(sort: nil)
14
+ # @deprecated Use {Katalyst::TablesComponent} instead.
15
+ def sort_url_for(sort: nil, default: nil)
14
16
  # Implementation inspired by pagy's `pagy_url_for` helper.
15
17
  # Preserve any existing GET parameters
16
18
  # CAUTION: these parameters are not sanitised
17
- params = request.GET.merge("sort" => sort).except("page")
19
+ params = if sort && !sort.eql?(default)
20
+ request.GET.merge("sort" => sort).except("page")
21
+ else
22
+ request.GET.except("page", "sort")
23
+ end
18
24
  query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
19
25
 
20
26
  "#{request.path}#{query_string}"
21
27
  end
22
-
23
- private
24
-
25
- def html_options_for_table_with(html: {}, **options)
26
- html_options = options.slice(:id, :class, :data).merge(html)
27
- html_options.stringify_keys!
28
- end
29
28
  end
30
29
  end
31
30
  end
@@ -1,51 +1,21 @@
1
1
  # frozen_string_literal: true
2
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
3
  require_relative "frontend/helper"
9
- require_relative "frontend/table_builder"
10
4
 
11
5
  module Katalyst
12
6
  module Tables
13
7
  # View Helper for generating HTML tables. Include in your ApplicationHelper, or similar.
14
8
  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)&.i18n_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)
9
+ def table_with(collection:, component: nil, **options, &block)
10
+ component ||= default_table_component_class
11
+ render(component.new(collection: collection, **options), &block)
42
12
  end
43
13
 
44
14
  private
45
15
 
46
- def default_table_builder_class
47
- builder = controller.try(:default_table_builder) || TableBuilder
48
- builder.respond_to?(:constantize) ? builder.constantize : builder
16
+ def default_table_component_class
17
+ component = controller.try(:default_table_component) || TableComponent
18
+ component.respond_to?(:constantize) ? component.constantize : component
49
19
  end
50
20
  end
51
21
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Katalyst
4
4
  module Tables
5
- VERSION = "1.1.0"
5
+ VERSION = "2.1.0"
6
6
  end
7
7
  end
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "view_component"
4
+
3
5
  require_relative "tables/backend"
6
+ require_relative "tables/engine"
4
7
  require_relative "tables/frontend"
5
8
  require_relative "tables/version"
6
9
 
10
+ require_relative "tables/engine" if Object.const_defined?("Rails")
11
+
7
12
  module Katalyst
8
13
  module Tables
9
14
  class Error < StandardError; end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-tables
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-18 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-07-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: html-attributes-utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: view_component
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  description: Builder-style HTML table generator for building tabular index views.
14
42
  Supports sorting by columns.
15
43
  email:
@@ -21,17 +49,34 @@ files:
21
49
  - CHANGELOG.md
22
50
  - LICENSE.txt
23
51
  - README.md
52
+ - app/assets/config/katalyst-tables.js
53
+ - app/assets/javascripts/controllers/tables/turbo_collection_controller.js
54
+ - app/components/concerns/katalyst/tables/configurable_component.rb
55
+ - app/components/concerns/katalyst/tables/has_html_attributes.rb
56
+ - app/components/concerns/katalyst/tables/has_table_content.rb
57
+ - app/components/concerns/katalyst/tables/sortable.rb
58
+ - app/components/concerns/katalyst/tables/turbo_replaceable.rb
59
+ - app/components/katalyst/table_component.rb
60
+ - app/components/katalyst/tables/body_cell_component.rb
61
+ - app/components/katalyst/tables/body_row_component.rb
62
+ - app/components/katalyst/tables/empty_caption_component.html.erb
63
+ - app/components/katalyst/tables/empty_caption_component.rb
64
+ - app/components/katalyst/tables/header_cell_component.rb
65
+ - app/components/katalyst/tables/header_row_component.rb
66
+ - app/components/katalyst/tables/pagy_nav_component.rb
67
+ - app/components/katalyst/turbo/pagy_nav_component.rb
68
+ - app/components/katalyst/turbo/table_component.rb
69
+ - app/models/concerns/katalyst/tables/collection/core.rb
70
+ - app/models/concerns/katalyst/tables/collection/pagination.rb
71
+ - app/models/concerns/katalyst/tables/collection/sorting.rb
72
+ - app/models/katalyst/tables/collection.rb
73
+ - config/importmap.rb
24
74
  - lib/katalyst/tables.rb
25
75
  - lib/katalyst/tables/backend.rb
26
76
  - lib/katalyst/tables/backend/sort_form.rb
77
+ - lib/katalyst/tables/engine.rb
27
78
  - lib/katalyst/tables/frontend.rb
28
- - lib/katalyst/tables/frontend/builder/base.rb
29
- - lib/katalyst/tables/frontend/builder/body_cell.rb
30
- - lib/katalyst/tables/frontend/builder/body_row.rb
31
- - lib/katalyst/tables/frontend/builder/header_cell.rb
32
- - lib/katalyst/tables/frontend/builder/header_row.rb
33
79
  - lib/katalyst/tables/frontend/helper.rb
34
- - lib/katalyst/tables/frontend/table_builder.rb
35
80
  - lib/katalyst/tables/version.rb
36
81
  homepage: https://github.com/katalyst/katalyst-tables
37
82
  licenses:
@@ -1,63 +0,0 @@
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
- :request,
29
- :translate,
30
- :with_output_buffer,
31
- to: :template
32
-
33
- def initialize(table, **options)
34
- @table = table
35
- @header = false
36
- self.options(**options)
37
- end
38
-
39
- def header?
40
- @header
41
- end
42
-
43
- def body?
44
- !@header
45
- end
46
-
47
- def options(**options)
48
- @html_options = html_options_for_table_with(**options)
49
- end
50
-
51
- private
52
-
53
- def table_tag(type, value = nil, &block)
54
- # capture output before calling tag, to allow users to modify `options` during body execution
55
- value = with_output_buffer(&block) if block_given?
56
-
57
- content_tag(type, value, @html_options, &block)
58
- end
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,31 +0,0 @@
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
@@ -1,29 +0,0 @@
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
@@ -1,55 +0,0 @@
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, sort_url_for(sort: sort.toggle(method)))
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,23 +0,0 @@
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
@@ -1,71 +0,0 @@
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