Tables with Hotwire
Interactive table components with Turbo Frames & Stimulus
Sortable Table
Click column headers to sort • Powered by Stimulus
|
Name
|
Role
|
Email
|
Status |
|---|---|---|---|
| John Doe | Admin | [email protected] | Active |
| Jane Smith | Editor | [email protected] | Inactive |
| Mike Johnson | Viewer | [email protected] | Pending |
| Sarah Williams | Manager | [email protected] | Active |
| David Brown | Admin | [email protected] | Inactive |
<div data-controller="table">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 cursor-pointer"
data-action="click->table#sort"
data-column="name"
>
<div class="flex items-center space-x-1">
<span>Name</span>
<svg data-table-target="sortIcon">...</svg>
</div>
</th>
</tr>
</thead>
<tbody>
<tr data-table-target="row">
<td data-sort-value="John Doe">John Doe</td>
</tr>
</tbody>
</table>
</div>
Searchable Table with Live Filter
Client-side instant search • Powered by Stimulus
| Name | Role | Status | |
|---|---|---|---|
| John Doe | [email protected] | Admin | Active |
| Jane Smith | [email protected] | Editor | Inactive |
| Mike Johnson | [email protected] | Viewer | Pending |
| Sarah Williams | [email protected] | Manager | Active |
| David Brown | [email protected] | Admin | Inactive |
| Emily Davis | [email protected] | Editor | Pending |
| Chris Wilson | [email protected] | Viewer | Active |
| Lisa Anderson | [email protected] | Manager | Inactive |
No results found
Try adjusting your search query.
<div data-controller="table">
<input
type="text"
placeholder="Search..."
data-action="input->table#search"
>
<table>
<tbody>
<tr data-table-target="row">
<td>John Doe</td>
<td>[email protected]</td>
</tr>
</tbody>
</table>
<div data-empty-state class="hidden">
No results found
</div>
</div>
Bulk Actions & Row Selection
Select rows • Perform bulk operations • Powered by Stimulus
| Name | Role | Status | ||
|---|---|---|---|---|
|
JD
John Doe
|
[email protected] | Admin | Active | |
|
JS
Jane Smith
|
[email protected] | Editor | Inactive | |
|
MJ
Mike Johnson
|
[email protected] | Viewer | Pending | |
|
SW
Sarah Williams
|
[email protected] | Manager | Active | |
|
DB
David Brown
|
[email protected] | Admin | Inactive |
<div data-controller="table">
<!-- Bulk Actions Bar -->
<div data-table-target="bulkActions" class="hidden">
<span data-count>0</span> items selected
<button data-action="click->table#bulkExport">Export</button>
<button data-action="click->table#bulkDelete">Delete</button>
</div>
<table>
<thead>
<tr>
<th>
<input
type="checkbox"
data-table-target="checkbox selectAll"
data-action="change->table#toggleAll"
>
</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr data-id="1">
<td>
<input
type="checkbox"
data-table-target="checkbox"
data-action="change->table#toggleRow"
>
</td>
<td>John Doe</td>
</tr>
</tbody>
</table>
</div>
Expandable Rows
Click to expand & show additional details • Powered by Stimulus
| Product | Price | Stock | Status | |
|---|---|---|---|---|
| Wireless Headphones | $299.99 | 145 | In Stock | |
| Laptop Stand | $49.99 | 3 | Low Stock | |
| USB-C Cable | $19.99 | 0 | Out of Stock | |
| Mechanical Keyboard | $159.99 | 87 | In Stock | |
| Wireless Mouse | $79.99 | 234 | In Stock |
<div data-controller="table">
<table>
<tbody>
<tr>
<td>
<button data-action="click->table#toggleExpand">
<svg class="transition-transform">...</svg>
</button>
</td>
<td>Product Name</td>
</tr>
<!-- Expandable Content -->
<tr class="expandable-content hidden">
<td colspan="5">
Additional product details...
</td>
</tr>
</tbody>
</table>
</div>
Inline Editing with Turbo Frames
Edit cells directly • Auto-save • No page reload
| User | Role | Status | Actions | |
|---|---|---|---|---|
|
JD
John Doe
|
[email protected] | Admin | Active | Edit |
|
JS
Jane Smith
|
[email protected] | Editor | Inactive | Edit |
|
MJ
Mike Johnson
|
[email protected] | Viewer | Pending | Edit |
|
SW
Sarah Williams
|
[email protected] | Manager | Active | Edit |
<table>
<tbody>
<%= turbo_frame_tag "user_1" do %>
<tr>
<td><%= form.text_field :name %></td>
<td><%= form.text_field :email %></td>
<td>
<%= form.submit "Save",
data: { turbo_frame: "user_1" } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<!-- Controller -->
def inline_edit
render turbo_stream: turbo_stream.replace(
"user_#{params[:id]}",
partial: "user_row",
locals: { user: updated_user }
)
end
Live Search with Turbo Frames
Server-side filtering • Real-time results • Debounced requests
| Name | Role | Status | |
|---|---|---|---|
|
JD
John Doe
|
[email protected] | Admin | Active |
|
JS
Jane Smith
|
[email protected] | Editor | Inactive |
|
MJ
Mike Johnson
|
[email protected] | Viewer | Pending |
|
SW
Sarah Williams
|
[email protected] | Manager | Active |
|
DB
David Brown
|
[email protected] | Admin | Inactive |
|
ED
Emily Davis
|
[email protected] | Editor | Pending |
|
CW
Chris Wilson
|
[email protected] | Viewer | Active |
|
LA
Lisa Anderson
|
[email protected] | Manager | Inactive |
<%= turbo_frame_tag "search_results" do %>
<%= form_with url: filter_path,
data: {
turbo_frame: "search_results",
action: "input->debounce#search"
} do |f| %>
<%= f.text_field :query %>
<% end %>
<table id="users_table">
<!-- Table content -->
</table>
<% end %>
<!-- Controller -->
def filter
@users = User.where("name LIKE ?", "%#{params[:query]}%")
render turbo_stream: turbo_stream.replace(
"users_table",
partial: "users_table",
locals: { users: @users }
)
end
Pagination with Turbo Frames
Navigate pages • No full page reload • Smooth transitions
| Name | Role | Status | |
|---|---|---|---|
|
JD
John Doe
|
[email protected] | Admin | Active |
|
JS
Jane Smith
|
[email protected] | Editor | Inactive |
|
MJ
Mike Johnson
|
[email protected] | Viewer | Pending |
|
SW
Sarah Williams
|
[email protected] | Manager | Active |
|
DB
David Brown
|
[email protected] | Admin | Inactive |
Page 1 of 4
<%= turbo_frame_tag "paginated_users" do %>
<table>
<!-- Table content -->
</table>
<% end %>
<%= turbo_frame_tag "pagination" do %>
<div class="flex justify-between">
<%= link_to "Previous",
paginate_path(page: @page - 1),
data: { turbo_frame: "paginated_users" } %>
<%= link_to "Next",
paginate_path(page: @page + 1),
data: { turbo_frame: "paginated_users" } %>
</div>
<% end %>
<!-- Controller -->
def paginate
@users = User.page(params[:page])
render turbo_stream: [
turbo_stream.replace("paginated_users",
partial: "users_table"),
turbo_stream.replace("pagination",
partial: "pagination")
]
end
Lazy Loading Details with Turbo Frames
Click to load additional data • On-demand fetching • Loading states
| Transaction | Date | Amount | Details |
|---|---|---|---|
|
Payment from Customer A
ID: 001
|
Jan 15, 2025 | +$1200.00 |
|
|
|
|||
|
Subscription Renewal
ID: 002
|
Jan 16, 2025 | +$99.00 |
|
|
|
|||
|
Refund to Customer B
ID: 003
|
Jan 17, 2025 | $350.00 |
|
|
|
|||
|
Payment from Customer C
ID: 004
|
Jan 18, 2025 | +$2500.00 |
|
|
|
|||
|
Service Fee
ID: 005
|
Jan 19, 2025 | $45.00 |
|
|
|
|||
<table>
<tbody>
<tr>
<td>Transaction Info</td>
<td>
<%= turbo_frame_tag "details_1" do %>
<%= link_to "View Details",
details_path(1),
data: { turbo_frame: "details_1" } %>
<% end %>
</td>
</tr>
<tr>
<td colspan="4">
<%= turbo_frame_tag "details_1" do %>
<!-- Details loaded here -->
<% end %>
</td>
</tr>
</tbody>
</table>
<!-- Controller -->
def load_details
@transaction = Transaction.find(params[:id])
render partial: "transaction_details",
locals: { transaction: @transaction }
end
Responsive Mobile-Friendly Table
Adapts to small screens • Card layout on mobile • Powered by Tailwind
| Product | Category | Price | Stock | Status | Actions |
|---|---|---|---|---|---|
| Wireless Headphones | Electronics | $299.99 | 145 | In Stock | Edit Delete |
| Laptop Stand | Electronics | $49.99 | 3 | Low Stock | Edit Delete |
| USB-C Cable | Electronics | $19.99 | 0 | Out of Stock | Edit Delete |
| Mechanical Keyboard | Electronics | $159.99 | 87 | In Stock | Edit Delete |
| Wireless Mouse | Electronics | $79.99 | 234 | In Stock | Edit Delete |
<!-- Desktop Table View -->
<div class="hidden md:block">
<table class="min-w-full">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Wireless Headphones</td>
<td>$299.99</td>
<td><a href="#">Edit</a></td>
</tr>
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="md:hidden space-y-4">
<div class="border rounded-lg p-4">
<h3 class="font-semibold">Wireless Headphones</h3>
<div class="grid grid-cols-2 gap-3 mt-3">
<div>
<p class="text-xs text-gray-500">Price</p>
<p class="font-medium">$299.99</p>
</div>
</div>
<div class="flex justify-end mt-3 pt-3 border-t">
<a href="#">Edit</a>
</div>
</div>
</div>
Advanced Filters with Turbo Frames
Multiple filter options • Real-time updates • Powered by Turbo
Showing 8 results
| User | Role | Status | Actions | |
|---|---|---|---|---|
|
JD
John Doe
|
[email protected] | Admin | Active | |
|
JS
Jane Smith
|
[email protected] | Editor | Inactive | |
|
MJ
Mike Johnson
|
[email protected] | Viewer | Pending | |
|
SW
Sarah Williams
|
[email protected] | Manager | Active | |
|
DB
David Brown
|
[email protected] | Admin | Inactive | |
|
ED
Emily Davis
|
[email protected] | Editor | Pending | |
|
CW
Chris Wilson
|
[email protected] | Viewer | Active | |
|
LA
Lisa Anderson
|
[email protected] | Manager | Inactive |
No matching results
Try adjusting your filters or search terms.
<div data-controller="table">
<!-- Filters -->
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
<input
type="text"
data-action="input->table#search"
placeholder="Search..."
>
<select data-action="change->table#search">
<option value="">All Roles</option>
<option value="admin">Admin</option>
</select>
</div>
<!-- Results -->
<table>
<tbody>
<tr data-table-target="row">
<td>John Doe</td>
<td>Admin</td>
</tr>
</tbody>
</table>
<!-- Empty State -->
<div data-empty-state class="hidden">
No matching results
</div>
</div>