katalyst-tables 3.0.0.beta1 → 3.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 +4 -4
- data/CHANGELOG.md +12 -2
- data/README.md +56 -190
- data/app/assets/builds/katalyst/tables.esm.js +17 -47
- data/app/assets/builds/katalyst/tables.js +17 -47
- data/app/assets/builds/katalyst/tables.min.js +1 -1
- data/app/assets/builds/katalyst/tables.min.js.map +1 -1
- data/app/components/concerns/katalyst/tables/has_table_content.rb +17 -8
- data/app/components/concerns/katalyst/tables/identifiable.rb +51 -0
- data/app/components/concerns/katalyst/tables/orderable.rb +35 -105
- data/app/components/concerns/katalyst/tables/selectable.rb +18 -75
- data/app/components/concerns/katalyst/tables/sortable.rb +51 -17
- data/app/components/katalyst/table_component.html.erb +4 -4
- data/app/components/katalyst/table_component.rb +271 -53
- data/app/components/katalyst/tables/body_row_component.html.erb +5 -0
- data/app/components/katalyst/tables/body_row_component.rb +4 -31
- data/app/components/katalyst/tables/cell_component.rb +85 -0
- data/app/components/katalyst/tables/{body → cells}/boolean_component.rb +8 -2
- data/app/components/katalyst/tables/{body → cells}/currency_component.rb +7 -7
- data/app/components/katalyst/tables/{body → cells}/date_component.rb +12 -9
- data/app/components/katalyst/tables/{body → cells}/date_time_component.rb +13 -10
- data/app/components/katalyst/tables/{body → cells}/number_component.rb +5 -5
- data/app/components/katalyst/tables/cells/ordinal_component.rb +44 -0
- data/app/components/katalyst/tables/{body → cells}/rich_text_component.rb +8 -5
- data/app/components/katalyst/tables/cells/select_component.rb +39 -0
- data/app/components/katalyst/tables/data.rb +30 -0
- data/app/components/katalyst/tables/header_row_component.html.erb +5 -0
- data/app/components/katalyst/tables/header_row_component.rb +4 -25
- data/app/components/katalyst/tables/label.rb +37 -0
- data/app/components/katalyst/tables/orderable/form_component.rb +38 -0
- data/app/components/katalyst/tables/selectable/form_component.html.erb +3 -3
- data/app/components/katalyst/tables/selectable/form_component.rb +8 -11
- data/app/controllers/concerns/katalyst/tables/backend.rb +2 -28
- data/app/helpers/katalyst/tables/frontend.rb +48 -2
- data/app/javascript/tables/application.js +0 -5
- data/app/javascript/tables/orderable/form_controller.js +8 -6
- data/app/javascript/tables/orderable/item_controller.js +9 -0
- data/app/models/concerns/katalyst/tables/collection/core.rb +6 -1
- data/app/models/concerns/katalyst/tables/collection/sorting.rb +85 -17
- data/app/models/katalyst/tables/collection/array.rb +38 -0
- data/app/models/katalyst/tables/collection/base.rb +4 -0
- data/config/locales/tables.en.yml +0 -6
- data/lib/katalyst/tables/config.rb +23 -0
- data/lib/katalyst/tables.rb +9 -0
- metadata +22 -29
- data/app/components/concerns/katalyst/tables/body/typed_columns.rb +0 -132
- data/app/components/concerns/katalyst/tables/configurable_component.rb +0 -52
- data/app/components/concerns/katalyst/tables/header/typed_columns.rb +0 -179
- data/app/components/katalyst/tables/body/attachment_component.rb +0 -58
- data/app/components/katalyst/tables/body/link_component.rb +0 -40
- data/app/components/katalyst/tables/body_cell_component.rb +0 -55
- data/app/components/katalyst/tables/header/attachment_component.rb +0 -15
- data/app/components/katalyst/tables/header/boolean_component.rb +0 -15
- data/app/components/katalyst/tables/header/currency_component.rb +0 -15
- data/app/components/katalyst/tables/header/date_component.rb +0 -15
- data/app/components/katalyst/tables/header/date_time_component.rb +0 -15
- data/app/components/katalyst/tables/header/link_component.rb +0 -15
- data/app/components/katalyst/tables/header/number_component.rb +0 -15
- data/app/components/katalyst/tables/header/rich_text_component.rb +0 -15
- data/app/components/katalyst/tables/header_cell_component.rb +0 -97
- data/app/helpers/katalyst/tables/frontend/helper.rb +0 -31
- data/app/javascript/tables/turbo/collection_controller.js +0 -38
- data/app/models/katalyst/tables/collection/sort_form.rb +0 -120
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: cda2701479ecd72f9964ab539c5203bd0c300aeb962cf926552904575474ddea
         | 
| 4 | 
            +
              data.tar.gz: 6f43fc5e31c8536811e28cba0db4be3c0967db24d06ad22f2a72c89512030d87
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 14acdb5b09216c84c06b2082ed2a11888f0930b1807f7ade14cbc7fa02751f3c888a69b5397df7eb028395b7902eaad47abaa710e33fa24ecc6d6ed98757616f
         | 
| 7 | 
            +
              data.tar.gz: f3721287c65a5aa0d4e81c36d5f704b90e2995cf372376ba19c9b03cb5b0129a5f4c8a56b369a09b2c62e72eb585163d656abb303a8dd62aa8b7912193f710b5
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,8 +1,18 @@ | |
| 1 | 
            -
            ## [3.0.0 | 
| 2 | 
            -
             | 
| 1 | 
            +
            ## [3.0.0]
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            - Breaking change: remove Turbo Streams from table and pagination components,
         | 
| 4 | 
            +
              focus preservation is handled via Turbo Morph
         | 
| 3 5 | 
             
            - Improve spec coverage
         | 
| 6 | 
            +
            - Re-write internals to make it easier to extend and customize
         | 
| 4 7 | 
             
            - Update examples in [README](README.md) and [docs](/docs) to reflect changes
         | 
| 5 8 |  | 
| 9 | 
            +
            If you're upgrading from 2.x, you'll need to change your controllers to use
         | 
| 10 | 
            +
            the recommendations from the README. The changes should be straightforward,
         | 
| 11 | 
            +
            but you will need to enable morphing to allow focus preservation.
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            In general, we don't recommend using row partials anymore, as it's easier to
         | 
| 14 | 
            +
            read the code when the row is defined in the index view.
         | 
| 15 | 
            +
             | 
| 6 16 | 
             
            ## [2.6.0]
         | 
| 7 17 |  | 
| 8 18 | 
             
            - Added table row selection
         | 
    
        data/README.md
    CHANGED
    
    | @@ -24,33 +24,48 @@ import tables from "@katalyst/tables"; | |
| 24 24 | 
             
            application.load(tables);
         | 
| 25 25 | 
             
            ```
         | 
| 26 26 |  | 
| 27 | 
            +
            ```scss
         | 
| 28 | 
            +
            // app/assets/stylesheets/application.scss
         | 
| 29 | 
            +
            @use "@katalyst/tables";
         | 
| 30 | 
            +
            ```
         | 
| 31 | 
            +
             | 
| 27 32 | 
             
            ## Usage
         | 
| 28 33 |  | 
| 29 34 | 
             
            This gem provides entry points for backend and frontend concerns:
         | 
| 30 | 
            -
            * `Katalyst::TableComponent` can be used render encapsulated tables | 
| 31 | 
            -
              partial for each row.
         | 
| 35 | 
            +
            * `Katalyst::TableComponent` can be used render encapsulated tables.
         | 
| 32 36 | 
             
            * `Katalyst::Tables::Frontend` provides `table_with` for inline table generation
         | 
| 33 37 | 
             
            * `Katalyst::Tables::Collection::Base` provides a default entry point for
         | 
| 34 38 | 
             
              building collections in your controller actions.
         | 
| 35 39 |  | 
| 36 | 
            -
             | 
| 40 | 
            +
            ### Frontend
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            You can use the `table_with` helper to generate a table inline in your view without explicitly interacting with the
         | 
| 45 | 
            +
            table component. This is the preferred approach when creating tables. However, if the table is complex or you need to 
         | 
| 46 | 
            +
            reuse it, you should consider moving the definition of the row into a partial.
         | 
| 37 47 |  | 
| 38 | 
            -
             | 
| 39 | 
            -
            collection | 
| 48 | 
            +
            ```erb
         | 
| 49 | 
            +
            <%= table_with collection: @people do |row, person| %>
         | 
| 50 | 
            +
              <% row.text :name do |cell| %>
         | 
| 51 | 
            +
                <%= link_to cell.value, [:edit, person] %>
         | 
| 52 | 
            +
              <% end %>
         | 
| 53 | 
            +
              <% row.text :email %>
         | 
| 54 | 
            +
            <% end %>
         | 
| 55 | 
            +
            ```
         | 
| 40 56 |  | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
            render it for each row (and once for the header row).
         | 
| 57 | 
            +
            By not providing a block to the `table_with` call, the gem will look for a partial called `_person.html+row.erb` to
         | 
| 58 | 
            +
            render each row:
         | 
| 44 59 |  | 
| 45 60 | 
             
            ```erb
         | 
| 46 61 | 
             
            <%# locals: { row:, person: nil } %>
         | 
| 47 | 
            -
            <% row. | 
| 62 | 
            +
            <% row.text :name do |cell| %>
         | 
| 48 63 | 
             
              <%= link_to cell.value, [:edit, person] %>
         | 
| 49 64 | 
             
            <% end %>
         | 
| 50 | 
            -
            <% row. | 
| 65 | 
            +
            <% row.text :email %>
         | 
| 51 66 | 
             
            ```
         | 
| 52 67 |  | 
| 53 | 
            -
            The table  | 
| 68 | 
            +
            The table will call your block / partial once per row and accumulate the cells
         | 
| 54 69 | 
             
            you generate into rows, including a header row:
         | 
| 55 70 |  | 
| 56 71 | 
             
            ```html
         | 
| @@ -79,52 +94,36 @@ You can customize the partial and/or the name of the resource in a similar style | |
| 79 94 | 
             
            to view partials:
         | 
| 80 95 |  | 
| 81 96 | 
             
            ```erb
         | 
| 82 | 
            -
            <%=  | 
| 83 | 
            -
            ``` 
         | 
| 84 | 
            -
             | 
| 85 | 
            -
            ### Inline tables
         | 
| 86 | 
            -
             | 
| 87 | 
            -
            You can use the `table_with` helper to generate a table inline in your view without explicitly interacting with the
         | 
| 88 | 
            -
            table component. This is primarily intended for backwards compatibility, but it can be useful for simple tables.
         | 
| 89 | 
            -
             | 
| 90 | 
            -
            Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
         | 
| 91 | 
            -
             | 
| 92 | 
            -
            ```erb
         | 
| 93 | 
            -
            <%= table_with collection: @people do |row, person| %>
         | 
| 94 | 
            -
              <% row.cell :name do |cell| %>
         | 
| 95 | 
            -
                <%= link_to cell.value, [:edit, person] %>
         | 
| 96 | 
            -
              <% end %>
         | 
| 97 | 
            -
              <% row.cell :email %>
         | 
| 98 | 
            -
            <% end %>
         | 
| 97 | 
            +
            <%= table_with(collection: @employees, as: :person, partial: "person") %>
         | 
| 99 98 | 
             
            ```
         | 
| 100 99 |  | 
| 101 100 | 
             
            ### HTML Attributes
         | 
| 102 101 |  | 
| 103 102 | 
             
            You can add custom attributes on table, row, and cell tags.
         | 
| 104 103 |  | 
| 105 | 
            -
            The table tag takes attributes passed to ` | 
| 104 | 
            +
            The table tag takes attributes passed to `table_with` helper, similar to `form_with`:
         | 
| 106 105 |  | 
| 107 106 | 
             
            ```erb
         | 
| 108 | 
            -
            <%=  | 
| 107 | 
            +
            <%= table_with(collection: @people, id: "people-table")
         | 
| 109 108 | 
             
            ```
         | 
| 110 109 |  | 
| 111 110 | 
             
            Cells support the same approach:
         | 
| 112 111 |  | 
| 113 112 | 
             
            ```erb
         | 
| 114 | 
            -
            <%= row. | 
| 113 | 
            +
            <%= row.text :name, class: "name" %>
         | 
| 115 114 | 
             
            ```
         | 
| 116 115 |  | 
| 117 116 | 
             
            Rows do not get called directly, so instead you can assign to `html_attributes` on the row builder to customize row tag
         | 
| 118 117 | 
             
            generation.
         | 
| 119 118 |  | 
| 120 119 | 
             
            ```erb
         | 
| 121 | 
            -
            <% row. | 
| 120 | 
            +
            <% row.update_html_attributes(id: person.id) if row.body? %>
         | 
| 122 121 | 
             
            ```
         | 
| 123 122 |  | 
| 124 123 | 
             
            Note: because the row builder gets called to generate the header row, you may need to guard calls that access the
         | 
| 125 124 | 
             
            `person` directly as shown in the previous example. You could also check whether `person` is present.
         | 
| 126 125 |  | 
| 127 | 
            -
             | 
| 126 | 
            +
            ### Headers
         | 
| 128 127 |  | 
| 129 128 | 
             
            Tables will automatically generate a header row for you by calling your row partial or provided block with no object.
         | 
| 130 129 | 
             
            During this call, `row.header?` is true, `row.body?` is false, and the object (`person`) is nil.
         | 
| @@ -133,14 +132,14 @@ All cells generated in the table header iteration will automatically be header c | |
| 133 132 | 
             
            in your body rows by passing `heading: true` when you generate the cell.
         | 
| 134 133 |  | 
| 135 134 | 
             
            ```erb
         | 
| 136 | 
            -
            <% row. | 
| 135 | 
            +
            <% row.number :id, heading: true %>
         | 
| 137 136 | 
             
            ```
         | 
| 138 137 |  | 
| 139 138 | 
             
            The table header cells default to showing the capitalized column name, but you can customize this in one of two ways:
         | 
| 140 139 |  | 
| 141 140 | 
             
            * Set the value inline
         | 
| 142 141 | 
             
                ```erb
         | 
| 143 | 
            -
                <% row. | 
| 142 | 
            +
                <% row.number :id, label: "ID" %>
         | 
| 144 143 | 
             
                ```
         | 
| 145 144 | 
             
            * Define a translation for the attribute
         | 
| 146 145 | 
             
                ```yml
         | 
| @@ -163,194 +162,61 @@ the table cell. This is often all you need to do, but if you do want to customis | |
| 163 162 | 
             
            the value you can pass a block instead:
         | 
| 164 163 |  | 
| 165 164 | 
             
            ```erb
         | 
| 166 | 
            -
            <% row. | 
| 165 | 
            +
            <% row.text :status do %>
         | 
| 167 166 | 
             
              <%= person.password.present? ? "Active" : "Invited" %>
         | 
| 168 167 | 
             
            <% end %>
         | 
| 169 168 | 
             
            ```
         | 
| 170 169 |  | 
| 171 | 
            -
            In the context of the block you have access the cell  | 
| 170 | 
            +
            In the context of the block you have access the cell component if you simply
         | 
| 172 171 | 
             
            want to extend the default behaviour:
         | 
| 173 172 |  | 
| 174 173 | 
             
            ```erb
         | 
| 175 | 
            -
             | 
| 176 | 
            -
             | 
| 174 | 
            +
            <%# @type [Katalyst::Tables::CellComponent] cell %>
         | 
| 175 | 
            +
            <% row.text :name do |cell| %>
         | 
| 176 | 
            +
              <%= link_to cell, person %>
         | 
| 177 177 | 
             
            <% end %>
         | 
| 178 178 | 
             
            ```
         | 
| 179 179 |  | 
| 180 | 
            -
            You can also  | 
| 181 | 
            -
            builder,  | 
| 182 | 
            -
            as arguments.
         | 
| 180 | 
            +
            You can also update `html_attributes` on the cell builder, similar to the row
         | 
| 181 | 
            +
            builder, see `katalyst-html-attributes` for details.
         | 
| 183 182 |  | 
| 184 183 | 
             
            ## Collections
         | 
| 185 184 |  | 
| 186 185 | 
             
            The `Katalyst::Tables::Collection::Base` class provides a convenient way to
         | 
| 187 186 | 
             
            manage collections in your controller actions. It is designed to be used with
         | 
| 188 187 | 
             
            Pagy for pagination and provides built-in sorting when used with ActiveRecord
         | 
| 189 | 
            -
            collections. Sorting and Pagination are off by default,  | 
| 190 | 
            -
            a custom ` | 
| 188 | 
            +
            collections. Sorting and Pagination are off by default, you can either set them
         | 
| 189 | 
            +
            on creation or create a custom `Collection` class that sets them on by default:
         | 
| 191 190 |  | 
| 192 191 | 
             
            ```ruby
         | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 192 | 
            +
            # in #index
         | 
| 193 | 
            +
            Katalyst::Tables::Collection::Base.new(sorting: "name asc", pagination: true)
         | 
| 194 | 
            +
            # or as a nested class in your controller
         | 
| 195 | 
            +
            class Collection < Katalyst::Tables::Collection::Base
         | 
| 196 | 
            +
              config.sorting = "name asc" # requires models have a name attribute
         | 
| 195 197 | 
             
              config.pagination = true
         | 
| 196 198 | 
             
            end
         | 
| 197 199 | 
             
            ```
         | 
| 198 200 |  | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
            ```ruby
         | 
| 202 | 
            -
            class PeopleController < ApplicationController
         | 
| 203 | 
            -
              def index
         | 
| 204 | 
            -
                @people = ApplicationCollection.new.with_params(params).apply(People.all)
         | 
| 205 | 
            -
              end
         | 
| 206 | 
            -
            end
         | 
| 207 | 
            -
            ```
         | 
| 208 | 
            -
             | 
| 209 | 
            -
            Collections can be passed directly to `TableComponent` and it will automatically
         | 
| 201 | 
            +
            Collections can be passed directly to `table_with` method and it will automatically
         | 
| 210 202 | 
             
            detect features such as sorting and generate the appropriate table header links.
         | 
| 211 203 |  | 
| 212 204 | 
             
            ```erb
         | 
| 213 | 
            -
            <%=  | 
| 214 | 
            -
            ```
         | 
| 215 | 
            -
             | 
| 216 | 
            -
            ## Sort
         | 
| 217 | 
            -
             | 
| 218 | 
            -
            When sort is enabled, table columns will be automatically sortable in the
         | 
| 219 | 
            -
            frontend for any column that corresponds to an attribute on the model. You can
         | 
| 220 | 
            -
            also add sorting to non-attribute columns by defining a scope in your
         | 
| 221 | 
            -
            model:
         | 
| 222 | 
            -
             | 
| 223 | 
            -
            ```
         | 
| 224 | 
            -
            scope :order_by_status, ->(direction) { ... }
         | 
| 225 | 
            -
            ```
         | 
| 226 | 
            -
             | 
| 227 | 
            -
            You can also use sort without using collections, this was the primary backend
         | 
| 228 | 
            -
            interface for V1 and takes design cues from Pagy. Start by including the backend
         | 
| 229 | 
            -
            in your controller(s):
         | 
| 230 | 
            -
             | 
| 231 | 
            -
            ```ruby
         | 
| 232 | 
            -
            include Katalyst::Tables::Backend
         | 
| 233 | 
            -
            ```
         | 
| 234 | 
            -
             | 
| 235 | 
            -
            Now, in your controller index actions, you can sort your active record
         | 
| 236 | 
            -
            collections based on the `sort` param which is appended to the current URL as a
         | 
| 237 | 
            -
            get parameter when a user clicks on a column header.
         | 
| 238 | 
            -
             | 
| 239 | 
            -
            Building on our example from earlier:
         | 
| 240 | 
            -
             | 
| 241 | 
            -
            ```ruby
         | 
| 242 | 
            -
            class PeopleController < ApplicationController
         | 
| 243 | 
            -
              include Katalyst::Tables::Backend
         | 
| 244 | 
            -
              
         | 
| 245 | 
            -
              def index
         | 
| 246 | 
            -
                @people = People.all
         | 
| 247 | 
            -
             | 
| 248 | 
            -
                @sort, @people = table_sort(@people) # sort
         | 
| 249 | 
            -
              end
         | 
| 250 | 
            -
            end
         | 
| 251 | 
            -
            ```
         | 
| 252 | 
            -
             | 
| 253 | 
            -
            You then add the sort form object to your view so that it can add column header
         | 
| 254 | 
            -
            links and show the current sort state:
         | 
| 255 | 
            -
             | 
| 256 | 
            -
            ```erb
         | 
| 257 | 
            -
            <%= table_with collection: @people, sort: @sort do |row, person| %>
         | 
| 258 | 
            -
              <%= row.cell :name %>
         | 
| 259 | 
            -
              <%= row.cell :email %>
         | 
| 260 | 
            -
            <% end %>
         | 
| 261 | 
            -
            ```
         | 
| 262 | 
            -
             | 
| 263 | 
            -
            ## Pagination
         | 
| 264 | 
            -
             | 
| 265 | 
            -
            This gem designed to work with [pagy](https://github.com/ddnexus/pagy/).
         | 
| 266 | 
            -
             | 
| 267 | 
            -
            If you use collections and enable pagination then pagy will be called internally
         | 
| 268 | 
            -
            and the pagy metadata will be available as `pagination` on the collection.
         | 
| 269 | 
            -
             | 
| 270 | 
            -
            `Katalyst::Tables::PagyNavComponent` can be used to render the pagination links
         | 
| 271 | 
            -
            for a collection.
         | 
| 272 | 
            -
             | 
| 273 | 
            -
            ```erb
         | 
| 274 | 
            -
            <%= render Katalyst::Tables::PagyNavComponent.new(collection: @people) %>
         | 
| 205 | 
            +
            <%= table_with(collection:) %>
         | 
| 275 206 | 
             
            ```
         | 
| 276 207 |  | 
| 277 208 | 
             
            ## Extensions
         | 
| 278 209 |  | 
| 279 | 
            -
            The following extensions are available:
         | 
| 210 | 
            +
            The following extensions are available and activated by default:
         | 
| 280 211 |  | 
| 212 | 
            +
            * [Identifiable](docs/identifiable.md) - adds default dom ids to the table and data rows.
         | 
| 281 213 | 
             
            * [Orderable](docs/orderable.md) - adds bulk-update for 'ordinal' columns via dragging rows in the table.
         | 
| 214 | 
            +
            * [Pagination](docs/pagination.md) - handles paginating of data in the collection.
         | 
| 282 215 | 
             
            * [Selectable](docs/selectable.md) - adds bulk-action support for rows in the table.
         | 
| 216 | 
            +
            * [Sortable](docs/sortable.md) - table column headers that can be sorted will be wrapped in links.
         | 
| 217 | 
            +
            * [Customization](docs/customization.md) - customize the table and cell rendering.
         | 
| 283 218 |  | 
| 284 | 
            -
             | 
| 285 | 
            -
             | 
| 286 | 
            -
            A common pattern we use is to have a cell at the end of the table for actions. For example:
         | 
| 287 | 
            -
             | 
| 288 | 
            -
            ```html
         | 
| 289 | 
            -
            <table class="action-table">
         | 
| 290 | 
            -
              <thead>
         | 
| 291 | 
            -
                <tr>
         | 
| 292 | 
            -
                  <th>Name</th>
         | 
| 293 | 
            -
                  <th class="actions"></th>
         | 
| 294 | 
            -
                </tr>
         | 
| 295 | 
            -
              </thead>
         | 
| 296 | 
            -
              <tbody>
         | 
| 297 | 
            -
                <tr>
         | 
| 298 | 
            -
                  <td>Alice</td>
         | 
| 299 | 
            -
                  <td class="actions">
         | 
| 300 | 
            -
                    <a href="/people/1/edit">Edit</a>
         | 
| 301 | 
            -
                    <a href="/people/1" method="delete">Delete</a>
         | 
| 302 | 
            -
                  </td>
         | 
| 303 | 
            -
                </tr>
         | 
| 304 | 
            -
              </tbody>
         | 
| 305 | 
            -
            </table>
         | 
| 306 | 
            -
            ```
         | 
| 307 | 
            -
             | 
| 308 | 
            -
            You can write a custom component that helps generate this type of table by
         | 
| 309 | 
            -
            adding the required classes and adding helpers for generating the actions.
         | 
| 310 | 
            -
            This allows for a declarative table syntax, something like this:
         | 
| 311 | 
            -
             | 
| 312 | 
            -
            ```erb
         | 
| 313 | 
            -
            <%= render ActionTableComponent.new(collection:) do |row| %>
         | 
| 314 | 
            -
              <% row.cell :name %>
         | 
| 315 | 
            -
              <% row.actions do |cell| %>
         | 
| 316 | 
            -
                <%= cell.action "Edit", :edit %>
         | 
| 317 | 
            -
                <%= cell.action "Delete", :delete, method: :delete %>
         | 
| 318 | 
            -
              <% end %>
         | 
| 319 | 
            -
            <% end %>
         | 
| 320 | 
            -
            ```
         | 
| 321 | 
            -
             | 
| 322 | 
            -
            And the customized component:
         | 
| 323 | 
            -
             | 
| 324 | 
            -
            ```ruby
         | 
| 325 | 
            -
            class ActionTableComponent < Katalyst::TableComponent
         | 
| 326 | 
            -
             | 
| 327 | 
            -
              config.header_row = "ActionHeaderRow"
         | 
| 328 | 
            -
              config.body_row   = "ActionBodyRow"
         | 
| 329 | 
            -
              config.body_cell  = "ActionBodyCell"
         | 
| 330 | 
            -
              
         | 
| 331 | 
            -
              def default_html_attributes
         | 
| 332 | 
            -
                { class: "action-table" }
         | 
| 333 | 
            -
              end
         | 
| 334 | 
            -
             | 
| 335 | 
            -
              class ActionHeaderRow < Katalyst::Tables::HeaderRowComponent
         | 
| 336 | 
            -
                def actions(&block)
         | 
| 337 | 
            -
                  cell(:actions, class: "actions", label: "", &block)
         | 
| 338 | 
            -
                end
         | 
| 339 | 
            -
              end
         | 
| 340 | 
            -
             | 
| 341 | 
            -
              class ActionBodyRow < Katalyst::Tables::BodyRowComponent
         | 
| 342 | 
            -
                def actions(&block)
         | 
| 343 | 
            -
                  cell(:actions, class: "actions", &block)
         | 
| 344 | 
            -
                end
         | 
| 345 | 
            -
              end
         | 
| 346 | 
            -
             | 
| 347 | 
            -
              class ActionBodyCell < Katalyst::Tables::BodyCellComponent
         | 
| 348 | 
            -
                def action(label, href, **attrs)
         | 
| 349 | 
            -
                  content_tag(:a, label, href: href, **attrs)
         | 
| 350 | 
            -
                end
         | 
| 351 | 
            -
              end
         | 
| 352 | 
            -
            end
         | 
| 353 | 
            -
            ```
         | 
| 219 | 
            +
            You can disable extensions by altering the `Katalyst::Tables.config.component_extensions` before initialization.
         | 
| 354 220 |  | 
| 355 221 | 
             
            ## Development
         | 
| 356 222 |  | 
| @@ -1,41 +1,4 @@ | |
| 1 1 | 
             
            import { Controller } from '@hotwired/stimulus';
         | 
| 2 | 
            -
            import { Turbo } from '@hotwired/turbo-rails';
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            class TurboCollectionController extends Controller {
         | 
| 5 | 
            -
              static values = {
         | 
| 6 | 
            -
                query: String,
         | 
| 7 | 
            -
                sort: String,
         | 
| 8 | 
            -
              };
         | 
| 9 | 
            -
             | 
| 10 | 
            -
              queryValueChanged(query) {
         | 
| 11 | 
            -
                Turbo.navigator.history.replace(this.#url(query));
         | 
| 12 | 
            -
              }
         | 
| 13 | 
            -
             | 
| 14 | 
            -
              sortValueChanged(sort) {
         | 
| 15 | 
            -
                document.querySelectorAll(this.#sortSelector).forEach((input) => {
         | 
| 16 | 
            -
                  if (input) input.value = sort;
         | 
| 17 | 
            -
                });
         | 
| 18 | 
            -
              }
         | 
| 19 | 
            -
             | 
| 20 | 
            -
              get #sortSelector() {
         | 
| 21 | 
            -
                return "input[name='sort']";
         | 
| 22 | 
            -
              }
         | 
| 23 | 
            -
             | 
| 24 | 
            -
              #url(query) {
         | 
| 25 | 
            -
                const frame = this.element.closest("turbo-frame");
         | 
| 26 | 
            -
                let url;
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                if (frame) {
         | 
| 29 | 
            -
                  url = new URL(frame.baseURI);
         | 
| 30 | 
            -
                } else {
         | 
| 31 | 
            -
                  url = new URL(window.location.href);
         | 
| 32 | 
            -
                }
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                url.search = query;
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                return url;
         | 
| 37 | 
            -
              }
         | 
| 38 | 
            -
            }
         | 
| 39 2 |  | 
| 40 3 | 
             
            class OrderableRowController extends Controller {
         | 
| 41 4 | 
             
              static values = {
         | 
| @@ -85,6 +48,15 @@ class OrderableRowController extends Controller { | |
| 85 48 | 
             
                this.index = index;
         | 
| 86 49 | 
             
              }
         | 
| 87 50 |  | 
| 51 | 
            +
              /** Retrieve params for use in the form */
         | 
| 52 | 
            +
              params(scope) {
         | 
| 53 | 
            +
                const { id_name, id_value, index_name } = this.paramsValue;
         | 
| 54 | 
            +
                return [
         | 
| 55 | 
            +
                  { name: `${scope}[${id_value}][${id_name}]`, value: this.id },
         | 
| 56 | 
            +
                  { name: `${scope}[${id_value}][${index_name}]`, value: this.index },
         | 
| 57 | 
            +
                ];
         | 
| 58 | 
            +
              }
         | 
| 59 | 
            +
             | 
| 88 60 | 
             
              /**
         | 
| 89 61 | 
             
               * Restore any visual changes made during drag and remove the drag state.
         | 
| 90 62 | 
             
               */
         | 
| @@ -450,13 +422,15 @@ class DragState { | |
| 450 422 | 
             
            }
         | 
| 451 423 |  | 
| 452 424 | 
             
            class OrderableFormController extends Controller {
         | 
| 425 | 
            +
              static values = { scope: String };
         | 
| 426 | 
            +
             | 
| 453 427 | 
             
              add(item) {
         | 
| 454 | 
            -
                 | 
| 455 | 
            -
             | 
| 456 | 
            -
             | 
| 457 | 
            -
             | 
| 458 | 
            -
             | 
| 459 | 
            -
                );
         | 
| 428 | 
            +
                item.params(this.scopeValue).forEach(({ name, value }) => {
         | 
| 429 | 
            +
                  this.element.insertAdjacentHTML(
         | 
| 430 | 
            +
                    "beforeend",
         | 
| 431 | 
            +
                    `<input type="hidden" name="${name}" value="${value}" data-generated>`,
         | 
| 432 | 
            +
                  );
         | 
| 433 | 
            +
                });
         | 
| 460 434 | 
             
              }
         | 
| 461 435 |  | 
| 462 436 | 
             
              submit() {
         | 
| @@ -560,10 +534,6 @@ class SelectionItemController extends Controller { | |
| 560 534 | 
             
            }
         | 
| 561 535 |  | 
| 562 536 | 
             
            const Definitions = [
         | 
| 563 | 
            -
              {
         | 
| 564 | 
            -
                identifier: "tables--turbo--collection",
         | 
| 565 | 
            -
                controllerConstructor: TurboCollectionController,
         | 
| 566 | 
            -
              },
         | 
| 567 537 | 
             
              {
         | 
| 568 538 | 
             
                identifier: "tables--orderable--item",
         | 
| 569 539 | 
             
                controllerConstructor: OrderableRowController,
         | 
| @@ -1,41 +1,4 @@ | |
| 1 1 | 
             
            import { Controller } from '@hotwired/stimulus';
         | 
| 2 | 
            -
            import { Turbo } from '@hotwired/turbo-rails';
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            class TurboCollectionController extends Controller {
         | 
| 5 | 
            -
              static values = {
         | 
| 6 | 
            -
                query: String,
         | 
| 7 | 
            -
                sort: String,
         | 
| 8 | 
            -
              };
         | 
| 9 | 
            -
             | 
| 10 | 
            -
              queryValueChanged(query) {
         | 
| 11 | 
            -
                Turbo.navigator.history.replace(this.#url(query));
         | 
| 12 | 
            -
              }
         | 
| 13 | 
            -
             | 
| 14 | 
            -
              sortValueChanged(sort) {
         | 
| 15 | 
            -
                document.querySelectorAll(this.#sortSelector).forEach((input) => {
         | 
| 16 | 
            -
                  if (input) input.value = sort;
         | 
| 17 | 
            -
                });
         | 
| 18 | 
            -
              }
         | 
| 19 | 
            -
             | 
| 20 | 
            -
              get #sortSelector() {
         | 
| 21 | 
            -
                return "input[name='sort']";
         | 
| 22 | 
            -
              }
         | 
| 23 | 
            -
             | 
| 24 | 
            -
              #url(query) {
         | 
| 25 | 
            -
                const frame = this.element.closest("turbo-frame");
         | 
| 26 | 
            -
                let url;
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                if (frame) {
         | 
| 29 | 
            -
                  url = new URL(frame.baseURI);
         | 
| 30 | 
            -
                } else {
         | 
| 31 | 
            -
                  url = new URL(window.location.href);
         | 
| 32 | 
            -
                }
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                url.search = query;
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                return url;
         | 
| 37 | 
            -
              }
         | 
| 38 | 
            -
            }
         | 
| 39 2 |  | 
| 40 3 | 
             
            class OrderableRowController extends Controller {
         | 
| 41 4 | 
             
              static values = {
         | 
| @@ -85,6 +48,15 @@ class OrderableRowController extends Controller { | |
| 85 48 | 
             
                this.index = index;
         | 
| 86 49 | 
             
              }
         | 
| 87 50 |  | 
| 51 | 
            +
              /** Retrieve params for use in the form */
         | 
| 52 | 
            +
              params(scope) {
         | 
| 53 | 
            +
                const { id_name, id_value, index_name } = this.paramsValue;
         | 
| 54 | 
            +
                return [
         | 
| 55 | 
            +
                  { name: `${scope}[${id_value}][${id_name}]`, value: this.id },
         | 
| 56 | 
            +
                  { name: `${scope}[${id_value}][${index_name}]`, value: this.index },
         | 
| 57 | 
            +
                ];
         | 
| 58 | 
            +
              }
         | 
| 59 | 
            +
             | 
| 88 60 | 
             
              /**
         | 
| 89 61 | 
             
               * Restore any visual changes made during drag and remove the drag state.
         | 
| 90 62 | 
             
               */
         | 
| @@ -450,13 +422,15 @@ class DragState { | |
| 450 422 | 
             
            }
         | 
| 451 423 |  | 
| 452 424 | 
             
            class OrderableFormController extends Controller {
         | 
| 425 | 
            +
              static values = { scope: String };
         | 
| 426 | 
            +
             | 
| 453 427 | 
             
              add(item) {
         | 
| 454 | 
            -
                 | 
| 455 | 
            -
             | 
| 456 | 
            -
             | 
| 457 | 
            -
             | 
| 458 | 
            -
             | 
| 459 | 
            -
                );
         | 
| 428 | 
            +
                item.params(this.scopeValue).forEach(({ name, value }) => {
         | 
| 429 | 
            +
                  this.element.insertAdjacentHTML(
         | 
| 430 | 
            +
                    "beforeend",
         | 
| 431 | 
            +
                    `<input type="hidden" name="${name}" value="${value}" data-generated>`,
         | 
| 432 | 
            +
                  );
         | 
| 433 | 
            +
                });
         | 
| 460 434 | 
             
              }
         | 
| 461 435 |  | 
| 462 436 | 
             
              submit() {
         | 
| @@ -560,10 +534,6 @@ class SelectionItemController extends Controller { | |
| 560 534 | 
             
            }
         | 
| 561 535 |  | 
| 562 536 | 
             
            const Definitions = [
         | 
| 563 | 
            -
              {
         | 
| 564 | 
            -
                identifier: "tables--turbo--collection",
         | 
| 565 | 
            -
                controllerConstructor: TurboCollectionController,
         | 
| 566 | 
            -
              },
         | 
| 567 537 | 
             
              {
         | 
| 568 538 | 
             
                identifier: "tables--orderable--item",
         | 
| 569 539 | 
             
                controllerConstructor: OrderableRowController,
         | 
| @@ -1,2 +1,2 @@ | |
| 1 | 
            -
            import{Controller as t}from"@hotwired/stimulus"; | 
| 1 | 
            +
            import{Controller as t}from"@hotwired/stimulus";class e{constructor(t,e,s){this.cursorOffset=e.offsetY,this.initialPosition=e.target.offsetTop-t.offsetTop,this.targetId=s}updateCursor(t,e,s,i){this.listOffset=t.getBoundingClientRect().top;let r=s.clientY-this.listOffset-this.cursorOffset;this.#t(t,e,r,i)}updateScroll(t,e,s){const i=this.listOffset;this.listOffset=t.getBoundingClientRect().top;const r=i-this.listOffset,a=this.position+r;this.#t(t,e,a,s)}#t(t,e,s,i){s=Math.max(s,0),s=Math.min(s,t.offsetHeight-e.offsetHeight),this.position=s;i(s-this.initialPosition)}}class s extends t{static outlets=["tables--selection--form"];static values={params:Object,checked:Boolean};tablesSelectionFormOutletConnected(t){this.checkedValue=t.isSelected(this.id)}change(t){t.preventDefault(),this.checkedValue=this.tablesSelectionFormOutlet.toggle(this.id)}get id(){return this.paramsValue.id}checkedValueChanged(t){this.element.querySelector("input").checked=t}}const i=[{identifier:"tables--orderable--item",controllerConstructor:class extends t{static values={params:Object};connect(){var t;this.index=(t=this.row,Array.from(t.parentElement.children).indexOf(t))}paramsValueChanged(t){this.id=t.id_value}dragUpdate(t){this.dragOffset=t,this.row.style.position="relative",this.row.style.top=t+"px",this.row.style.zIndex="1",this.row.toggleAttribute("dragging",!0)}updateVisually(t){this.row.style.position="relative",this.row.style.top=this.row.offsetHeight*(t-this.dragIndex)+"px"}updateIndex(t){this.index=t}params(t){const{id_name:e,id_value:s,index_name:i}=this.paramsValue;return[{name:`${t}[${s}][${e}]`,value:this.id},{name:`${t}[${s}][${i}]`,value:this.index}]}reset(){delete this.dragOffset,this.row.removeAttribute("style"),this.row.removeAttribute("dragging")}get hasChanges(){return this.paramsValue.index_value!==this.index}get dragIndex(){return this.dragOffset&&0!==this.dragOffset?this.index+Math.round(this.dragOffset/this.row.offsetHeight):this.index}get comparisonIndex(){return this.dragOffset?this.dragIndex+(this.dragOffset>0?.5:-.5):this.index}get row(){return this.element.parentElement}}},{identifier:"tables--orderable--list",controllerConstructor:class extends t{static outlets=["tables--orderable--item","tables--orderable--form"];startDragging(t){this.dragState=t,document.addEventListener("mousemove",this.mousemove),document.addEventListener("mouseup",this.mouseup),window.addEventListener("scroll",this.scroll,!0),this.element.style.position="relative"}stopDragging(){const t=this.dragState;return delete this.dragState,document.removeEventListener("mousemove",this.mousemove),document.removeEventListener("mouseup",this.mouseup),window.removeEventListener("scroll",this.scroll,!0),this.element.removeAttribute("style"),this.tablesOrderableItemOutlets.forEach((t=>t.reset())),t}drop(){const t=this.dragItem;if(!t)return;const e=t.dragIndex,s=this.tablesOrderableItemOutlets[e];s&&(e<t.index?s.row.insertAdjacentElement("beforebegin",t.row):e>t.index&&s.row.insertAdjacentElement("afterend",t.row),this.tablesOrderableItemOutlets.forEach(((t,e)=>t.updateIndex(e))),this.commitChanges())}commitChanges(){this.tablesOrderableFormOutlet.clear(),this.tablesOrderableItemOutlets.forEach((t=>{t.hasChanges&&this.tablesOrderableFormOutlet.add(t)})),this.tablesOrderableFormOutlet.submit()}mousedown(t){if(this.isDragging)return;const s=this.#e(t.target);s&&(t.preventDefault(),this.startDragging(new e(this.element,t,s.id)),this.dragState.updateCursor(this.element,s.row,t,this.animate))}mousemove=t=>{this.isDragging&&(t.preventDefault(),this.ticking||(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateCursor(this.element,this.dragItem.row,t,this.animate)}))))};scroll=t=>{this.isDragging&&!this.ticking&&(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateScroll(this.element,this.dragItem.row,this.animate)})))};mouseup=t=>{this.isDragging&&(this.drop(),this.stopDragging(),this.tablesOrderableFormOutlets.forEach((t=>delete t.dragState)))};tablesOrderableFormOutletConnected(t,e){t.dragState&&this.startDragging(t.dragState)}tablesOrderableFormOutletDisconnected(t,e){this.isDragging&&(t.dragState=this.stopDragging())}animate=t=>{const e=this.dragItem;e.dragUpdate(t),this.#s.forEach(((t,s)=>{t!==e&&t.updateVisually(s)}))};get isDragging(){return!!this.dragState}get dragItem(){return this.isDragging?this.tablesOrderableItemOutlets.find((t=>t.id===this.dragState.targetId)):null}get#s(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#e(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{static values={scope:String};add(t){t.params(this.scopeValue).forEach((({name:t,value:e})=>{this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${t}" value="${e}" data-generated>`)}))}submit(){0!==this.inputs.length&&this.element.requestSubmit()}clear(){this.inputs.forEach((t=>t.remove()))}get inputs(){return this.element.querySelectorAll("input[data-generated]")}}},{identifier:"tables--selection--form",controllerConstructor:class extends t{static values={count:Number,primaryKey:{type:String,default:"id"}};static targets=["count","singular","plural"];connect(){this.countValue=this.inputs.length}toggle(t){const e=this.input(t);return e?e.remove():this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${this.primaryKeyValue}[]" value="${t}">`),this.countValue=this.inputs.length,!e}isSelected(t){return!!this.input(t)}get inputs(){return this.element.querySelectorAll(`input[name="${this.primaryKeyValue}[]"]`)}input(t){return this.element.querySelector(`input[name="${this.primaryKeyValue}[]"][value="${t}"]`)}countValueChanged(t){this.element.toggleAttribute("hidden",0===t),this.countTarget.textContent=t,this.singularTarget.toggleAttribute("hidden",1!==t),this.pluralTarget.toggleAttribute("hidden",1===t)}}},{identifier:"tables--selection--item",controllerConstructor:s}];export{i as default};
         | 
| 2 2 | 
             
            //# sourceMappingURL=tables.min.js.map
         |