crud_components 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/Gemfile +23 -0
- data/LICENSE +21 -0
- data/README.md +511 -0
- data/RELEASING.md +44 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/crud_components.css +35 -0
- data/app/views/crud_components/_action_button.html.erb +11 -0
- data/app/views/crud_components/_actions.html.erb +12 -0
- data/app/views/crud_components/_column_header.html.erb +24 -0
- data/app/views/crud_components/_column_picker.html.erb +66 -0
- data/app/views/crud_components/_filter.html.erb +34 -0
- data/app/views/crud_components/_form.html.erb +30 -0
- data/app/views/crud_components/_pager.html.erb +41 -0
- data/app/views/crud_components/_record.html.erb +15 -0
- data/app/views/crud_components/_row.html.erb +26 -0
- data/app/views/crud_components/_selection_action.html.erb +14 -0
- data/app/views/crud_components/_sort_link.html.erb +17 -0
- data/app/views/crud_components/_toolbar.html.erb +50 -0
- data/app/views/crud_components/fields/_asciidoc.html.erb +8 -0
- data/app/views/crud_components/fields/_association.html.erb +13 -0
- data/app/views/crud_components/fields/_association_list.html.erb +24 -0
- data/app/views/crud_components/fields/_attachment.html.erb +16 -0
- data/app/views/crud_components/fields/_attachment_thumb.html.erb +17 -0
- data/app/views/crud_components/fields/_boolean.html.erb +13 -0
- data/app/views/crud_components/fields/_date.html.erb +6 -0
- data/app/views/crud_components/fields/_datetime.html.erb +6 -0
- data/app/views/crud_components/fields/_email.html.erb +7 -0
- data/app/views/crud_components/fields/_enum.html.erb +14 -0
- data/app/views/crud_components/fields/_json.html.erb +10 -0
- data/app/views/crud_components/fields/_markdown.html.erb +9 -0
- data/app/views/crud_components/fields/_number.html.erb +8 -0
- data/app/views/crud_components/fields/_string.html.erb +8 -0
- data/app/views/crud_components/fields/_text.html.erb +9 -0
- data/app/views/crud_components/fields/_url.html.erb +11 -0
- data/app/views/crud_components/filters/_boolean.html.erb +12 -0
- data/app/views/crud_components/filters/_date_range.html.erb +11 -0
- data/app/views/crud_components/filters/_number_range.html.erb +13 -0
- data/app/views/crud_components/filters/_select.html.erb +8 -0
- data/app/views/crud_components/filters/_text.html.erb +5 -0
- data/app/views/crud_components/form_fields/_belongs_to.html.erb +3 -0
- data/app/views/crud_components/form_fields/_boolean.html.erb +12 -0
- data/app/views/crud_components/form_fields/_date.html.erb +2 -0
- data/app/views/crud_components/form_fields/_datetime.html.erb +2 -0
- data/app/views/crud_components/form_fields/_enum.html.erb +8 -0
- data/app/views/crud_components/form_fields/_file.html.erb +47 -0
- data/app/views/crud_components/form_fields/_habtm.html.erb +5 -0
- data/app/views/crud_components/form_fields/_number.html.erb +2 -0
- data/app/views/crud_components/form_fields/_string.html.erb +3 -0
- data/app/views/crud_components/form_fields/_text.html.erb +2 -0
- data/app/views/crud_components/layouts/_table.html.erb +143 -0
- data/config/locales/crud_components.de.yml +39 -0
- data/config/locales/crud_components.en.yml +40 -0
- data/crud_components.gemspec +48 -0
- data/docs/extending.md +308 -0
- data/docs/fields.md +442 -0
- data/docs/forms.md +253 -0
- data/docs/performance.md +90 -0
- data/docs/security.md +139 -0
- data/docs/views.md +405 -0
- data/lib/crud_components/action.rb +85 -0
- data/lib/crud_components/builder.rb +246 -0
- data/lib/crud_components/config.rb +128 -0
- data/lib/crud_components/dynamic_column.rb +68 -0
- data/lib/crud_components/engine.rb +25 -0
- data/lib/crud_components/errors.rb +9 -0
- data/lib/crud_components/fields/attachment_field.rb +22 -0
- data/lib/crud_components/fields/base.rb +260 -0
- data/lib/crud_components/fields/belongs_to_field.rb +91 -0
- data/lib/crud_components/fields/boolean_field.rb +31 -0
- data/lib/crud_components/fields/computed_field.rb +34 -0
- data/lib/crud_components/fields/date_field.rb +51 -0
- data/lib/crud_components/fields/dynamic_field.rb +44 -0
- data/lib/crud_components/fields/enum_field.rb +40 -0
- data/lib/crud_components/fields/has_many_field.rb +50 -0
- data/lib/crud_components/fields/json_field.rb +10 -0
- data/lib/crud_components/fields/numeric_field.rb +31 -0
- data/lib/crud_components/fields/path_field.rb +327 -0
- data/lib/crud_components/fields/string_field.rb +41 -0
- data/lib/crud_components/fields/text_field.rb +9 -0
- data/lib/crud_components/fieldset.rb +38 -0
- data/lib/crud_components/helpers.rb +259 -0
- data/lib/crud_components/like_spec.rb +113 -0
- data/lib/crud_components/markup.rb +36 -0
- data/lib/crud_components/model.rb +33 -0
- data/lib/crud_components/permission_context.rb +62 -0
- data/lib/crud_components/presenters/actions.rb +51 -0
- data/lib/crud_components/presenters/base.rb +95 -0
- data/lib/crud_components/presenters/cell_context.rb +28 -0
- data/lib/crud_components/presenters/cells.rb +160 -0
- data/lib/crud_components/presenters/collection.rb +498 -0
- data/lib/crud_components/presenters/column_selection.rb +91 -0
- data/lib/crud_components/presenters/filter.rb +38 -0
- data/lib/crud_components/presenters/form.rb +57 -0
- data/lib/crud_components/presenters/record.rb +57 -0
- data/lib/crud_components/query.rb +110 -0
- data/lib/crud_components/route_resolver.rb +123 -0
- data/lib/crud_components/structure.rb +343 -0
- data/lib/crud_components/version.rb +3 -0
- data/lib/crud_components/where_like.rb +13 -0
- data/lib/crud_components.rb +160 -0
- data/lib/generators/crud_components/install/install_generator.rb +43 -0
- data/lib/generators/crud_components/install/templates/crud_columns_controller.js +76 -0
- data/lib/generators/crud_components/install/templates/crud_filter_controller.js +32 -0
- data/lib/generators/crud_components/install/templates/crud_multiselect_controller.js +70 -0
- data/lib/generators/crud_components/install/templates/crud_select_controller.js +35 -0
- data/lib/generators/crud_components/install/templates/initializer.rb +56 -0
- data/lib/generators/crud_components/views/views_generator.rb +14 -0
- metadata +209 -0
data/docs/extending.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Extending & styling
|
|
2
|
+
|
|
3
|
+
The gem ships **no CSS** but is designed for **Bootstrap 5 by default**, and is built to
|
|
4
|
+
drop into an app that has its own design — even one on a CSS framework that works nothing
|
|
5
|
+
like Bootstrap. The class map and the partials cover overrides: swap cosmetic classes in
|
|
6
|
+
the map, override individual partials where the structure differs. For a whole different
|
|
7
|
+
framework, PRs are welcome — open an issue to talk it through first. Two facts make that
|
|
8
|
+
practical:
|
|
9
|
+
|
|
10
|
+
1. **Everything visual is a partial**, and a file at the same path in your app wins
|
|
11
|
+
(standard Rails view-path precedence — the same mechanism as Devise or Kaminari
|
|
12
|
+
views). That one rule is the entire extension API.
|
|
13
|
+
2. **The surfaces are decomposed**, so overriding one piece doesn't mean reimplementing
|
|
14
|
+
the others — you reuse the presenter and the sub-partials.
|
|
15
|
+
|
|
16
|
+
## How far do you need to go?
|
|
17
|
+
|
|
18
|
+
| You want to… | Do this | Reach for |
|
|
19
|
+
| --------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------ |
|
|
20
|
+
| tweak colours / button styles | change CSS class names | the [class map](#styling) |
|
|
21
|
+
| restructure **one** surface (different table markup, your grid) | override **one** partial | `rails g crud_components:views`, then edit |
|
|
22
|
+
| add a whole new arrangement (cards, list, kanban) | add a layout partial | [Add a layout](#add-a-layout) |
|
|
23
|
+
| change a single field's display rendering | add a renderer partial | [renderers](#add-a-field-renderer) |
|
|
24
|
+
| move to a different CSS framework | override the partials (class map covers the easy bits) | this whole doc |
|
|
25
|
+
|
|
26
|
+
The class map is the *simplest* lever and deliberately covers only the common, cosmetic
|
|
27
|
+
cases — colours, sizes, button variants. It is **not** a full theming engine: structural
|
|
28
|
+
and utility classes (`d-flex`, `input-group`, `form-check`, `table-responsive`) live in
|
|
29
|
+
the partials, because pretending every framework shares Bootstrap's class vocabulary
|
|
30
|
+
would be a leaky abstraction. For a framework that works differently, you override the
|
|
31
|
+
relevant partials — and because they're small and decomposed, that stays cheap.
|
|
32
|
+
**When in doubt, copy the whole partial and rewrite it; that is a supported, first-class
|
|
33
|
+
path, not a failure.**
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
bin/rails generate crud_components:views # copy the gem's partials into your app to edit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
crud_components/
|
|
41
|
+
layouts/_table.html.erb # collection layouts (layout: :table, …)
|
|
42
|
+
_toolbar.html.erb # search box + reset + collection actions (reused by layouts)
|
|
43
|
+
_pager.html.erb # footer pager in the table (shown when the relation is paginated)
|
|
44
|
+
_actions.html.erb # a group of action buttons
|
|
45
|
+
fields/_string.html.erb … # value renderers (as: :string, …)
|
|
46
|
+
filters/_text.html.erb … # filter controls
|
|
47
|
+
_record.html.erb
|
|
48
|
+
_filter.html.erb
|
|
49
|
+
_form.html.erb # _form renders via simple_form
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Overriding one surface without rewriting the rest
|
|
53
|
+
|
|
54
|
+
Say you want a completely different collection table — your own `<table>` markup, your
|
|
55
|
+
framework's classes. Override `crud_components/layouts/_table.html.erb` and rewrite the
|
|
56
|
+
shell only. You do **not** reimplement search, filtering, sorting, cells or actions —
|
|
57
|
+
the `collection` presenter and the sub-partials hand them to you:
|
|
58
|
+
|
|
59
|
+
```erb
|
|
60
|
+
<%# your app/views/crud_components/layouts/_table.html.erb %>
|
|
61
|
+
<%= render 'crud_components/toolbar', collection: collection %> <%# search + actions %>
|
|
62
|
+
<table class="my-table">
|
|
63
|
+
<thead><tr>
|
|
64
|
+
<% collection.fields.each do |field| %>
|
|
65
|
+
<th>
|
|
66
|
+
<% if collection.sortable_field?(field) %>
|
|
67
|
+
<a href="<%= collection.sort_url(field) %>">
|
|
68
|
+
<%= field.human_name %>
|
|
69
|
+
<%# sort_direction is :asc / :desc / nil (nil = not the active column).
|
|
70
|
+
sort_numeric? picks a numeric vs alphabetic icon; css.icon_prefix is
|
|
71
|
+
the library prefix. %>
|
|
72
|
+
<% if (dir = collection.sort_direction(field)) %>
|
|
73
|
+
<% family = collection.sort_numeric?(field) ? 'sort-numeric' : 'sort-alpha' %>
|
|
74
|
+
<i class="<%= collection.css.icon_prefix %><%= family %>-<%= dir == :desc ? 'up' : 'down' %>"></i>
|
|
75
|
+
<% else %>
|
|
76
|
+
<i class="<%= collection.css.icon_prefix %>arrow-down-up text-muted opacity-25"></i>
|
|
77
|
+
<% end %>
|
|
78
|
+
</a>
|
|
79
|
+
<% else %>
|
|
80
|
+
<%= field.human_name %>
|
|
81
|
+
<% end %>
|
|
82
|
+
</th>
|
|
83
|
+
<% end %>
|
|
84
|
+
</tr></thead>
|
|
85
|
+
<tbody>
|
|
86
|
+
<% collection.records.each do |record| %>
|
|
87
|
+
<tr id="<%= dom_id(record) %>">
|
|
88
|
+
<% collection.fields.each do |field| %>
|
|
89
|
+
<td><%= collection.cell(field, record) %></td> <%# type-aware cell, links, click-to-filter %>
|
|
90
|
+
<% end %>
|
|
91
|
+
<td><%= render 'crud_components/actions', actions: collection.row_actions(record) %></td>
|
|
92
|
+
</tr>
|
|
93
|
+
<% end %>
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The reusable building blocks the `collection` presenter exposes:
|
|
99
|
+
|
|
100
|
+
| Method | Returns |
|
|
101
|
+
| ---------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `fields` | the permitted fields (columns) to show, in order |
|
|
103
|
+
| `records` | the resolved, filtered, sorted rows (an array) |
|
|
104
|
+
| `cell(field, record)` | the type-aware cell HTML — value renderer, label link, click-to-filter |
|
|
105
|
+
| `sortable_field?(field)` | boolean: is this column sortable |
|
|
106
|
+
| `sort_url(field)` | the link that toggles/sets this column's sort |
|
|
107
|
+
| `sort_active?(field)` | boolean: is the result currently sorted by this column |
|
|
108
|
+
| `sort_direction(field)` | `:asc` / `:desc`, or `nil` when not the active sort column — turn it into a glyph yourself |
|
|
109
|
+
| `sort_numeric?(field)` | boolean: does this column sort numerically (vs alphabetically) — pick a numeric vs alpha icon |
|
|
110
|
+
| `filterable_field?(field)` | boolean: does this column have a filter control |
|
|
111
|
+
| `render_filter_control(field, query, …)` | the inline filter control HTML for a field |
|
|
112
|
+
| `row_actions(record)` | an `Actions` presenter for one row — feed to `_actions` |
|
|
113
|
+
| `collection_actions` | an `Actions` presenter for collection-level actions (e.g. "New") |
|
|
114
|
+
| `searchable?` | boolean: is there a free-text search (`?q=`) |
|
|
115
|
+
| `search_param_name` | the query-param name for the search box (respects `param_prefix:`) |
|
|
116
|
+
| `filtered?` | boolean: is any filter/search/sort currently active |
|
|
117
|
+
| `reset_url` | URL that clears *this* collection's filter/search/sort/page params |
|
|
118
|
+
| `filter_form_id` | the id of the external `<form>` the inline filter inputs bind to |
|
|
119
|
+
| `preserved_params` | params to re-emit as hidden inputs so the filter form keeps unrelated state |
|
|
120
|
+
| `paginated?` | boolean: was the relation handed in already `.page`-d (kaminari/will_paginate) |
|
|
121
|
+
| `page_scope` | the underlying (possibly paginated) relation, for driving your own pager |
|
|
122
|
+
| `page_url(n)` | a URL for page `n` that keeps this collection's state and others' params |
|
|
123
|
+
| `pager_pages(window:)` | page numbers to render, with `:gap` markers for elided ranges |
|
|
124
|
+
|
|
125
|
+
For pagination, either render the gem's `_pager` sub-partial, or feed `page_scope` (the underlying relation) to your own pager — e.g. `<%= paginate collection.page_scope %>` for kaminari (you style its markup, as always with kaminari). Sub-partials you can drop in: `_toolbar`, `_pager`, `_actions`. Filtering and the whitelist are never reimplemented in a layout — the presenter has already done that.
|
|
126
|
+
|
|
127
|
+
## Add a field renderer
|
|
128
|
+
|
|
129
|
+
A renderer named `:stars` is the partial `crud_components/fields/_stars.html.erb`. It
|
|
130
|
+
receives `value`, `record`, `field`, `surface` (`:collection` or `:record`), and
|
|
131
|
+
`cell_context` (for click-to-filter; nil on surfaces without a query):
|
|
132
|
+
|
|
133
|
+
```erb
|
|
134
|
+
<%# app/views/crud_components/fields/_stars.html.erb %>
|
|
135
|
+
<span title="<%= value %>/5"><%= '★' * value.to_i %><%= '☆' * (5 - value.to_i) %></span>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
attribute :rating, as: :stars
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`surface:` is how the built-in `:text` truncates in tables but not on record pages, and
|
|
143
|
+
`:image` sizes itself. Built-in renderers are the same kind of partial at the same paths —
|
|
144
|
+
**shadow one in your app to change it everywhere.**
|
|
145
|
+
|
|
146
|
+
## Form inputs
|
|
147
|
+
|
|
148
|
+
Each input renders through a per-type partial,
|
|
149
|
+
`crud_components/form_fields/_<type>.html.erb`: **the partial decides *what* to render
|
|
150
|
+
(which `f.input`, its collection, blank options, …) and simple_form does the rest** (the
|
|
151
|
+
wrapper, label, hint and error markup, following your app's simple_form config). See
|
|
152
|
+
[Forms and your design system](#forms-and-your-design-system).
|
|
153
|
+
|
|
154
|
+
Two ways to customize:
|
|
155
|
+
|
|
156
|
+
* **Restyle a whole type** — shadow the partial, e.g. `form_fields/_enum.html.erb`, and it
|
|
157
|
+
changes everywhere that type appears.
|
|
158
|
+
* **Point one field at a different partial** — `attribute :slug, form_as: :string` renders
|
|
159
|
+
`slug` through `form_fields/_string.html.erb` (this mirrors `as:` for the display
|
|
160
|
+
renderer). There is no `form` facet.
|
|
161
|
+
|
|
162
|
+
To take over form rendering entirely, override `crud_components/_form.html.erb`.
|
|
163
|
+
|
|
164
|
+
## Add a layout
|
|
165
|
+
|
|
166
|
+
A layout named `:cards` is the partial `crud_components/layouts/_cards.html.erb`,
|
|
167
|
+
receiving one `collection` presenter with resolved fields, rows, query state and sort
|
|
168
|
+
URLs — a custom layout never reimplements filtering or whitelisting:
|
|
169
|
+
|
|
170
|
+
```erb
|
|
171
|
+
<%= crud_collection @books, layout: :cards %>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
176
|
+
A layout calls the same presenter interface [listed above](#overriding-one-surface-without-rewriting-the-rest)
|
|
177
|
+
— `fields`, `records`, `cell`, the sort/filter/action helpers, the pagination helpers — so
|
|
178
|
+
the built-in `_table` is a good starting point; copy it. For a worked example that pulls
|
|
179
|
+
an image field out as a card image, see the dummy app's
|
|
180
|
+
[`_cards.html.erb`](https://github.com/itadventurer/crud_components/blob/main/test/dummy/app/views/crud_components/layouts/_cards.html.erb).
|
|
181
|
+
|
|
182
|
+
## Progressive enhancement
|
|
183
|
+
|
|
184
|
+
The progressive-enhancement story is deliberately **one mechanism, not a fork**: the
|
|
185
|
+
markup is *always* the plain, accessible, no-JS baseline, and JavaScript enhances that
|
|
186
|
+
same markup in place via Stimulus controllers attached with `data-controller`. There are
|
|
187
|
+
**no** parallel "raw" vs. "fancy" template trees to keep in sync, and Bootstrap-vs-other
|
|
188
|
+
lives in the [class map](#styling), not in template variants. A controller that isn't
|
|
189
|
+
loaded simply leaves the baseline as-is.
|
|
190
|
+
|
|
191
|
+
The gem ships **four** optional controllers, copied in by the install generator (you
|
|
192
|
+
register them with Stimulus; the gem depends on none):
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
bin/rails generate crud_components:install
|
|
196
|
+
# initializer + crud-filter + crud-multiselect + crud-columns + crud-select
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- **`crud-filter`** strips empty params on submit (clean URLs) and auto-submits selects in
|
|
200
|
+
the inline filter row only (the standalone filter form never auto-submits — users
|
|
201
|
+
compose several filters there).
|
|
202
|
+
- **`crud-multiselect`** turns a habtm `<select multiple>` into a chips-list (each removable)
|
|
203
|
+
+ an "add" dropdown. The select stays the hidden source of truth, so the form submits
|
|
204
|
+
identically with or without JS. Good up to a few hundred options; for thousands, render
|
|
205
|
+
an autocomplete against your own endpoint instead (see [forms.md](forms.md)).
|
|
206
|
+
- **`crud-columns`** lets the user drag the column-picker rows to reorder, and collapses
|
|
207
|
+
the submitted `?cols[]=a&cols[]=b` into a tidier `?cols=a,b`. Without it the picker still
|
|
208
|
+
works (tick + Apply is a plain GET); you just lose drag-reorder and the prettier URL.
|
|
209
|
+
- **`crud-select`** adds a "select all visible" / per-group master checkbox and a live
|
|
210
|
+
"N selected" count to selectable tables (bulk/selection actions). Without it the row
|
|
211
|
+
checkboxes still submit; you just tick them individually.
|
|
212
|
+
|
|
213
|
+
Each follows the same recipe, which is the whole pattern for any enhancement (a belongs_to
|
|
214
|
+
text input into an autocomplete, a date field into a range picker, …):
|
|
215
|
+
|
|
216
|
+
1. The gem's partial renders the accessible baseline and, where useful, carries a
|
|
217
|
+
`data-controller` hook.
|
|
218
|
+
2. Your Stimulus controller reads that markup and enhances it in place, manipulating the
|
|
219
|
+
underlying inputs so form submission is unchanged with or without JS.
|
|
220
|
+
3. Ship the controller however you ship Stimulus (importmap pin, `app/javascript`, …).
|
|
221
|
+
|
|
222
|
+
## Styling
|
|
223
|
+
|
|
224
|
+
The gem ships **no CSS** and produces markup meant to look native in the host app —
|
|
225
|
+
Bootstrap 5 class names by default, concentrated in one overridable class map for the
|
|
226
|
+
common cosmetic cases:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# config/initializers/crud_components.rb (created by `rails g crud_components:install`)
|
|
230
|
+
CrudComponents.configure do |config|
|
|
231
|
+
config.css.table = 'table table-sm table-hover'
|
|
232
|
+
config.css.button = 'btn btn-outline-dark'
|
|
233
|
+
config.css.badge = 'badge text-bg-secondary'
|
|
234
|
+
config.select_limit = 250 # belongs_to filter: select → text input threshold
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The full key list is `CrudComponents::Config::DEFAULT_CSS`. Each key feeds the `class="…"`
|
|
239
|
+
of one kind of element (table, button, badge, inputs, the toolbar, the filter row, …).
|
|
240
|
+
|
|
241
|
+
**Scope, honestly.** The class map is the *simplest* lever, not a theming engine. It
|
|
242
|
+
covers the elements whose class is a single configurable value. It does **not** abstract
|
|
243
|
+
away structure — utility classes like `d-flex`, `input-group`, `form-check` and
|
|
244
|
+
`table-responsive` live in the partials, because a class map that tried to model every
|
|
245
|
+
framework's layout primitives would be a leaky abstraction that helps no one.
|
|
246
|
+
|
|
247
|
+
Icons are rendered as `<i class="#{css.icon_prefix}#{name}">` — **Bootstrap Icons by
|
|
248
|
+
default** (`config.css.icon_prefix = 'bi bi-'`). Switch icon libraries by setting the
|
|
249
|
+
prefix, e.g. `config.css.icon_prefix = 'fa fa-'` for Font Awesome (the built-in icon
|
|
250
|
+
*names* are Bootstrap Icons, so a different library may need its own names — see below).
|
|
251
|
+
|
|
252
|
+
The icon **names** (the part after the prefix) live in two maps, so a different library is
|
|
253
|
+
a config change rather than a partial override:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
config.action_icons[:destroy] = 'trash-fill' # per derived action; nil = no icon
|
|
257
|
+
config.file_icons['zip'] = 'file-earmark-zip' # attachment glyph by file extension
|
|
258
|
+
config.file_fallback_icon = 'file-earmark-text' # extension not in the map
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`config.action_icons` keys are the derived actions (`:new`/`:show`/`:edit`/`:destroy`);
|
|
262
|
+
`config.file_icons` maps a file extension to a full icon name (the whole Bootstrap
|
|
263
|
+
`filetype-*` family ships by default). Full lists: `Config::DEFAULT_ACTION_ICONS` /
|
|
264
|
+
`Config::DEFAULT_FILE_ICONS`.
|
|
265
|
+
|
|
266
|
+
So the real cost of a different framework is: **swap the class map for the cosmetic
|
|
267
|
+
classes, and override the few partials whose structure differs.** For a utility-first
|
|
268
|
+
framework like Tailwind (no semantic class names at all), you put the utility strings in
|
|
269
|
+
the map and override the structural partials:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
config.css.button = 'inline-flex items-center rounded px-3 py-1.5 bg-gray-100 hover:bg-gray-200'
|
|
273
|
+
config.css.input = 'block w-full rounded border-gray-300'
|
|
274
|
+
config.css.toolbar = 'flex items-center justify-between gap-2 mb-2'
|
|
275
|
+
config.css.badge = 'inline-flex rounded-full bg-gray-100 px-2 text-xs'
|
|
276
|
+
# …then `rails g crud_components:views` and adjust _form / _toolbar / the filter
|
|
277
|
+
# controls where the *structure* (not just the class) needs to change.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This is the intended path, not a fork: you keep all the derivation, the query layer and
|
|
281
|
+
the presenters; you rewrite only the markup that your framework shapes differently.
|
|
282
|
+
|
|
283
|
+
## Forms and your design system
|
|
284
|
+
|
|
285
|
+
Forms are the **one** surface you mostly don't have to reskin by hand: they render through
|
|
286
|
+
[simple_form](https://github.com/heartcombo/simple_form), so they inherit your app's
|
|
287
|
+
simple_form wrapper config automatically — Bootstrap by default, and the community ships
|
|
288
|
+
Tailwind/Bulma/Foundation wrappers. Configure simple_form for your framework (its install
|
|
289
|
+
generator does this) and the gem's forms follow, including per-field error display (so
|
|
290
|
+
there's no `field_with_errors` wart to neutralize). The gem still derives *which* fields,
|
|
291
|
+
their types, the [permit list](forms.md) and the read-only/permission rules; simple_form
|
|
292
|
+
owns the markup.
|
|
293
|
+
|
|
294
|
+
Need to go further than wrappers allow? Override a per-type partial under
|
|
295
|
+
`crud_components/form_fields/` (see [Form inputs](#form-inputs)), or override
|
|
296
|
+
`crud_components/_form.html.erb` itself and render the fields however you like — the
|
|
297
|
+
`form` presenter hands you `fields`, `editable?(field)`, `form_options`, `summary_errors`
|
|
298
|
+
and `display(field)`, and each `field` knows its own `form_partial`.
|
|
299
|
+
|
|
300
|
+
## i18n
|
|
301
|
+
|
|
302
|
+
Headers come from `human_attribute_name` (computed fields included), so your existing
|
|
303
|
+
ActiveRecord i18n applies. Every gem-generated string is looked up with a
|
|
304
|
+
`t(..., default:)` fallback, so the gem works with **zero** locale setup and is fully
|
|
305
|
+
translatable when you want it. Relevant keys live under `crud_components.*` (actions,
|
|
306
|
+
filter labels, "+n more", confirm dialogs, empty state).
|
|
307
|
+
|
|
308
|
+
See also: [Views](views.md) · [Fields](fields.md) · [Forms](forms.md).
|