Skip to content

select blank_option bug #72

@nimmolo

Description

@nimmolo

blank_option does not render an empty-string value attribute for the option, so when you pass text for the label, HTML will actually send that text as the value of the option.

Claude flagged this in one of my forms, and I confirmed this is the case (see below), so it seems worth addressing. Here's what it said:

⏺ Yes — it's a real bug in upstream Superform, but only when you pass a label to blank_option. Here's the matrix:

  ┌──────────────────────────────┬─────────────────────────────────────┬─────────────────┐                                                   
  │             Call             │               Renders               │ Browser submits │    
  ├──────────────────────────────┼─────────────────────────────────────┼─────────────────┤                                                   
  │ blank_option (no block)      │ <option selected></option>          │ "" ✓            │                                                  
  ├──────────────────────────────┼─────────────────────────────────────┼─────────────────┤                                                   
  │ blank_option { "Pick one…" } │ <option selected>Pick one…</option> │ "Pick one…" ✗   │                                                   
  └──────────────────────────────┴─────────────────────────────────────┴─────────────────┘

The unlabeled case works by accident via HTML's "if option has no value, fall back to text content" rule — empty text content means submitted value is "". So upstream maintainers (and most callers) probably never noticed: they use blank_option without a block as a visual separator, and submissions look right. The moment someone adds a hint label, it silently breaks.

Use cases where upstream's behavior is actually fine:

  • Purely visual blank option, no label (e.g., a single empty row at the top of a dropdown).
  • Where the developer wants the form to submit the option's label as the value — niche but legal (e.g., a dropdown where the labels
    themselves are valid params).

Use cases where it's broken:

  • blank_option { "Pick one…" } as a hint — submits "Pick one…" instead of "".
  • blank_option { "(No Project)" } (our pattern) — submits "(No Project)" instead of "".

The one-line upstream fix would be:

  def blank_option(&)                                                                                                                        
    option(selected: field.value.nil?, value: "", &)                                          
  end                                                                                                                                        

I'm happy to submit a PR for this if it sounds right to you. Until then, I have an override in my code to submit a manual blank option with ["", "label"] by coercing nil to "", instead of using superform's blank_option.


Here's my indication this is in fact what happens:

⏺ Confirmed empirically in headless Chromium:

  {"field_slip[project_id]":"(No Project)",
    "other_field":""}                                                                                 
  • <option>(No Project)</option> (no value attr) selected → FormData entry is the literal string "(No Project)".
  • <option value="">(Empty value)</option> (explicit empty value) selected → FormData entry is "".

So Rails would receive params[:field_slip][:project_id] == "(No Project)" for the first case — which is almost certainly never what the developer wanted. The browser does the text-content fallback per the HTML Living Standard (constructing form data set, step where an option's value is value attribute if present, otherwise text content).

The concise repro:

Superform::Rails::Components::Select#blank_option(&) invokes option(selected: ..., &) without a value: argument. Phlex's HTML DSL then omits the attribute entirely, producing <option>label</option>. When a label block is passed (blank_option { "Pick one…" }), the browser submits the label text as the form value rather than the empty string the developer presumably intended. Confirmed in headless Chromium via FormData: selected <option>(No Project)</option> produces {"field": "(No Project)"}.

▎ Suggested fix: option(selected: field.value.nil?, value: "", &). Doesn't regress the unlabeled case (<option selected value=""> still submits "").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions