Tabular Data

With searching, sorting, and pagination

100 results found

Name ↓ Stars Price Category
1480 Shakes ★★★★★ $ Greek
14 Cafe ★★★ $$$ Vegan
32 House ★★★★★ $$$ Vegetarian
391 Brasserie ★★ $$ Bakery
47 Coffee $$$ Korean
56 Coffee ★★★★ $ Vegetarian
582 Bakery ★★ $$ Healthy
7276 Juice Bar ★★★★ $ African
937 Sushi $ Thai
93 Juice Bar ★★★★ $ Vegan

Explanation

Sophsticated management of tabular data using declarative StimulusReflex attributes. No custom JavaScript was written for this demo.

  • No need for expensive third party libraries.
  • No need to send heavy JavaScript payloads to the client.
  • No need to build an API layer that returns JSON.
  • A little server-side HTML rendering and a Reflex is all it takes.

Code (157 LOC)

ERB (59 LOC)
app/views/tabulars/_demo.html.erb (6 LOC)
<div class="form-row mb-4">
<div class="col-6">
<input type="text" placeholder="Search..." class="form-control form-control-lg" data-reflex="debounced:input->TabularReflex#search">
</div>
</div>
<%= render partial: "tabulars/search_results" %>
app/views/tabulars/_search_results.html.erb (38 LOC)
<div id="search-results">
<p class="lead"><%= @pagy.count %> results found</p>
<% if @pagy.count > 0 %>
<table class="table table-striped table-bordered">
<thead class="bg-primary">
<tr>
<th style="width:25%">
<%= link_to "Name #{arrow :name}", "#", class: column_css(:name),
data: { reflex: "click->TabularReflex#order", column_name: :name, direction: direction } %>
</th>
<th style="width:25%">
<%= link_to "Stars #{arrow :stars}", "#", class: column_css(:stars),
data: { reflex: "click->TabularReflex#order", column_name: :stars, direction: direction } %>
</th>
<th style="width:25%">
<%= link_to "Price #{arrow :price}", "#", class: column_css(:price),
data: { reflex: "click->TabularReflex#order", column_name: :price, direction: direction } %>
</th>
<th style="width:25%">
<%= link_to "Category #{arrow :category}", "#", class: column_css(:category),
data: { reflex: "click->TabularReflex#order", column_name: :category, direction: direction } %>
</th>
</tr>
</thead>
<tbody>
<% @restaurants.each do |restaurant| %>
<tr>
<td><%= restaurant.name %></td>
<td><%= "★" * restaurant.stars %></td>
<td><%= "$" * restaurant.price %></td>
<td><%= restaurant.category %></td>
</tr>
<% end %>
</tbody>
</table>
<%= render "/tabulars/paginator" if @pagy.pages > 1 %>
<% end %>
</div>
app/views/tabulars/_paginator.html.erb (15 LOC)
<nav>
<ul class="pagination">
<li class="page-item"><a href="#" class="page-link" data-reflex="click->TabularReflex#paginate" data-page="<%= prev_page %>"></a></li>
<% @pagy.series.each do |item| %>
<% if item == :gap %>
<li class="page-item disabled"><a class="page-link">...</a></li>
<% else %>
<li class="page-item <%= "active" if item.is_a?(String) %>">
<a href="#" class="page-link" data-reflex="click->TabularReflex#paginate" data-page="<%= item %>"><%= item %></a>
</li>
<% end %>
<% end %>
<li class="page-item"><a href="#" class="page-link" data-reflex="click->TabularReflex#paginate" data-page="<%= next_page %>"></a></li>
</ul>
</nav>
Ruby (98 LOC)
app/helpers/tabulars_helper.rb (23 LOC)
module TabularsHelper
include Pagy::Frontend
def column_css(column_name)
return "text-light selected" if column_name.to_s == @order_by
"text-light"
end
def arrow(column_name)
return if column_name.to_s != @order_by
@direction == "desc" ? "↑" : "↓"
end
def direction
@direction == "asc" ? "desc" : "asc"
end
def pagy_get_params(params)
params.merge query: @query, order_by: @order_by, direction: @direction
end
def prev_page
@pagy.prev || 1
end
def next_page
@pagy.next || @pagy.last
end
end
app/models/restaurant.rb (10 LOC)
class Restaurant < ApplicationRecord
scope :search, ->(query) {
query = sanitize_sql_like(query)
where(arel_table[:name].matches("%#{query}%"))
.or(where(arel_table[:category].matches("%#{query}%")))
}
scope :category, ->(category) { where(category: category) if category.present? }
scope :stars, ->(stars) { where(stars: stars..) if stars.present? }
scope :price, ->(price) { where(price: price.split(",")) if price.present? }
end
app/controllers/tabulars_controller.rb (6 LOC)
class TabularsController < ApplicationController
include Tabular
def show
prepare_variables
end
end
app/reflexes/tabular_reflex.rb (35 LOC)
class TabularReflex < ApplicationReflex
include Tabular
def search
params[:query] = element[:value].strip
update_client
end
def order
params[:order_by] = element.dataset["column-name"]
params[:direction] = element.dataset["direction"]
update_client
end
def paginate
params[:page] = element.dataset[:page].to_i
update_client
end
private
def update_client
prepare_variables
assigns = {
query: @query,
order_by: @order_by,
direction: @direction,
page: @page,
pagy: @pagy,
restaurants: @restaurants
}
uri = URI.parse([request.base_url, request.path].join)
uri.query = assigns.except(:restaurants, :pagy).to_query
morph :nothing
cable_ready
.inner_html(selector: "#search-results", html: render(partial: "tabulars/search_results", assigns: assigns))
.push_state(url: uri.to_s)
.broadcast
end
end
app/controllers/concerns/tabular.rb (24 LOC)
module Tabular
extend ActiveSupport::Concern
include Pagy::Backend
protected
def prepare_variables
@query = params[:query]
@order_by = permitted_column_name(params[:order_by])
@direction = permitted_direction(params[:direction])
restaurants = Restaurant.order(@order_by => @direction)
restaurants = restaurants.search(@query) if @query.present?
page_count = (restaurants.count / Pagy::VARS[:items].to_f).ceil
@page = (params[:page] || 1).to_i
@page = page_count if @page > page_count
@page = 1 if @page < 1
@pagy, @restaurants = pagy(restaurants, page: @page)
end
private
def permitted_column_name(column_name)
%w[name stars price category].find { |permitted| column_name == permitted } || "name"
end
def permitted_direction(direction)
%w[asc desc].find { |permitted| direction == permitted } || "asc"
end
end