Skip to content

Commit 279ec4f

Browse files
committed
Add datalist component for native autocomplete inputs
Renders <input> + <datalist> for free-text input with browser-native autocomplete suggestions. No JavaScript required. Accepts the same option formats as select (arrays, hashes, single values, AR relations). Example: Field(:time_zone).datalist(*ActiveSupport::TimeZone.all.map(&:name))
1 parent 4f9bdc8 commit 279ec4f

5 files changed

Lines changed: 114 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
- **Choices module** (`Superform::Rails::Choices`) — `Choices::Choice` holds per-option state,
2020
`Choices::Mapper` (renamed from `OptionMapper`) maps option args to `(value, text)` pairs.
2121
- **Unique DOM ids** for radio and checkbox groups via `DOM#id(*suffixes)`. Prevents duplicate ids in valid HTML and allows labels to target individual inputs.
22+
- **Datalist component** with `Field(:time_zone).datalist(*ActiveSupport::TimeZone.all.map(&:name))`.
23+
Renders a native `<input>` + `<datalist>` for free-text input with autocomplete suggestions —
24+
no JavaScript required. Accepts the same option formats as `select`. Block form available
25+
for custom options.
2226
- **Select improvements**: blank options (`nil`) at any position, `multiple: true` support with hidden input for empty submissions, ActiveRecord relations as options.
2327
- **Preview server** — run `bin/preview` to view example forms at localhost:3000 with hot-reloading.
2428

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,10 @@ class JobPostingForm < Components::Form
466466
end
467467
end
468468

469+
# Datalist — free-text input with autocomplete suggestions.
470+
# No JavaScript required. The browser handles filtering natively.
471+
Field(:time_zone).datalist(*ActiveSupport::TimeZone.all.map(&:name))
472+
469473
# File upload (remember to set enctype on the form).
470474
div do
471475
Field(:job_description_pdf).label { "Upload job description" }
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module Superform
2+
module Rails
3+
module Components
4+
class Datalist < Field
5+
def initialize(field, options: [], **attributes)
6+
super(field, **attributes)
7+
@options = options
8+
end
9+
10+
def view_template(&block)
11+
datalist_id = DOM.join(dom.id, "datalist")
12+
input(list: datalist_id, **attributes)
13+
datalist(id: datalist_id) do
14+
if block
15+
yield self
16+
else
17+
options(*@options)
18+
end
19+
end
20+
end
21+
22+
def options(*collection)
23+
Choices::Mapper.new(collection).each do |value, text|
24+
if value == text
25+
option(value: value)
26+
else
27+
option(value: value) { text }
28+
end
29+
end
30+
end
31+
end
32+
end
33+
end
34+
end

lib/superform/rails/field.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ def select(*options, multiple: false, **attributes, &)
5555
)
5656
end
5757

58+
def datalist(*options, **attributes, &block)
59+
Components::Datalist.new(field, options:, **attributes, &block)
60+
end
61+
5862
def errors
5963
object.errors[key]
6064
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
RSpec.describe Superform::Rails::Components::Datalist, type: :view do
2+
let(:object) { double("object", time_zone: "Pacific Time (US & Canada)") }
3+
let(:field) do
4+
Superform::Rails::Field.new(:time_zone, parent: nil, object: object)
5+
end
6+
7+
it "renders an input linked to a datalist" do
8+
html = render(field.datalist("Eastern", "Central", "Pacific"))
9+
10+
expect(html).to include('<input')
11+
expect(html).to include('list="time_zone_datalist"')
12+
expect(html).to include('<datalist id="time_zone_datalist">')
13+
expect(html).to include('<option value="Eastern">')
14+
expect(html).to include('<option value="Central">')
15+
expect(html).to include('<option value="Pacific">')
16+
end
17+
18+
it "sets standard field attributes on the input" do
19+
html = render(field.datalist("Eastern"))
20+
21+
expect(html).to include('id="time_zone"')
22+
expect(html).to include('name="time_zone"')
23+
end
24+
25+
it "passes through HTML attributes to the input" do
26+
html = render(field.datalist("Eastern", class: "form-input", placeholder: "Start typing..."))
27+
28+
expect(html).to include('class="form-input"')
29+
expect(html).to include('placeholder="Start typing..."')
30+
end
31+
32+
it "accepts value/label pairs" do
33+
html = render(field.datalist(["est", "Eastern"], ["cst", "Central"]))
34+
35+
expect(html).to include('value="est"')
36+
expect(html).to include("Eastern")
37+
expect(html).to include('value="cst"')
38+
expect(html).to include("Central")
39+
end
40+
41+
it "accepts a hash of options" do
42+
html = render(field.datalist({ "est" => "Eastern", "cst" => "Central" }))
43+
44+
expect(html).to include('value="est"')
45+
expect(html).to include("Eastern")
46+
end
47+
48+
it "renders with a block for custom options" do
49+
component = field.datalist do |d|
50+
d.options("Eastern", "Central", "Pacific")
51+
end
52+
html = render(component)
53+
54+
expect(html).to include('<input')
55+
expect(html).to include('list="time_zone_datalist"')
56+
expect(html).to include('<option value="Eastern">')
57+
expect(html).to include('<option value="Central">')
58+
expect(html).to include('<option value="Pacific">')
59+
end
60+
61+
it "works as a one-liner for time zones" do
62+
zones = ["Eastern Time (US & Canada)", "Central Time (US & Canada)", "Pacific Time (US & Canada)"]
63+
html = render(field.datalist(*zones))
64+
65+
expect(html).to include('value="Eastern Time (US & Canada)"')
66+
expect(html).to include('value="Pacific Time (US & Canada)"')
67+
end
68+
end

0 commit comments

Comments
 (0)