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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cdee3c229caaeda5d7f572395009c1ffd03e58c1f5376c215ced0e751c35fb9
4
- data.tar.gz: 9896dd22aa1dc9af3727596c1bee1d82ca6b8f1462a3e24a5177b10306853888
3
+ metadata.gz: e3d70fb56c9207b30cbf94fa511904507ab603cfd2823fdb57abdfb5f8975fca
4
+ data.tar.gz: dd4deef3509a263dd2782153f51706e0460df6190007106d2956585124e080a0
5
5
  SHA512:
6
- metadata.gz: 271d3b88383037c504189080665c0c15e19a640e6f8efe0d580683d9f543a0a56ff496a667064b788fbd8a8f1ddc8f48512d755cfc10985ec043d36c042891ce
7
- data.tar.gz: be82081ad386749cb1ff40f1cd48ca80417104712c417de25dccc969082f3f2edfce09a912bce53467c34e170b1a0235245caede8d53717b7fff5d3b21d8f072
6
+ metadata.gz: 2c9dc8c29d5680178904699e5f32e2d9506e2abae14918da3c3457bc640d7c92ce458f8439461470e11e244a4a2a477ca63d0e2b1df24a99cc7bbcc76304a743
7
+ data.tar.gz: 4d4cb0845691bb1b4f1b1f814eafdbd5de506c4b36f9bfe8cbdbd6ab654dfef18502fab7bc92d98d02fca3349750f94d7484f8cb54ed3cdb711aa3a677a119fd
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
- the # Tam Select
1
+ # Tam Select
2
2
 
3
3
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
- [![Gem Version](https://img.shields.io/badge/version-1.1.0-blue.svg)](https://github.com/tamiru/tam_select)
4
+ [![Gem Version](https://img.shields.io/badge/version-1.2.3-blue.svg)](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
- ### Updating
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
- ## Rails and Stimulus
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
- The install generator creates `app/inputs/tam_select_input.rb`. Use `as: :tam_select` with any Simple Form collection input.
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": "Addis Ababa" },
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
- ### Generic remote controller
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
- The installer generates `TamSelectRemoteController`. Create a small subclass for each allowed remote source:
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
- ```ruby
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
- private
268
-
269
- # Override this method when records require authorization or tenant scoping.
270
- def tam_select_scope(config)
271
- current_account.regions.order(:name)
272
- end
273
- end
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
- Expose the JSON endpoint and pass it to the input:
335
+ For student search, add the admission number and photo URL:
277
336
 
278
337
  ```ruby
279
- # config/routes.rb
280
- get "region_options", to: "region_options#index", defaults: { format: :json }
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
- <%= form.input :region_id,
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 /region_options.json?q=addis&page=1
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/region_options.json?q=addis&page=1"
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 "tam-select"
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
- # The gem has no Ruby runtime tests yet.
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
- copy_file "lib/generators/tam_select/templates/tam_select_input.rb", "app/inputs/tam_select_input.rb"
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,5 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
- import TamSelect from "../tam_select/tam_select"
2
+ import TamSelect from "tam_select"
3
3
 
4
4
  export default class extends Controller {
5
5
  static values = {
@@ -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] = [data[:controller], "tam-select"].compact.join(" ")
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] = [data[:controller], "tam-select"].compact.join(" ")
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.public_send(value).to_s,
15
- label: label.respond_to?(:call) ? label.call(record) : record.public_send(label).to_s
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
@@ -1,3 +1,3 @@
1
1
  module TamSelect
2
- VERSION = "1.1.1"
2
+ VERSION = "1.2.3"
3
3
  end
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