Skip to content

Conversation

@nimmolo
Copy link

@nimmolo nimmolo commented Nov 13, 2025

This is a reboot of PR #33. It intends to support the multiple HTML spec for <select> elements.

# Multiple select with automatic [] appending and hidden input
field(:tag_ids).select(
  [1, "Ruby"], [2, "Rails"], [3, "Phlex"],
  multiple: true
)
# Renders: <input type="hidden" name="tag_ids[]" value="">
#          <select name="tag_ids[]" multiple>...</select>
  • Handles a multiple kwarg to the select method. Full backwards compatibility with the current API of select.
  • Field naming: names the select multiple field correctly — i.e., using square brackets in a way that does not interfere with parent field notation. (It has to prevent double [] when multiple select is used inside collections.) See this conversation. Tests all permutations I could think of.
  • Adds hidden field when multiple, to handle the way browsers handle blank selects (form value may not be submitted unless hidden field present)
  • Renames the internal Select component param for the array of options from collection to options (to avoid confusion with .collection method, which creates FieldCollections). Backwards compatible, both keywords accepted.
  • Adds tests for generated HTML.
  • Updates the README with examples of the new select API. Also updates the rest of the README with consistent syntax (field vs Field)
  • Updates the GEMSPEC to require Ruby 2.7, since the Superform codebase already uses anonymous arg forwarding all over the place (so it already effectively requires 2.7).

Note, I realize you proposed a separate multiple_select component method [in the discussion here] (#33 (comment)) and mentioned that also below, but I would suggest this is not really necessary or desirable, since the current PR maintains backwards compatibility with the current method signature. Keeping the select logic in one component seems to be the most intuitive and elegant way. In the select HTML element spec, multiple is just an attribute, not a separate element.

@nimmolo nimmolo changed the title Add multiple and include_blank keywords to select Add multiple and include_blank keywords to select Nov 13, 2025
@nimmolo
Copy link
Author

nimmolo commented Nov 14, 2025

more details:

Multiple select support
# Multiple select with automatic [] appending and hidden input
field(:tag_ids).select(
  [1, "Ruby"], [2, "Rails"], [3, "Phlex"],
  multiple: true
)
# Renders: <input type="hidden" name="tag_ids[]" value="">
#          <select name="tag_ids[]" multiple>...</select>
Include blank option
# Add blank option at the start
field(:country).select(
  nil, [1, "USA"], [2, "Canada"], [3, "Mexico"]
)
API harmonized with with radio component PR #65
# Positional arguments
field(:role_id).select([1, 'Admin'], [2, 'Editor'], [3, 'Viewer'])

# ActiveRecord relations (wrapped in array)
field(:author_id).select(User.select(:id, :name))

Notes

Multiple select handling
  • Array value detection (line 50): Array(field.value).include?(key) works for both single and multiple
  • Smart field naming (lines 74-78): Appends [] unless already in Field collection
  • Hidden input (lines 32-35): Ensures empty submissions send a value
  • Collection support: Handles user[orders][0][tag_ids][] correctly
Field naming logic
# Single select
name="role_id"

# Multiple select
name="role_ids[]"

# Multiple select in collection
name="user[orders][0][tag_ids][]"

# Multiple select in Field collection (parent is Field)
name="users[][]"  # No extra [] appended

Form integration tests

Integration tests to confirm Select works correctly in Rails forms:

Single Select
  • Field naming in collections: user[orders][0][item_id]
  • Pre-selection: Options correctly selected based on model values
  • Form submission cycle: Values round-trip correctly
Multiple Select
  • Array value handling: [1, 3] correctly selects multiple options
  • Field naming: user[orders][0][tag_ids][] with hidden inputs
  • Form submission cycle: Array values round-trip correctly
  • Empty submissions: Hidden input ensures params are sent

@bradgessler
Copy link
Contributor

Feedback is similar to the radio buttons PR

Eliminate the options keyword argument. Positional arguments handle this.

Eliminate the include_blank argument. A nil can be passed into the positional argument to achieve that via Field(:items).select(nil, *items). If this isn't clear, you might update the docs.

Similar to Field(:blah).radio().buttons.each vs Field(:blah).radio_buttons.each, we might separate out select_multiple with its own method like Field(:blah).select_multiple. I'm preferring the latter since it's very straight forward to explain to somebody verbally or in docs "Hey, if you want to select multiple, you'll have to call Field(:foo).select_multipleand it doesn't conflict with theselect` input method.

@nimmolo
Copy link
Author

nimmolo commented Nov 19, 2025

PR updated to reflect feedback.

Question (also asked on #65, but relevant here)

In Select currently, the field(:foo).select method seems to accept the "options" either as positional args, or sent under the kwarg "collection".

# undocumented kwarg on main
field(:foo).select(collection: [[1, 'Admin'], [2, 'Editor']])

# positional  
field(:foo).select([1, 'Admin'], [2, 'Editor'])

Is this kwarg available just because of the way it's initialized? Or is this considered a feature undocumented in the README? Maybe nobody uses it, can add a deprecation notice if passed directly.

@nimmolo nimmolo changed the title Add multiple and include_blank keywords to select Add multiple keyword/functionality to select Nov 19, 2025
@nimmolo
Copy link
Author

nimmolo commented Nov 25, 2025

An unexpected API gotcha i'm sure you're aware of:

Superform select uses [value, label] ordering for select options, which is the opposite of Rails' options_for_select [label, value].

This is a real gotcha for refactoring from ERB, it means devs have to manually flip all the arrays. It would be good to change this API. I can't imagine how to change it without breaking people's existing select implementations, though.

Copy link
Contributor

@bradgessler bradgessler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit feels "too big" for the functionality it implements. Changes to the README, 500+ LOC specs, and the Ruby version change feels like an LLM got carried away.

As far as the actual code goes, I left comments inline for a few things to change.

README.md Outdated
Field(:body).textarea(rows: 10)
Field(:publish_at).date
Field(:featured).checkbox
field(:title).text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Field methods should remain capitalized.

# Hidden input ensures a value is sent even when all options are
# deselected in a multiple select
if @multiple
hidden_name = field.parent.is_a?(Superform::Field) ? dom.name : "#{dom.name}[]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I use "#{dom.name}[]" in a collection somewhere. Could that same class be used here? If it doesn't make sense to drop that class in here then "#{dom.name}[]" should be dropped into a helper class method somewhere and called so it goes through the same code path.

Copy link
Author

@nimmolo nimmolo Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. This is something we probably want to be reusable, in light of the checkbox/radio group PR coming up.

Does it make sense to add a method to the DOM class called array_name?

# Returns the name with `[]` appended for array/multiple value fields.
# Used by multiple selects, checkbox groups, etc.
def array_name
  "#{name}[]"
end

Components::Select.new(field, attributes:, collection:, &)
def select(*options, **attributes, &)
# Extract select-specific parameters from attributes
multiple = attributes.delete(:multiple) || false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move into the method definition: def select(*options, multiple: false, **attributes, &)

No need to extract.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood - done.

spec.homepage = "https://github.com/rubymonolith/superform"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
spec.required_ruby_version = ">= 2.7.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work with Ruby 2.6. If it doesn't, bump Ruby versions in different commits.

Copy link
Author

@nimmolo nimmolo Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this change to a new PR branch.

map_options(collection).each do |key, value|
option(selected: field.value == key, value: key) { value }
# Handle both single values and arrays (for multiple selects)
selected = Array(field.value).include?(key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allocates a new array per iteration. Move selected outside of the loop if it doesn't need to be there and then check if the key is included in selected.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

select(**attributes) { options(*@collection) }
select(**attributes) do
# If first option is nil, render a blank option
include_blank = @options.first.nil?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised to see this. What if blank is the last option? Or first? It could also be 3rd, 5th, and 8th.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Adjusted to handle nil at any position

@nimmolo
Copy link
Author

nimmolo commented Nov 30, 2025

That's all true about the LLM, but

  • the Ruby version change was brought up by @mvkampen on the original multiple select PR. I just did a separate PR for the version change, that seems more appropriate.
  • The README currently seems to show confusing (to me) syntax inconsistencies around field/Field. This could also be a separate PR, happy to do it.
  • I asked Claude to write specs for every possible combination of caller args, because I dont want to break your gem! 😅 Let me know what seems superfluous, can remove.

nimmolo and others added 8 commits November 29, 2025 23:19
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Avoids allocating a new array on each iteration when checking
if a value is selected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Previously nil was only handled as the first option. Now nil
renders as a blank option wherever it appears in the collection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Cleaner than extracting from attributes hash.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The capitalized Field is intentional - it's the Phlex Kit syntax.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The capitalized Field is intentional - it's the Phlex Kit syntax.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants