katalyst-tables 1.1.0 → 2.1.0

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