remote_select 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cce3aae6bcdda3d4094e25da7d93613b40d1971a6e62284dd0f8416569e4fa4
4
- data.tar.gz: 3f4d2f7cc3ce1f84e87b4da8b5b1c9b9e33611bfae03a03e354ea2d4d6db005d
3
+ metadata.gz: 15df89b787a91d68146ed4b189ba178c6ba1e0f83f9f5cffbd7e613cd3ccf3cd
4
+ data.tar.gz: 4187104ab27012204f54186dcf2047fb3c75523f10de6a4e05eee795d2c7a565
5
5
  SHA512:
6
- metadata.gz: 2d0f92bff38d7f97943075af39052ff425cd3a1e1a6314bafaa3ddb0073383c2d716bc01dc00831657858187fa4e47c55567bc2e98f62a647fb61b3b23961d62
7
- data.tar.gz: 95699a31cdf43ba2ff131e4bd9a017380da506f21d6f1946af94d9b5712da2e51383414e718903afb0fc5288aed498ed4f440f2d6fb111010e5c9fad7b022c98
6
+ metadata.gz: 2e0272bf4bef5398addcc85d635ad09ee6a1f78e7df4987e8f71d032ab966d4e80dc9bc88ecf376b758651104189d0e85e2248c03b2e3ac3222f02f60895febf
7
+ data.tar.gz: '089a736b827eadffdd02d3b0dd1bfe996f7f1f9c43a9dc7d0012c08849b2a295b77a014162c573d77377200cf42ac531ebd25659289f7120205c0c27973e7ae0'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.0] - 2026-03-20
6
+
7
+ ### Added
8
+ - **Install generator** (`rails generate remote_select:install`) — copies JS and CSS into the consuming app with pipeline-specific post-install instructions. Works with esbuild, rollup, webpack, importmap, and Sprockets.
9
+ - **Select2-compatible response parsing** — accepts both `has_more` and `pagination.more` response formats, enabling zero-backend-change migration from Select2.
10
+ - **AI assistant integration guide** (`COPILOT_INSTRUCTIONS.md`) — concise reference for coding assistants to integrate the gem correctly.
11
+ - **Minitest test suite** — view helper tests, generator tests, JS/CSS source integrity tests, version tests.
12
+ - **CSS custom properties documentation** in README with full theming reference table.
13
+ - **Version headers** in JS and CSS source files for traceability.
14
+
15
+ ### Changed
16
+ - README restructured with generator-first installation flow, per-pipeline setup instructions (collapsible details), and dual response format documentation.
17
+
18
+ ### Fixed
19
+ - JS/CSS files are now resolvable by esbuild, rollup, webpack, and Vite via the install generator (previously only worked with importmap/Sprockets).
20
+ - Pagination with `has_more: false` now correctly returns `false` (changed `||` to `??` to avoid truthy coercion).
21
+
5
22
  ## [0.1.0] - 2026-03-20
6
23
 
7
24
  ### Added
@@ -0,0 +1,151 @@
1
+ # remote_select — AI Coding Assistant Integration Guide
2
+
3
+ > Concise reference for LLMs and coding assistants integrating `remote_select` into Rails applications.
4
+
5
+ ## What it is
6
+
7
+ A **Ruby gem** (not an npm package) providing a searchable remote-data select for Rails forms. Zero JS dependencies, no jQuery, no Stimulus required. Works with Turbo.
8
+
9
+ ## Quick integration checklist
10
+
11
+ 1. `gem "remote_select"` in Gemfile → `bundle install`
12
+ 2. `rails generate remote_select:install` (copies JS + CSS, prints pipeline-specific instructions)
13
+ 3. Import JS in entry point (see pipeline table below)
14
+ 4. Import CSS in entry point (see pipeline table below)
15
+ 5. Add `remote_select()` helper in form view
16
+ 6. Create JSON search endpoint in controller
17
+
18
+ ## Asset pipeline setup
19
+
20
+ | Pipeline | JS import | CSS import |
21
+ |----------|-----------|------------|
22
+ | **esbuild / rollup / webpack** | `import "./remote_select"` in `application.js` | `@import "./remote_select";` in SCSS/CSS entry |
23
+ | **importmap (Rails 8)** | `pin "remote_select"` in `config/importmap.rb` + `import "remote_select"` | `*= require remote_select` in `application.css` |
24
+ | **Sprockets** | `//= require remote_select` | `*= require remote_select` |
25
+
26
+ For **esbuild/rollup/webpack**: the generator **must** be run — bare `import "remote_select"` will fail because the gem path is not in `node_modules`.
27
+
28
+ For **importmap/Sprockets**: the generator is optional — the engine registers asset paths automatically.
29
+
30
+ ## View helper signature
31
+
32
+ ```ruby
33
+ remote_select(form, attribute, endpoint, options = {})
34
+ ```
35
+
36
+ ### Parameters
37
+
38
+ - `form` — Rails form builder instance
39
+ - `attribute` — model attribute symbol (e.g., `:company_id`)
40
+ - `endpoint` — URL string returning JSON (e.g., `search_companies_path`)
41
+ - `options` — Hash:
42
+
43
+ | Key | Type | Default |
44
+ |-----|------|---------|
45
+ | `:selected_value` | String/Integer | `nil` |
46
+ | `:selected_text` | String | `nil` |
47
+ | `:min_chars` | Integer | `2` |
48
+ | `:debounce_delay` | Integer | `250` |
49
+ | `:placeholder` | String | `"Type to search..."` |
50
+ | `:per_page` | Integer | `20` |
51
+ | `:depends_on` | String | `nil` (CSS selectors, comma-separated) |
52
+ | `:clear_on_dependency_change` | Boolean | `true` |
53
+ | `:empty_text` | String | `"No results found"` |
54
+ | `:loading_text` | String | `"Loading..."` |
55
+ | `:html` | Hash | `{}` (extra attrs on hidden input) |
56
+
57
+ ## Typical form usage
58
+
59
+ ```erb
60
+ <%= form_with model: @article do |form| %>
61
+ <%= remote_select(form, :company_id, search_companies_path,
62
+ selected_value: @article.company_id,
63
+ selected_text: @article.company&.name,
64
+ placeholder: "Search companies...",
65
+ min_chars: 2) %>
66
+ <% end %>
67
+ ```
68
+
69
+ ## Dependent select pattern
70
+
71
+ ```erb
72
+ <%= form.select :country_id, countries_options, {}, { id: "country-select" } %>
73
+ <%= remote_select(form, :city_id, search_cities_path,
74
+ depends_on: "#country-select",
75
+ clear_on_dependency_change: true) %>
76
+ ```
77
+
78
+ The `country_id` param is automatically appended to the fetch URL when the parent changes.
79
+
80
+ ## Required JSON endpoint
81
+
82
+ The controller action receives `q`, `page`, `per_page` as query params (plus any dependency params). It must return:
83
+
84
+ ```ruby
85
+ def search_companies
86
+ query = params[:q].to_s.strip
87
+ page = (params[:page] || 1).to_i
88
+ per_page = (params[:per_page] || 20).to_i
89
+
90
+ scope = Company.order(:name)
91
+ scope = scope.where("name ILIKE ?", "%#{query}%") if query.present?
92
+
93
+ total = scope.count
94
+ results = scope.limit(per_page).offset((page - 1) * per_page)
95
+
96
+ render json: {
97
+ results: results.map { |c| { id: c.id, text: c.name } },
98
+ has_more: (page * per_page) < total
99
+ }
100
+ end
101
+ ```
102
+
103
+ ### Accepted response shapes
104
+
105
+ ```json
106
+ { "results": [{ "id": 1, "text": "Name" }], "has_more": true }
107
+ ```
108
+
109
+ ```json
110
+ { "results": [{ "id": 1, "text": "Name" }], "pagination": { "more": true } }
111
+ ```
112
+
113
+ Both formats work. Each result object **must** have `id` and `text` keys.
114
+
115
+ ## Route example
116
+
117
+ ```ruby
118
+ # config/routes.rb
119
+ get "companies/search", to: "companies#search", as: :search_companies
120
+ ```
121
+
122
+ ## Theming
123
+
124
+ Override CSS custom properties — no need to edit the stylesheet:
125
+
126
+ ```css
127
+ .remote-select-container {
128
+ --rs-focus-border-color: #198754;
129
+ --rs-border-radius: 0.25rem;
130
+ --rs-dropdown-max-height: 400px;
131
+ }
132
+ ```
133
+
134
+ ## Common mistakes to avoid
135
+
136
+ 1. **Do NOT** use bare `import "remote_select"` with esbuild/webpack — run the generator first
137
+ 2. **Do NOT** add an npm package — this is a Ruby gem only
138
+ 3. **Do NOT** forget the `text` key in JSON results — `name` won't work, must be `text`
139
+ 4. **Do NOT** skip `selected_text` when setting `selected_value` — both are needed for preselection display
140
+ 5. **Do NOT** add Stimulus controllers — the component auto-initializes on `DOMContentLoaded` and `turbo:load`
141
+
142
+ ## JS API (for programmatic use)
143
+
144
+ ```js
145
+ const el = document.querySelector('[data-remote-select]');
146
+ // Access instance if needed:
147
+ const rs = new RemoteSelect(el, { endpoint: '/search', minChars: 1 });
148
+ rs.setParam('category', 'books');
149
+ rs.clearSelection();
150
+ rs.destroy();
151
+ ```
data/README.md CHANGED
@@ -16,7 +16,8 @@ Replaces Select2 / Tom Select for the common case of "type to search a remote li
16
16
  - **Preselected values** — display preselected options on page load
17
17
  - **i18n** — default strings resolved via `I18n.t` with English fallbacks
18
18
  - **Turbo compatible** — full-page navigation and Turbo Frames, no Stimulus required
19
- - **No dependencies** — pure vanilla JavaScript
19
+ - **Select2-compatible** — accepts Select2 `pagination.more` response format out of the box
20
+ - **No dependencies** — pure vanilla JavaScript (ES2020)
20
21
 
21
22
  ## Installation
22
23
 
@@ -32,9 +33,26 @@ Run:
32
33
  bundle install
33
34
  ```
34
35
 
35
- ### JavaScript
36
+ Then run the install generator to copy JS and CSS into your app:
36
37
 
37
- **Importmap** (Rails 8 default):
38
+ ```bash
39
+ rails generate remote_select:install
40
+ ```
41
+
42
+ The generator copies two files and prints setup instructions for your specific asset pipeline (esbuild, importmap, Sprockets, etc.).
43
+
44
+ Use `--force` to overwrite previously copied files when updating the gem:
45
+
46
+ ```bash
47
+ rails generate remote_select:install --force
48
+ ```
49
+
50
+ ### Manual setup (if you prefer not to use the generator)
51
+
52
+ <details>
53
+ <summary>Importmap (Rails 8 default)</summary>
54
+
55
+ No generator needed — importmap resolves from the engine automatically.
38
56
 
39
57
  ```ruby
40
58
  # config/importmap.rb
@@ -46,32 +64,47 @@ pin "remote_select", to: "remote_select.js"
46
64
  import "remote_select"
47
65
  ```
48
66
 
49
- **esbuild / rollup / webpack**:
67
+ For CSS, add to `application.css`:
50
68
 
51
- ```js
52
- import "remote_select"
69
+ ```css
70
+ /*= require remote_select */
53
71
  ```
54
72
 
55
- **Sprockets** (`application.js`):
73
+ </details>
74
+
75
+ <details>
76
+ <summary>Sprockets (no bundler)</summary>
56
77
 
57
78
  ```js
79
+ // app/assets/javascripts/application.js
58
80
  //= require remote_select
59
81
  ```
60
82
 
61
- ### Stylesheet
83
+ ```css
84
+ /* app/assets/stylesheets/application.css */
85
+ /*= require remote_select */
86
+ ```
62
87
 
63
- **Sass / SCSS**:
88
+ </details>
64
89
 
65
- ```scss
66
- @import 'remote_select';
90
+ <details>
91
+ <summary>esbuild / rollup / webpack (jsbundling-rails)</summary>
92
+
93
+ Run the generator first (see above), then:
94
+
95
+ ```js
96
+ // app/javascript/application.js
97
+ import "./remote_select"
67
98
  ```
68
99
 
69
- **Sprockets** (`application.css`):
100
+ For CSS, add to your SCSS/CSS entry:
70
101
 
71
- ```css
72
- /*= require remote_select */
102
+ ```scss
103
+ @import './remote_select';
73
104
  ```
74
105
 
106
+ </details>
107
+
75
108
  ## Usage
76
109
 
77
110
  ### Basic
@@ -144,7 +177,9 @@ en:
144
177
 
145
178
  ## Backend endpoint
146
179
 
147
- Your action must return JSON:
180
+ Your action must return JSON with a `results` array. Pagination is optional.
181
+
182
+ ### Recommended format
148
183
 
149
184
  ```ruby
150
185
  def search_companies
@@ -166,8 +201,6 @@ def search_companies
166
201
  end
167
202
  ```
168
203
 
169
- ### Required response shape
170
-
171
204
  ```json
172
205
  {
173
206
  "results": [{ "id": 1, "text": "Acme Corp" }],
@@ -176,6 +209,19 @@ end
176
209
  }
177
210
  ```
178
211
 
212
+ ### Select2-compatible format (also accepted)
213
+
214
+ If you're migrating from Select2, your existing endpoints work as-is:
215
+
216
+ ```json
217
+ {
218
+ "results": [{ "id": 1, "text": "Acme Corp" }],
219
+ "pagination": { "more": true }
220
+ }
221
+ ```
222
+
223
+ Both `has_more` and `pagination.more` are recognized. If neither is present, pagination is disabled.
224
+
179
225
  ## Theming
180
226
 
181
227
  All visual properties are CSS custom properties on `.remote-select-container`. Override without touching the stylesheet:
@@ -190,6 +236,23 @@ All visual properties are CSS custom properties on `.remote-select-container`. O
190
236
  }
191
237
  ```
192
238
 
239
+ ### Available CSS custom properties
240
+
241
+ | Property | Default | Description |
242
+ |----------|---------|-------------|
243
+ | `--rs-text-color` | `#212529` | Main text color |
244
+ | `--rs-muted-color` | `#6c757d` | Placeholder / message color |
245
+ | `--rs-bg-color` | `#fff` | Background |
246
+ | `--rs-border-color` | `#dee2e6` | Border color |
247
+ | `--rs-border-radius` | `0.375rem` | Corner radius |
248
+ | `--rs-focus-border-color` | `#86b7fe` | Focus ring border |
249
+ | `--rs-focus-shadow` | blue 0.25rem | Focus ring shadow |
250
+ | `--rs-item-hover-bg` | `#e9ecef` | Hovered item background |
251
+ | `--rs-error-color` | `#dc3545` | Error text color |
252
+ | `--rs-zindex` | `1050` | Dropdown z-index |
253
+ | `--rs-dropdown-max-height` | `300px` | Dropdown max height |
254
+ | `--rs-results-max-height` | `250px` | Results list max height |
255
+
193
256
  ## JavaScript API
194
257
 
195
258
  ```js
@@ -209,7 +272,7 @@ rs.destroy(); // remove all listeners + DOM
209
272
 
210
273
  ## Browser support
211
274
 
212
- Modern browsers (Chrome, Firefox, Safari, Edge). Requires ES2020 (`??`, `async/await`, `AbortController`). No IE11.
275
+ Modern browsers (Chrome, Firefox, Safari, Edge). Requires ES2020 (`??`, `?.`, `async/await`, `AbortController`). No IE11.
213
276
 
214
277
  ## License
215
278
 
@@ -1,3 +1,4 @@
1
+ /* remote_select v0.2.0 — https://github.com/GhennadiiMir/remote_select */
1
2
  /* RemoteSelect component styles */
2
3
 
3
4
  /* ── Theming variables ────────────────────────────────────────────────────── */
@@ -1,3 +1,4 @@
1
+ // remote_select v0.2.0 — https://github.com/GhennadiiMir/remote_select
1
2
  /**
2
3
  * RemoteSelect - Vanilla JS remote data select component
3
4
  * A lightweight replacement for select2/tom-select with remote data source
@@ -263,7 +264,7 @@ class RemoteSelect {
263
264
 
264
265
  const data = await response.json();
265
266
  this.results = append ? [...this.results, ...data.results] : data.results;
266
- this.hasMore = data.has_more || false;
267
+ this.hasMore = data.has_more ?? data.pagination?.more ?? false;
267
268
  this._renderResults(append);
268
269
  } catch (err) {
269
270
  if (err.name === 'AbortError') return; // request was intentionally cancelled
@@ -0,0 +1,119 @@
1
+ require "rails/generators/base"
2
+
3
+ module RemoteSelect
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ desc "Copy remote_select JS and CSS into your application"
7
+
8
+ class_option :force, type: :boolean, default: false,
9
+ desc: "Overwrite existing files"
10
+
11
+ def copy_javascript
12
+ source = RemoteSelect::Engine.root.join("app/javascript/remote_select.js")
13
+ dest = File.join(destination_root, "app/javascript/remote_select.js")
14
+
15
+ if File.exist?(dest) && !options[:force]
16
+ say_status :skip, "app/javascript/remote_select.js already exists (use --force to overwrite)", :yellow
17
+ else
18
+ version_comment = "// remote_select v#{RemoteSelect::VERSION} — copied by rails generate remote_select:install\n"
19
+ create_file "app/javascript/remote_select.js", version_comment + source.read
20
+ end
21
+ end
22
+
23
+ def copy_stylesheet
24
+ source = RemoteSelect::Engine.root.join("app/assets/stylesheets/remote_select.css")
25
+ dest = File.join(destination_root, "app/assets/stylesheets/remote_select.css")
26
+
27
+ if File.exist?(dest) && !options[:force]
28
+ say_status :skip, "app/assets/stylesheets/remote_select.css already exists (use --force to overwrite)", :yellow
29
+ else
30
+ version_comment = "/* remote_select v#{RemoteSelect::VERSION} — copied by rails generate remote_select:install */\n"
31
+ create_file "app/assets/stylesheets/remote_select.css", version_comment + source.read
32
+ end
33
+ end
34
+
35
+ def print_post_install
36
+ say ""
37
+ say "remote_select files copied successfully!", :green
38
+ say ""
39
+
40
+ js_pipeline = detect_js_pipeline
41
+ case js_pipeline
42
+ when :importmap
43
+ say "Importmap detected. Add to config/importmap.rb:"
44
+ say ' pin "remote_select"'
45
+ say ""
46
+ say "And in app/javascript/application.js:"
47
+ say ' import "remote_select"'
48
+ when :esbuild, :rollup, :webpack
49
+ say "#{js_pipeline} detected. Add to app/javascript/application.js:"
50
+ say ' import "./remote_select"'
51
+ else
52
+ say "Add to your JS entry point:"
53
+ say ' import "./remote_select"'
54
+ end
55
+
56
+ say ""
57
+
58
+ css_pipeline = detect_css_pipeline
59
+ case css_pipeline
60
+ when :cssbundling
61
+ say "cssbundling (sass) detected. Add to your main SCSS/CSS entry:"
62
+ say ' @import "./remote_select";'
63
+ when :sprockets
64
+ say "Sprockets detected. Add to application.css:"
65
+ say ' *= require remote_select'
66
+ else
67
+ say "Add to your CSS entry point:"
68
+ say ' @import "./remote_select";'
69
+ say ' or (Sprockets): *= require remote_select'
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def app_root
76
+ Pathname.new(destination_root)
77
+ end
78
+
79
+ def detect_js_pipeline
80
+ return :importmap if app_root.join("config/importmap.rb").exist?
81
+
82
+ pkg = read_package_json
83
+ return nil unless pkg
84
+
85
+ build_script = pkg.dig("scripts", "build") || ""
86
+ deps = (pkg["dependencies"] || {}).merge(pkg["devDependencies"] || {})
87
+
88
+ return :esbuild if deps.key?("esbuild") || build_script.include?("esbuild")
89
+ return :rollup if deps.key?("rollup") || build_script.include?("rollup")
90
+ return :webpack if deps.key?("webpack") || build_script.include?("webpack")
91
+
92
+ nil
93
+ end
94
+
95
+ def detect_css_pipeline
96
+ pkg = read_package_json
97
+ if pkg
98
+ build_css = pkg.dig("scripts", "build:css") || ""
99
+ deps = (pkg["dependencies"] || {}).merge(pkg["devDependencies"] || {})
100
+ return :cssbundling if build_css.include?("sass") || deps.key?("sass")
101
+ end
102
+
103
+ if app_root.join("app/assets/stylesheets/application.css").exist?
104
+ return :sprockets
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ def read_package_json
111
+ path = app_root.join("package.json")
112
+ return nil unless path.exist?
113
+ JSON.parse(path.read)
114
+ rescue JSON::ParserError
115
+ nil
116
+ end
117
+ end
118
+ end
119
+ end
@@ -1,3 +1,3 @@
1
1
  module RemoteSelect
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: remote_select
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ghennadii Mir
@@ -35,11 +35,13 @@ extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
37
  - CHANGELOG.md
38
+ - COPILOT_INSTRUCTIONS.md
38
39
  - LICENSE
39
40
  - README.md
40
41
  - app/assets/stylesheets/remote_select.css
41
42
  - app/javascript/remote_select.js
42
43
  - config/locales/en.yml
44
+ - lib/generators/remote_select/install_generator.rb
43
45
  - lib/remote_select.rb
44
46
  - lib/remote_select/engine.rb
45
47
  - lib/remote_select/version.rb