x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<div class="p-8"><turbo-frame id="users-table"> <div data-controller="midwest-table" class="midwest-table-wrapper"> <div class="midwest-table-loading-overlay" aria-hidden="true"> <div class="midwest-loading-icon" role="img" aria-label="Loading" decorative="true"> <div class="midwest-loading-icon-inner"> <!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL --> <svg viewbox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="Midwest-LoadingIcon__Svg" focusable="false" aria-hidden="true"> <circle cx="15" cy="15" r="15"> <animate attributename="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcmode="linear" repeatcount="indefinite"></animate> <animate attributename="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcmode="linear" repeatcount="indefinite"></animate> </circle> <circle cx="60" cy="15" r="9" fill-opacity="0.3"> <animate attributename="r" from="9" to="9" begin="0s" dur="0.8s" values="9;15;9" calcmode="linear" repeatcount="indefinite"></animate> <animate attributename="fill-opacity" from="0.5" to="0.5" begin="0s" dur="0.8s" values=".5;1;.5" calcmode="linear" repeatcount="indefinite"></animate> </circle> <circle cx="105" cy="15" r="15"> <animate attributename="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcmode="linear" repeatcount="indefinite"></animate> <animate attributename="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcmode="linear" repeatcount="indefinite"></animate> </circle> </svg> </div> </div> </div> <div class="midwest-table-scroll"> <table class="midwest-table"> <caption class="sr-only">Users</caption> <thead class="midwest-table-head"> <tr> <th scope="col" class="midwest-table-th align-start sortable sorted" aria-sort="ascending"> <a class="midwest-table-sort-link" href="?sort=name&dir=desc&per_page=25"> Name <span class="midwest-table-sort-icon" aria-hidden="true"></span> </a> </th> <th scope="col" class="midwest-table-th align-start sortable" aria-sort="none"> <a class="midwest-table-sort-link" href="?sort=email&dir=asc&per_page=25"> Email <span class="midwest-table-sort-icon" aria-hidden="true"></span> </a> </th> <th scope="col" class="midwest-table-th align-start sortable" aria-sort="none"> <a class="midwest-table-sort-link" href="?sort=role&dir=asc&per_page=25"> Role <span class="midwest-table-sort-icon" aria-hidden="true"></span> </a> </th> <th scope="col" class="midwest-table-th align-start sortable" aria-sort="none"> <a class="midwest-table-sort-link" href="?sort=created_at&dir=asc&per_page=25"> Created At <span class="midwest-table-sort-icon" aria-hidden="true"></span> </a> </th> </tr> </thead> <tbody class="midwest-table-body"> <tr class="midwest-table-row"> <td class="midwest-table-td align-start"> Alice Johnson </td> <td class="midwest-table-td align-start"> alice@example.com </td> <td class="midwest-table-td align-start"> Engineer </td> <td class="midwest-table-td align-start"> Jan 2022 </td> </tr> <tr class="midwest-table-row"> <td class="midwest-table-td align-start"> Bob Smith </td> <td class="midwest-table-td align-start"> bob@example.com </td> <td class="midwest-table-td align-start"> Designer </td> <td class="midwest-table-td align-start"> Mar 2021 </td> </tr> <tr class="midwest-table-row"> <td class="midwest-table-td align-start"> Carol White </td> <td class="midwest-table-td align-start"> carol@example.com </td> <td class="midwest-table-td align-start"> Manager </td> <td class="midwest-table-td align-start"> Nov 2020 </td> </tr> <tr class="midwest-table-row"> <td class="midwest-table-td align-start"> David Lee </td> <td class="midwest-table-td align-start"> david@example.com </td> <td class="midwest-table-td align-start"> Engineer </td> <td class="midwest-table-td align-start"> Jul 2023 </td> </tr> <tr class="midwest-table-row"> <td class="midwest-table-td align-start"> Eve Martinez </td> <td class="midwest-table-td align-start"> eve@example.com </td> <td class="midwest-table-td align-start"> Designer </td> <td class="midwest-table-td align-start"> Feb 2024 </td> </tr> </tbody> </table> </div> <div class="midwest-table-pagination"> <nav class="midwest-pagination variant-default" aria-label="Pagination" data-controller="midwest-pagination" data-midwest-pagination-url-template-value="?sort=name&dir=asc&page=__PAGE__&per_page=25" data-midwest-pagination-turbo-frame-id-value="users-table"> <div class="midwest-pagination-controls"> <span class="midwest-pagination-btn is-prev is-disabled" aria-disabled="true" aria-label="Previous page"> <div class="midwest-icon " aria-hidden="true"> <div class="midwest-icon-inner"> <svg xmlns="http://www.w3.org/2000/svg" class="Midwest-Icon__Svg" viewbox="0 0 512 512" focusable="false" role="img" aria-label="Arrow back"><title>Arrow back</title> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M244 400L100 256l144-144M120 256h292"></path></svg> </div> </div> </span> <span class="midwest-pagination-btn is-current" aria-current="page">1</span> <a class="midwest-pagination-btn" aria-label="Go to page 2" href="?sort=name&dir=asc&page=2&per_page=25">2</a> <a class="midwest-pagination-btn" aria-label="Go to page 3" href="?sort=name&dir=asc&page=3&per_page=25">3</a> <span class="midwest-pagination-ellipsis" aria-hidden="true">…</span> <a class="midwest-pagination-btn" aria-label="Go to page 12" href="?sort=name&dir=asc&page=12&per_page=25">12</a> <a class="midwest-pagination-btn is-next" aria-label="Go to next page" href="?sort=name&dir=asc&page=2&per_page=25"> <div class="midwest-icon " aria-hidden="true"> <div class="midwest-icon-inner"> <svg xmlns="http://www.w3.org/2000/svg" class="Midwest-Icon__Svg" viewbox="0 0 512 512" focusable="false" role="img" aria-label="Arrow forward"><title>Arrow forward</title> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M268 112l144 144-144 144M392 256H100"></path></svg> </div> </div> </a> </div> <div class="midwest-pagination-footer"> <form class="midwest-pagination-jump" data-action="submit->midwest-pagination#jump"> <label for="page-jump-f610">Page</label> <input type="number" id="page-jump-f610" autocomplete="off" min="1" max="12" value="1" data-midwest-pagination-target="jumpInput"> <button type="submit">Go</button> </form> <div class="midwest-pagination-per-page"> <label for="per-page-f610">Rows per page</label> <select id="per-page-f610" data-action="change->midwest-pagination#navigate"> <option value="?sort=name&dir=asc&per_page=10" > 10 </option> <option value="?sort=name&dir=asc&per_page=25" selected> 25 </option> <option value="?sort=name&dir=asc&per_page=50" > 50 </option> </select> </div> </div> </nav> </div> </div></turbo-frame></div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<% # -------------------------------------------------------------------------- # Midwest::TurboTable — controller concern usage guide # # 1. Include the concern and call `turbo_table` in your controller: # # class UsersController < ApplicationController # include Midwest::TurboTable # # turbo_table :users, # default_sort: :name, # default_direction: :asc, # allowed_sorts: %i[name email role created_at], # default_per_page: 25, # per_page_options: [10, 25, 50] # # def index # @users = User.order(table_order).page(table_page).per(table_per_page) # end # end # # 2. In routes, no changes needed — sort, page, and per_page are query params. # # 3. In your view, wrap the table in turbo_frame_tag using the helper ID. # Pass turbo_frame_id: midwest_table_frame_id to midwest_pagination so # per-page and jump-to controls update only the frame, not the full page. # -------------------------------------------------------------------------- # Simulated state for the preview (normally comes from the concern helpers) current_sort = :name current_direction = :asc current_page = 1 current_per_page = 25 frame_id = 'users-table' sort_url = ->(col) { dir = (current_sort == col && current_direction == :asc) ? 'desc' : 'asc' "?sort=#{col}&dir=#{dir}&per_page=#{current_per_page}" } page_url = ->(p) { "?sort=#{current_sort}&dir=#{current_direction}&page=#{p}&per_page=#{current_per_page}" } per_page_url = ->(n) { "?sort=#{current_sort}&dir=#{current_direction}&per_page=#{n}" } row_class = Struct.new(:id, :name, :email, :role, :created_at, keyword_init: true) rows = [ row_class.new(id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', created_at: 'Jan 2022'), row_class.new(id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', created_at: 'Mar 2021'), row_class.new(id: 3, name: 'Carol White', email: 'carol@example.com', role: 'Manager', created_at: 'Nov 2020'), row_class.new(id: 4, name: 'David Lee', email: 'david@example.com', role: 'Engineer', created_at: 'Jul 2023'), row_class.new(id: 5, name: 'Eve Martinez', email: 'eve@example.com', role: 'Designer', created_at: 'Feb 2024'), ]%><div class="p-8"> <%# In a real view this would be: turbo_frame_tag midwest_table_frame_id %> <%= turbo_frame_tag frame_id do %> <%= midwest_table(collection: rows, caption: 'Users') do |t| %> <% t.with_column 'Name', key: :name, sortable: true, sorted: current_sort == :name, sort_direction: current_direction, sort_url: sort_url.call(:name) %> <% t.with_column 'Email', key: :email, sortable: true, sorted: current_sort == :email, sort_direction: current_direction, sort_url: sort_url.call(:email) %> <% t.with_column 'Role', key: :role, sortable: true, sorted: current_sort == :role, sort_direction: current_direction, sort_url: sort_url.call(:role) %> <% t.with_column 'Created At', key: :created_at, sortable: true, sorted: current_sort == :created_at, sort_direction: current_direction, sort_url: sort_url.call(:created_at) %> <% t.with_pagination do %> <%= midwest_pagination( current_page: current_page, total_pages: 12, per_page: current_per_page, per_page_options: [10, 25, 50], page_url: page_url, per_page_url: per_page_url, turbo_frame_id: frame_id, show_per_page: true, show_jump_to: true ) %> <% end %> <% end %> <% end %></div>Turbo Frame
How to use Midwest::TurboTable for async sorting and pagination without
full-page reloads. Wrap the table in turbo_frame_tag and wire sort/page
URLs through the concern helpers. The loading overlay appears automatically
while the frame is fetching via the turbo-frame[aria-busy] CSS hook.
No params configured.
No assets to display.
Add a .js or .css file alongside the component to see it here.
Figma embed for Table/turbo_frame
Annotate a preview to embed its Figma design here:
@figma "https://figma.com/..."