# Katalyst::Tables
Tools for building HTML tables from ActiveRecord collections.
## Installation
Add this line to your application's Gemfile:
```ruby
gem "katalyst-tables"
```
And then execute:
$ bundle install
## Usage
This gem provides two entry points: `Frontend` for use in your views, and `Backend` for use in your controllers. The backend
entry point is optional, as it's only required if you want to support sorting by column headers.
### Frontend
Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
```erb
<%= table_with collection: @people do |row, person| %>
<%= row.cell :name %>
<%= row.cell :email %>
<%= row.cell :actions do %>
<%= link_to "Edit", person %>
<% end %>
<% end %>
```
The table builder will call your block once per row and accumulate the cells you generate into rows:
```html
Name |
Email |
Actions |
Alice |
alice@acme.org |
Edit |
Bob |
bob@acme.org |
Edit |
```
### Options
You can customise the options passed to the table, rows, and cells.
Tables support options via the call to `table_with`, similar to `form_with`.
```erb
<%= table_with collection: @people, id: "people-table" do |row, person| %>
...
<% end %>
```
Cells support the same approach:
```erb
<%= row.cell :name, class: "name" %>
```
Rows do not get called directly, so instead you can call `options` on the row builder to customize the row tag
generation.
```erb
<%= table_with collection: @people, id: "people-table" do |row, person| %>
<% row.options data: { id: person.id } if row.body? %>
...
<% end %>
```
Note: because the row builder gets called to generate the header row, you may need to guard calls that access the
`person` directly as shown in the previous example. You could also check whether `person` is present.
#### Headers
`table_builder` will automatically generate a header row for you by calling your block with no object. During this
iteration, `row.header?` is true, `row.body?` is false, and the object (`person`) is nil.
All cells generated in the table header iteration will automatically be header cells, but you can also make header cells
in your body rows by passing `heading: true` when you generate the cell.
```erb
<% row.cell :id, heading: true %>
```
The table header cells default to showing the capitalized column name, but you can customize this in one of two ways:
* Set the value inline
```erb
<% row.cell :id, label: "ID" %>
```
* Define a translation for the attribute
```yml
# en.yml
activerecord:
attributes:
person:
id: "ID"
```
Note: if the cell is given a block, it is not called during the header pass. This
is because the block is assumed to be for generating data for the body, not the
header. We suggest you set `label` instead.
#### Cell values
If you do not provide a value when you call the cell builder, the attribute you
provide will be retrieved from the current item and the result will be rendered in
the table cell. This is often all you need to do, but if you do want to customise
the value you can pass a block instead:
```erb
<%= row.cell :status do %>
<%= person.password.present? ? "Active" : "Invited" %>
<% end %>
```
In the context of the block you have access the cell builder if you simply
want to extend the default behaviour:
```erb
<%= row.cell :status do |cell| %>
<%= link_to cell.value, person %>
<% end %>
```
You can also call `options` on the cell builder, similar to the row builder, but
please note that this will replace any options passed to the cell as arguments.
### Sort
The major reason why you should use this gem, apart the convenience of the
builder, is for adding efficient and simple column sorting to your tables.
Start by including the backend in your controller(s):
```ruby
include Katalyst::Tables::Backend
```
Now, in your controller index actions, you can sort your active record
collections based on the `sort` param which is appended to the current URL as a
get parameter when a user clicks on a column header.
Building on our example from earlier:
```ruby
class PeopleController < ApplicationController
include Katalyst::Tables::Backend
def index
@people = People.all
@sort, @people = table_sort(@people) # sort
end
end
```
You then add the sort form object to your view so that it can add column header
links and show the current sort state:
```erb
<%= table_with collection: @people, sort: @sort do |row, person| %>
<%= row.cell :name %>
<%= row.cell :email %>
<%= row.cell :actions do %>
<%= link_to "Edit", person %>
<% end %>
<% end %>
```
That's it! Any column that corresponds to an ActiveRecord attribute will now be
automatically sortable in the frontend.
You can also add sorting to non-attribute columns by defining a scope in your
model:
```
scope :order_by_status, ->(direction) { ... }
```
Finally, you can use sort with a collection that is already ordered, but please
note that the backend will call `reorder` if the user provides a sort option. If
you want to provide a tie-breaker default ordering, the best way to do so is after
calling `table_sort`.
You may also want to whitelist the `sort` param if you encounter strong param warnings.
### Pagination
This gem designed to work with [pagy](https://github.com/ddnexus/pagy/).
```ruby
def index
@people = People.all
@sort, @people = table_sort(@people) # sort
@pagy, @people = pagy(@people) # then paginate
end
```
```erb
<%= table_with collection: @people, sort: @sort do |row, person| %>
<%= row.cell :name %>
<%= row.cell :email %>
<%= row.cell :actions do %>
<%= link_to "Edit", person %>
<% end %>
<% end %>
<%== pagy_nav(@pagy) %>
```
### Customization
A common pattern we use is to have a cell at the end of the table for actions. For example:
```html
```
You can write a custom builder that helps generate this type of table by adding the required classes and adding helpers
for generating the actions. This allows for a declarative table syntax, something like this:
```erb
<%= table_with(collection: collection, component: ActionTableComponent) do |row| %>
<% row.cell :name %>
<% row.actions do |cell| %>
<%= cell.action "Edit", :edit %>
<%= cell.action "Delete", :delete, method: :delete %>
<% end %>
<% end %>
```
And the customized component:
```ruby
class ActionTableComponent < Katalyst::TableComponent
config.header_row = "ActionHeaderRow"
config.body_row = "ActionBodyRow"
config.body_cell = "ActionBodyCell"
def call
options(class: "action-table")
super
end
class ActionHeaderRow < Katalyst::Tables::HeaderRowComponent
def actions(&block)
cell(:actions, class: "actions", label: "", &block)
end
end
class ActionBodyRow < Katalyst::Tables::BodyRowComponent
def actions(&block)
cell(:actions, class: "actions", &block)
end
end
class ActionBodyCell < Katalyst::Tables::BodyCellComponent
def action(label, href, **opts)
content_tag :a, label, { href: href }.merge(opts)
end
end
end
```
If you have a table component you want to reuse, you can set it as a default for some or all of your controllers:
```html
class ApplicationController < ActiveController::Base
default_table_component ActionTableComponent
end
```
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
To install this gem onto your local machine, run `bundle exec rake install`.
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/katalyst-tables.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).