tam_select 1.1.1 → 1.2.3
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 +4 -4
- data/README.md +148 -49
- data/Rakefile +5 -2
- data/lib/generators/tam_select/install_generator.rb +33 -1
- data/lib/generators/tam_select/templates/tam_select_controller.js +1 -1
- data/lib/generators/tam_select/templates/tam_select_helper.rb +1 -1
- data/lib/generators/tam_select/templates/tam_select_input.rb +1 -1
- data/lib/generators/tam_select/templates/tam_select_paginatable.rb +11 -4
- data/lib/generators/tam_select/templates/tam_select_remote.rb +7 -1
- data/lib/tam_select/version.rb +1 -1
- data/package.json +48 -0
- data/rails/app/controllers/concerns/tam_select_paginatable.rb +32 -0
- data/rails/app/helpers/tam_select_helper.rb +17 -0
- data/rails/app/inputs/tam_select_input.rb +30 -0
- data/rails/app/javascript/controllers/tam_select_controller.js +31 -0
- data/src/tam-select.js +592 -149
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3d70fb56c9207b30cbf94fa511904507ab603cfd2823fdb57abdfb5f8975fca
|
|
4
|
+
data.tar.gz: dd4deef3509a263dd2782153f51706e0460df6190007106d2956585124e080a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c9dc8c29d5680178904699e5f32e2d9506e2abae14918da3c3457bc640d7c92ce458f8439461470e11e244a4a2a477ca63d0e2b1df24a99cc7bbcc76304a743
|
|
7
|
+
data.tar.gz: 4d4cb0845691bb1b4f1b1f814eafdbd5de506c4b36f9bfe8cbdbd6ab654dfef18502fab7bc92d98d02fca3349750f94d7484f8cb54ed3cdb711aa3a677a119fd
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
# Tam Select
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](https://github.com/tamiru/tam_select)
|
|
5
5
|
|
|
6
6
|
**Tam Select** is an accessible, searchable select component for Ruby on Rails, built for Simple Form, Stimulus, Turbo, and Tailwind CSS. It keeps the native `<select>` as the source of truth, so Rails form submission, validation, selected values, and browser autofill continue to work.
|
|
7
7
|
|
|
@@ -29,20 +29,53 @@ bundle install
|
|
|
29
29
|
bin/rails generate tam_select:install
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The generator installs:
|
|
32
|
+
The generator installs the core module, Stimulus controller, Rails helpers, and remote-search concerns:
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
35
|
app/javascript/tam_select/tam_select.js
|
|
36
36
|
app/javascript/controllers/tam_select_controller.js
|
|
37
|
-
app/inputs/tam_select_input.rb
|
|
38
37
|
app/controllers/concerns/tam_select_paginatable.rb
|
|
39
38
|
app/controllers/concerns/tam_select_remote.rb
|
|
40
39
|
app/controllers/tam_select_remote_controller.rb
|
|
41
40
|
app/helpers/tam_select_helper.rb
|
|
42
41
|
```
|
|
43
42
|
|
|
43
|
+
When Simple Form is present, it also installs `app/inputs/tam_select_input.rb`. Simple Form is optional; applications without it receive a short skip message and continue with standard Rails form helpers.
|
|
44
|
+
|
|
44
45
|
Commit these generated files with the Rails application. This makes customization straightforward and allows Tailwind CSS 4 to scan the component without resolving a Ruby gem directory at build time.
|
|
45
46
|
|
|
47
|
+
### Rails 8 importmap
|
|
48
|
+
|
|
49
|
+
The default Rails 8 importmap setup needs no manual JavaScript package installation. The generator adds this pin to `config/importmap.rb`:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
pin "tam_select", to: "tam_select/tam_select.js"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The generated Stimulus controller imports `TamSelect` from `"tam_select"`. Running the generator again updates an old pin when necessary and never adds a duplicate. Rails' normal `pin_all_from "app/javascript/controllers", under: "controllers"` line discovers the generated controller.
|
|
56
|
+
|
|
57
|
+
### jsbundling-rails and esbuild
|
|
58
|
+
|
|
59
|
+
The same generated controller works with esbuild when the bare `tam_select` import is aliased to the generated core file. Add the alias flag to the existing `build` script in `package.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets --alias:tam_select=./app/javascript/tam_select/tam_select.js"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This uses the local file installed by the generator. For non-Rails bundler use, install the npm package from GitHub and import its published package name instead:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install github:tamiru/tam_select
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import TamSelect from "tam-select"
|
|
77
|
+
```
|
|
78
|
+
|
|
46
79
|
Add the generated JavaScript to Tailwind's source detection in `app/assets/tailwind/application.css`:
|
|
47
80
|
|
|
48
81
|
```css
|
|
@@ -50,7 +83,7 @@ Add the generated JavaScript to Tailwind's source detection in `app/assets/tailw
|
|
|
50
83
|
@source "../../javascript/tam_select/**/*.js";
|
|
51
84
|
```
|
|
52
85
|
|
|
53
|
-
###
|
|
86
|
+
### Upgrading
|
|
54
87
|
|
|
55
88
|
The Rails integration files are copied into your application, so updating the gem does not update them automatically. Commit local customizations first, then run:
|
|
56
89
|
|
|
@@ -67,12 +100,15 @@ bin/rails generate tam_select:install --force
|
|
|
67
100
|
|
|
68
101
|
Review `git diff` afterward because `--force` overwrites application-specific customizations.
|
|
69
102
|
|
|
103
|
+
Versions before this release generated a relative controller import. After upgrading, confirm the controller uses `import TamSelect from "tam_select"` and that the importmap pin or esbuild alias above is present.
|
|
104
|
+
|
|
70
105
|
## Features
|
|
71
106
|
|
|
72
107
|
- Single and multiple selection
|
|
73
108
|
- Local search and user-created tags
|
|
74
109
|
- Remote JSON search with debouncing and incremental pagination
|
|
75
110
|
- Loading, empty, and error states
|
|
111
|
+
- Select2-style closed selection and open search states with clear active, selected, disabled, loading, empty, and error feedback
|
|
76
112
|
- Keyboard navigation: arrows, Enter, Escape, Tab, and Backspace
|
|
77
113
|
- Combobox/listbox ARIA semantics
|
|
78
114
|
- Light and dark Tailwind themes
|
|
@@ -80,14 +116,6 @@ Review `git diff` afterward because `--force` overwrites application-specific cu
|
|
|
80
116
|
- Public API and bubbling custom events
|
|
81
117
|
- No jQuery, Tom Select, Select2, Preline, or Floating UI dependency
|
|
82
118
|
|
|
83
|
-
## JavaScript installation
|
|
84
|
-
|
|
85
|
-
For a non-Rails application, install directly from GitHub:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
npm install github:tamiru/tam_select
|
|
89
|
-
```
|
|
90
|
-
|
|
91
119
|
## Tailwind CSS 4
|
|
92
120
|
|
|
93
121
|
Add the package source to `app/assets/tailwind/application.css` so Tailwind generates every class used by the JavaScript templates:
|
|
@@ -104,7 +132,19 @@ When the Rails generator is used, scan the generated component source:
|
|
|
104
132
|
@source "../../javascript/tam_select/**/*.js";
|
|
105
133
|
```
|
|
106
134
|
|
|
107
|
-
|
|
135
|
+
Tam Select includes adaptive light and dark styles for every state. It follows a `dark` class on the document or any ancestor and also sets the appropriate native `color-scheme`:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
document.documentElement.classList.toggle("dark")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For Tailwind CSS 4 class-based theming, define the variant in the application stylesheet if it is not already present:
|
|
142
|
+
|
|
143
|
+
```css
|
|
144
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Standard Rails forms and Stimulus
|
|
108
148
|
|
|
109
149
|
The install generator copies the Stimulus controller into your application, where Stimulus normally discovers it automatically. If controllers are registered manually:
|
|
110
150
|
|
|
@@ -130,9 +170,28 @@ Use a normal Rails select:
|
|
|
130
170
|
|
|
131
171
|
Stimulus destroys generated markup when Turbo removes the select and recreates it on reconnection.
|
|
132
172
|
|
|
173
|
+
For multiple selection, Rails must receive an array field and the native select must include `multiple: true`:
|
|
174
|
+
|
|
175
|
+
```erb
|
|
176
|
+
<%= form.select :skill_ids,
|
|
177
|
+
options_from_collection_for_select(Skill.order(:name), :id, :name, form.object.skill_ids),
|
|
178
|
+
{},
|
|
179
|
+
multiple: true,
|
|
180
|
+
data: {
|
|
181
|
+
controller: "tam-select",
|
|
182
|
+
tam_select_options_value: {
|
|
183
|
+
searchable: true,
|
|
184
|
+
closeAfterSelect: false,
|
|
185
|
+
placeholder: "Add skills…"
|
|
186
|
+
}.to_json
|
|
187
|
+
} %>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The original `<select>` remains the source of truth. This preserves form names in nested forms, submitted values, prompts, `required`, validation metadata, and native `change` events.
|
|
191
|
+
|
|
133
192
|
## Simple Form
|
|
134
193
|
|
|
135
|
-
|
|
194
|
+
When the `simple_form` gem is installed, the generator creates `app/inputs/tam_select_input.rb`. Use `as: :tam_select` with any Simple Form collection input. The input forwards Simple Form's prompt, selected values, multiple flag, error classes, ARIA attributes, and nested builder context to the native collection select.
|
|
136
195
|
|
|
137
196
|
### Local collection
|
|
138
197
|
|
|
@@ -193,8 +252,8 @@ Options are passed under `input_html[:tam_options]`. Common options include `sea
|
|
|
193
252
|
```json
|
|
194
253
|
{
|
|
195
254
|
"items": [
|
|
196
|
-
{ "value": "1", "label": "
|
|
197
|
-
{ "value": "2", "label": "Afar" }
|
|
255
|
+
{ "value": "1", "label": "Hana Bekele", "detail": "Admission no. UG/1024/26", "meta": "Active", "image": "/avatars/hana.jpg" },
|
|
256
|
+
{ "value": "2", "label": "Afar", "detail": "Semera", "meta": "AF" }
|
|
198
257
|
],
|
|
199
258
|
"pagination": {
|
|
200
259
|
"page": 1,
|
|
@@ -221,6 +280,8 @@ class RegionsController < ApplicationController
|
|
|
221
280
|
model: Region,
|
|
222
281
|
label: :name,
|
|
223
282
|
value: :id,
|
|
283
|
+
detail: :description,
|
|
284
|
+
meta: :code,
|
|
224
285
|
search_by: %i[name code],
|
|
225
286
|
scope: -> { Region.order(:name) },
|
|
226
287
|
per_page: 20
|
|
@@ -255,51 +316,45 @@ Point Simple Form to that collection action:
|
|
|
255
316
|
|
|
256
317
|
Typing sends `GET /regions/tam_select_options.json?q=addis&page=1` and receives the standard Tam Select JSON payload.
|
|
257
318
|
|
|
258
|
-
|
|
319
|
+
When `pagination.has_more` is true, scrolling near the bottom requests `next_page`. Earlier pages keep their order, duplicate values are collapsed, and a later response can update an existing item's label, metadata, image, or disabled state. A new query aborts the old request and never displays cached results from the previous query.
|
|
259
320
|
|
|
260
|
-
|
|
321
|
+
Remote items may include optional `detail`, `meta`, and `image` fields. Tam Select renders the primary label with the detail on a second line and an optional circular image on the left. The selected value keeps the same image, label, and detail, including an initial value rendered before remote search completes. `meta` remains a badge on the right.
|
|
261
322
|
|
|
262
|
-
|
|
263
|
-
# app/controllers/region_options_controller.rb
|
|
264
|
-
class RegionOptionsController < TamSelectRemoteController
|
|
265
|
-
tam_select model: Region, label: :name, search_by: %i[name code]
|
|
323
|
+
This works well for program and student searches. For example, return the program name as `label` and admission as `detail`:
|
|
266
324
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
325
|
+
```ruby
|
|
326
|
+
tam_select_remote(
|
|
327
|
+
model: Estudent::Program,
|
|
328
|
+
label: :name,
|
|
329
|
+
detail: ->(program) { program.admission.to_s },
|
|
330
|
+
search_by: %i[name],
|
|
331
|
+
scope: -> { Estudent::Program.includes(admission: %i[admission_type enrollment_type enrollment_mode]).order(:name) }
|
|
332
|
+
)
|
|
274
333
|
```
|
|
275
334
|
|
|
276
|
-
|
|
335
|
+
For student search, add the admission number and photo URL:
|
|
277
336
|
|
|
278
337
|
```ruby
|
|
279
|
-
|
|
280
|
-
|
|
338
|
+
tam_select_remote(
|
|
339
|
+
model: Estudent::Student,
|
|
340
|
+
label: ->(student) { student.full_name },
|
|
341
|
+
detail: ->(student) { "Admission no. #{student.applicant.registration_number}" },
|
|
342
|
+
image: ->(student) { url_for(student.applicant.person.avatar) if student.applicant.person.avatar.attached? },
|
|
343
|
+
search_by: %i[id_number],
|
|
344
|
+
scope: -> { Estudent::Student.includes(applicant: { person: { avatar_attachment: :blob } }) }
|
|
345
|
+
)
|
|
281
346
|
```
|
|
282
347
|
|
|
348
|
+
For a local native select, provide the same values as option data attributes:
|
|
349
|
+
|
|
283
350
|
```erb
|
|
284
|
-
|
|
285
|
-
as: :tam_select,
|
|
286
|
-
collection: [form.object.region].compact,
|
|
287
|
-
label_method: :name,
|
|
288
|
-
value_method: :id,
|
|
289
|
-
input_html: {
|
|
290
|
-
tam_options: {
|
|
291
|
-
remoteUrl: region_options_path(format: :json),
|
|
292
|
-
minQueryLength: 1
|
|
293
|
-
}
|
|
294
|
-
} %>
|
|
351
|
+
<option value="1" data-detail="Admission no. UG/1024/26" data-meta="Active" data-image="/avatars/hana.jpg">Hana Bekele</option>
|
|
295
352
|
```
|
|
296
353
|
|
|
297
|
-
Do not accept a model name from request parameters. Declaring each source in a controller subclass prevents clients from querying arbitrary application models.
|
|
298
|
-
|
|
299
354
|
The browser sends requests such as:
|
|
300
355
|
|
|
301
356
|
```text
|
|
302
|
-
GET /
|
|
357
|
+
GET /regions/tam_select_options.json?q=addis&page=1
|
|
303
358
|
Accept: application/json
|
|
304
359
|
```
|
|
305
360
|
|
|
@@ -307,15 +362,47 @@ Test an endpoint independently with:
|
|
|
307
362
|
|
|
308
363
|
```bash
|
|
309
364
|
curl -H "Accept: application/json" \
|
|
310
|
-
"http://localhost:3000/
|
|
365
|
+
"http://localhost:3000/regions/tam_select_options.json?q=addis&page=1"
|
|
311
366
|
```
|
|
312
367
|
|
|
313
368
|
A `406 Not Acceptable` response means the route or controller rejected JSON. Keep `defaults: { format: :json }` on the route, use a `.json` URL, and ensure the controller does not restrict responses to HTML only.
|
|
314
369
|
|
|
370
|
+
## Creatable values
|
|
371
|
+
|
|
372
|
+
Set `creatable: true` for tag-style input. The Create action is part of the same keyboard list as normal results, so ArrowUp and ArrowDown can highlight it and Enter creates it.
|
|
373
|
+
|
|
374
|
+
```erb
|
|
375
|
+
<%= form.select :category_ids,
|
|
376
|
+
options_from_collection_for_select(Category.order(:name), :id, :name),
|
|
377
|
+
{},
|
|
378
|
+
multiple: true,
|
|
379
|
+
data: {
|
|
380
|
+
controller: "tam-select",
|
|
381
|
+
tam_select_options_value: {
|
|
382
|
+
creatable: true,
|
|
383
|
+
closeAfterSelect: false,
|
|
384
|
+
placeholder: "Add categories…"
|
|
385
|
+
}.to_json
|
|
386
|
+
} %>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Values and labels are compared with normalized, case-insensitive, accent-insensitive text before creation, preventing duplicate entries such as `Café` and `cafe`. A successful creation dispatches `tam-select:create`.
|
|
390
|
+
|
|
391
|
+
## Keyboard and accessibility behavior
|
|
392
|
+
|
|
393
|
+
- ArrowDown and ArrowUp open the list, move through the rendered entries, and skip disabled options.
|
|
394
|
+
- Enter selects the highlighted option or activates the highlighted Create action.
|
|
395
|
+
- Escape closes the list and returns focus to the combobox; Tab closes without trapping focus.
|
|
396
|
+
- Backspace removes the last tag from a multiple select when the search input is empty.
|
|
397
|
+
- Searchable controls use an input combobox; non-searchable controls use a focusable button combobox.
|
|
398
|
+
- Rails labels, descriptions, required state, disabled state, and invalid state are mirrored from the native select.
|
|
399
|
+
|
|
315
400
|
## Core JavaScript
|
|
316
401
|
|
|
402
|
+
For a generated Rails/importmap installation, use the pinned module name:
|
|
403
|
+
|
|
317
404
|
```js
|
|
318
|
-
import TamSelect from "
|
|
405
|
+
import TamSelect from "tam_select"
|
|
319
406
|
|
|
320
407
|
const instance = new TamSelect(document.querySelector("#student_region_id"), {
|
|
321
408
|
searchable: true,
|
|
@@ -325,11 +412,15 @@ const instance = new TamSelect(document.querySelector("#student_region_id"), {
|
|
|
325
412
|
})
|
|
326
413
|
|
|
327
414
|
instance.setValue("2")
|
|
415
|
+
instance.open()
|
|
416
|
+
instance.close()
|
|
328
417
|
instance.clear()
|
|
329
418
|
instance.refresh()
|
|
330
419
|
instance.destroy()
|
|
331
420
|
```
|
|
332
421
|
|
|
422
|
+
When consuming the npm package directly, import from `"tam-select"` instead.
|
|
423
|
+
|
|
333
424
|
## Main options
|
|
334
425
|
|
|
335
426
|
| Option | Default | Purpose |
|
|
@@ -338,6 +429,8 @@ instance.destroy()
|
|
|
338
429
|
| `creatable` | `false` | Allows typed values to become options |
|
|
339
430
|
| `clearable` | `true` | Displays the clear control |
|
|
340
431
|
| `closeAfterSelect` | Single only | Keeps multiple dropdowns open |
|
|
432
|
+
| `placeholder` | Native prompt or `Select…` | Closed-control placeholder |
|
|
433
|
+
| `searchPlaceholder` | `Search…` | Search-input placeholder |
|
|
341
434
|
| `remoteUrl` | `null` | JSON search endpoint |
|
|
342
435
|
| `queryParam` | `q` | Remote search parameter |
|
|
343
436
|
| `pageParam` | `page` | Remote page parameter |
|
|
@@ -345,6 +438,8 @@ instance.destroy()
|
|
|
345
438
|
| `minQueryLength` | `0` | Characters required before requesting |
|
|
346
439
|
| `valueField` | `value` | Remote item value key |
|
|
347
440
|
| `labelField` | `label` | Remote item label key |
|
|
441
|
+
| `imageField` | `image` | Remote item image URL key |
|
|
442
|
+
| `matcher` | `null` | Optional `(item, query) => boolean` local matcher |
|
|
348
443
|
| `classes` | `{}` | Overrides any Tailwind class group |
|
|
349
444
|
|
|
350
445
|
## Events
|
|
@@ -353,6 +448,8 @@ Listen on the original select. Every event bubbles:
|
|
|
353
448
|
|
|
354
449
|
```js
|
|
355
450
|
select.addEventListener("tam-select:change", ({ detail }) => console.log(detail.value))
|
|
451
|
+
select.addEventListener("tam-select:open", () => console.log("opened"))
|
|
452
|
+
select.addEventListener("tam-select:close", () => console.log("closed"))
|
|
356
453
|
select.addEventListener("tam-select:load", ({ detail }) => console.log(detail.items))
|
|
357
454
|
select.addEventListener("tam-select:create", ({ detail }) => console.log(detail.item))
|
|
358
455
|
select.addEventListener("tam-select:error", ({ detail }) => console.error(detail.error))
|
|
@@ -360,6 +457,8 @@ select.addEventListener("tam-select:error", ({ detail }) => console.error(detail
|
|
|
360
457
|
|
|
361
458
|
Standard native `change` events are also dispatched for Rails and other controllers.
|
|
362
459
|
|
|
460
|
+
The supported public methods are `open()`, `close()`, `setValue(value)`, `clear()`, `refresh()`, and `destroy()`. Read the current native-compatible selection through `instance.value`, and recover an existing instance with `TamSelect.getInstance(select)`.
|
|
461
|
+
|
|
363
462
|
## Development
|
|
364
463
|
|
|
365
464
|
```bash
|
data/Rakefile
CHANGED
|
@@ -2,7 +2,10 @@ require "bundler/gem_tasks"
|
|
|
2
2
|
|
|
3
3
|
begin
|
|
4
4
|
require "rake/testtask"
|
|
5
|
-
Rake::TestTask.new
|
|
5
|
+
Rake::TestTask.new do |test|
|
|
6
|
+
test.libs << "test"
|
|
7
|
+
test.pattern = "test/**/*_test.rb"
|
|
8
|
+
end
|
|
6
9
|
rescue LoadError
|
|
7
|
-
#
|
|
10
|
+
# Rake's test task is unavailable.
|
|
8
11
|
end
|
|
@@ -12,8 +12,30 @@ module TamSelect
|
|
|
12
12
|
copy_file "lib/generators/tam_select/templates/tam_select_controller.js", "app/javascript/controllers/tam_select_controller.js"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def configure_importmap
|
|
16
|
+
importmap = "config/importmap.rb"
|
|
17
|
+
unless File.exist?(destination_path(importmap))
|
|
18
|
+
say "Importmap was not detected. For jsbundling/esbuild, alias tam_select to app/javascript/tam_select/tam_select.js.", :yellow
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
pin = 'pin "tam_select", to: "tam_select/tam_select.js"'
|
|
23
|
+
contents = File.read(destination_path(importmap))
|
|
24
|
+
existing_pin = /^\s*pin\s+["']tam_select["'].*$/
|
|
25
|
+
|
|
26
|
+
if contents.match?(existing_pin)
|
|
27
|
+
gsub_file importmap, existing_pin, pin unless contents.match?(/^#{Regexp.escape(pin)}$/)
|
|
28
|
+
else
|
|
29
|
+
append_to_file importmap, "\n#{pin}\n"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
15
33
|
def copy_simple_form_input
|
|
16
|
-
|
|
34
|
+
if simple_form_available?
|
|
35
|
+
copy_file "lib/generators/tam_select/templates/tam_select_input.rb", "app/inputs/tam_select_input.rb"
|
|
36
|
+
else
|
|
37
|
+
say "Simple Form is not installed; skipped app/inputs/tam_select_input.rb. Standard Rails form helpers remain available.", :yellow
|
|
38
|
+
end
|
|
17
39
|
end
|
|
18
40
|
|
|
19
41
|
def copy_controller_concern
|
|
@@ -35,6 +57,16 @@ module TamSelect
|
|
|
35
57
|
say "If your setup uses explicit sources, add:"
|
|
36
58
|
say '@source "../../javascript/tam_select/**/*.js";'
|
|
37
59
|
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def destination_path(relative_path)
|
|
64
|
+
File.join(destination_root, relative_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def simple_form_available?
|
|
68
|
+
Gem.loaded_specs.key?("simple_form")
|
|
69
|
+
end
|
|
38
70
|
end
|
|
39
71
|
end
|
|
40
72
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module TamSelectHelper
|
|
2
2
|
def tam_select_tag(name, option_tags = nil, options: {}, html_options: {}, &block)
|
|
3
3
|
data = (html_options[:data] ||= {})
|
|
4
|
-
data[:controller] =
|
|
4
|
+
data[:controller] = (data[:controller].to_s.split + ["tam-select"]).uniq.join(" ")
|
|
5
5
|
data[:tam_select_options_value] = options.to_json
|
|
6
6
|
select_tag(name, option_tags, html_options, &block)
|
|
7
7
|
end
|
|
@@ -4,7 +4,7 @@ class TamSelectInput < SimpleForm::Inputs::CollectionSelectInput
|
|
|
4
4
|
label_method, value_method = detect_collection_methods
|
|
5
5
|
tam_options = attributes.delete(:tam_options) || {}
|
|
6
6
|
data = attributes[:data] ||= {}
|
|
7
|
-
data[:controller] =
|
|
7
|
+
data[:controller] = (data[:controller].to_s.split + ["tam-select"]).uniq.join(" ")
|
|
8
8
|
data[:tam_select_options_value] = defaults.merge(tam_options).to_json
|
|
9
9
|
|
|
10
10
|
@builder.collection_select(
|
|
@@ -3,7 +3,7 @@ module TamSelectPaginatable
|
|
|
3
3
|
|
|
4
4
|
private
|
|
5
5
|
|
|
6
|
-
def tam_select_payload(scope, label:, value: :id, per_page: 20)
|
|
6
|
+
def tam_select_payload(scope, label:, value: :id, detail: nil, meta: nil, image: nil, per_page: 20)
|
|
7
7
|
page = [params.fetch(:page, 1).to_i, 1].max
|
|
8
8
|
records = scope.limit(per_page + 1).offset((page - 1) * per_page).to_a
|
|
9
9
|
has_more = records.length > per_page
|
|
@@ -11,9 +11,12 @@ module TamSelectPaginatable
|
|
|
11
11
|
{
|
|
12
12
|
items: records.first(per_page).map do |record|
|
|
13
13
|
{
|
|
14
|
-
value: record
|
|
15
|
-
label:
|
|
16
|
-
|
|
14
|
+
value: tam_select_record_value(record, value).to_s,
|
|
15
|
+
label: tam_select_record_value(record, label).to_s,
|
|
16
|
+
detail: detail && tam_select_record_value(record, detail).to_s,
|
|
17
|
+
meta: meta && tam_select_record_value(record, meta).to_s,
|
|
18
|
+
image: image && tam_select_record_value(record, image).to_s
|
|
19
|
+
}.compact
|
|
17
20
|
end,
|
|
18
21
|
pagination: {
|
|
19
22
|
page: page,
|
|
@@ -22,4 +25,8 @@ module TamSelectPaginatable
|
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
def tam_select_record_value(record, resolver)
|
|
30
|
+
resolver.respond_to?(:call) ? resolver.call(record) : record.public_send(resolver)
|
|
31
|
+
end
|
|
25
32
|
end
|
|
@@ -7,11 +7,14 @@ module TamSelectRemote
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
class_methods do
|
|
10
|
-
def tam_select_remote(model:, label:, value: :id, search_by: nil, scope: nil, per_page: 20)
|
|
10
|
+
def tam_select_remote(model:, label:, value: :id, detail: nil, meta: nil, image: nil, search_by: nil, scope: nil, per_page: 20)
|
|
11
11
|
self.tam_select_remote_config = {
|
|
12
12
|
model: model,
|
|
13
13
|
label: label,
|
|
14
14
|
value: value,
|
|
15
|
+
detail: detail,
|
|
16
|
+
meta: meta,
|
|
17
|
+
image: image,
|
|
15
18
|
search_by: Array(search_by || label),
|
|
16
19
|
scope: scope,
|
|
17
20
|
per_page: per_page
|
|
@@ -27,6 +30,9 @@ module TamSelectRemote
|
|
|
27
30
|
records,
|
|
28
31
|
label: config[:label],
|
|
29
32
|
value: config[:value],
|
|
33
|
+
detail: config[:detail],
|
|
34
|
+
meta: config[:meta],
|
|
35
|
+
image: config[:image],
|
|
30
36
|
per_page: config[:per_page]
|
|
31
37
|
)
|
|
32
38
|
end
|
data/lib/tam_select/version.rb
CHANGED
data/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tam-select",
|
|
3
|
+
"version": "1.2.3",
|
|
4
|
+
"description": "Accessible, searchable select component for Ruby on Rails, Simple Form, Stimulus, Turbo, and Tailwind CSS",
|
|
5
|
+
"homepage": "https://github.com/tamiru/tam_select",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/tamiru/tam_select.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./src/tam-select.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/tam-select.js",
|
|
14
|
+
"./rails-controller": "./rails/app/javascript/controllers/tam_select_controller.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"rails",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test test/*.test.js",
|
|
24
|
+
"check": "node --check src/tam-select.js && node --check rails/app/javascript/controllers/tam_select_controller.js"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"select",
|
|
28
|
+
"tailwindcss",
|
|
29
|
+
"stimulus",
|
|
30
|
+
"turbo",
|
|
31
|
+
"rails",
|
|
32
|
+
"combobox"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@hotwired/stimulus": ">=3.2.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@hotwired/stimulus": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@hotwired/stimulus": "^3.2.2",
|
|
45
|
+
"esbuild": "^0.28.1",
|
|
46
|
+
"jsdom": "^29.1.1"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module TamSelectPaginatable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def tam_select_payload(scope, label:, value: :id, detail: nil, meta: nil, image: nil, per_page: 20)
|
|
7
|
+
page = [params.fetch(:page, 1).to_i, 1].max
|
|
8
|
+
records = scope.limit(per_page + 1).offset((page - 1) * per_page).to_a
|
|
9
|
+
has_more = records.length > per_page
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
items: records.first(per_page).map do |record|
|
|
13
|
+
{
|
|
14
|
+
value: tam_select_record_value(record, value).to_s,
|
|
15
|
+
label: tam_select_record_value(record, label).to_s,
|
|
16
|
+
detail: detail && tam_select_record_value(record, detail).to_s,
|
|
17
|
+
meta: meta && tam_select_record_value(record, meta).to_s,
|
|
18
|
+
image: image && tam_select_record_value(record, image).to_s
|
|
19
|
+
}.compact
|
|
20
|
+
end,
|
|
21
|
+
pagination: {
|
|
22
|
+
page: page,
|
|
23
|
+
next_page: has_more ? page + 1 : nil,
|
|
24
|
+
has_more: has_more
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tam_select_record_value(record, resolver)
|
|
30
|
+
resolver.respond_to?(:call) ? resolver.call(record) : record.public_send(resolver)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module TamSelectHelper
|
|
2
|
+
def tam_select_tag(name, option_tags = nil, options: {}, html_options: {}, &block)
|
|
3
|
+
data = (html_options[:data] ||= {})
|
|
4
|
+
data[:controller] = (data[:controller].to_s.split + ["tam-select"]).uniq.join(" ")
|
|
5
|
+
data[:tam_select_options_value] = options.to_json
|
|
6
|
+
select_tag(name, option_tags, html_options, &block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def tam_select_options(**overrides)
|
|
10
|
+
{
|
|
11
|
+
searchable: true,
|
|
12
|
+
clearable: true,
|
|
13
|
+
creatable: false,
|
|
14
|
+
placeholder: "Select…"
|
|
15
|
+
}.merge(overrides)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class TamSelectInput < SimpleForm::Inputs::CollectionSelectInput
|
|
2
|
+
def input(wrapper_options = nil)
|
|
3
|
+
attributes = merge_wrapper_options(input_html_options, wrapper_options)
|
|
4
|
+
label_method, value_method = detect_collection_methods
|
|
5
|
+
tam_options = attributes.delete(:tam_options) || {}
|
|
6
|
+
data = attributes[:data] ||= {}
|
|
7
|
+
data[:controller] = (data[:controller].to_s.split + ["tam-select"]).uniq.join(" ")
|
|
8
|
+
data[:tam_select_options_value] = defaults.merge(tam_options).to_json
|
|
9
|
+
|
|
10
|
+
@builder.collection_select(
|
|
11
|
+
attribute_name,
|
|
12
|
+
collection,
|
|
13
|
+
value_method,
|
|
14
|
+
label_method,
|
|
15
|
+
input_options,
|
|
16
|
+
attributes
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def defaults
|
|
23
|
+
{
|
|
24
|
+
searchable: options.fetch(:searchable, true),
|
|
25
|
+
creatable: options.fetch(:creatable, false),
|
|
26
|
+
clearable: options.fetch(:clearable, true),
|
|
27
|
+
placeholder: options[:placeholder] || options[:prompt] || "Select…"
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|