From 2607d0bc4e1799e10a1de1214cae5440781ff605 Mon Sep 17 00:00:00 2001 From: Mikael Henriksson Date: Wed, 12 Aug 2026 12:00:12 +0200 Subject: [PATCH] fix(file_input): append [] to the name for multiple-file inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rails file_field parity: without the [] suffix the browser collapses a multi-file selection into ONE scalar param, which a host app's params.expect(attr: []) silently discards — uploads no-op with a success response (bit cosmos yoga in production, Aug 2026). Applies to the standalone component, the Plain variant, and the form builder path; names already ending in [] are left alone. --- CHANGELOG.md | 9 +++++++ lib/forms/file_input.rb | 5 +++- spec/forms/file_input_spec.rb | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 spec/forms/file_input_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed25f1..e8aa3fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **`FileInput` multiple uploads submit an array** (Rails `file_field` parity): + with `multiple:` set, the input name now gets `[]` appended (unless already + present), for the standalone component, the `Plain` variant, and the form + builder path. Without it the browser collapsed a multi-file selection into + ONE scalar param, which a host app's `params.expect(attr: [])` silently + discarded — uploads no-opped with a success response. + ### Added - **`checkbox_group` — batched checkbox group for array-valued fields** (the diff --git a/lib/forms/file_input.rb b/lib/forms/file_input.rb index e404d89..6c17313 100644 --- a/lib/forms/file_input.rb +++ b/lib/forms/file_input.rb @@ -8,7 +8,10 @@ class FileInput < Phlex::HTML def initialize(*modifiers, name: nil, id: nil, multiple: false, accept: nil, error: false, disabled: false, required: false, full_width: true, **attributes) @modifiers = normalize_modifiers(modifiers) - @name = name + # Rails file_field parity: a multiple input needs an array param name, or + # the browser collapses the selection into one scalar file — which a host + # app's `params.expect(attr: [])` then silently discards. + @name = multiple && name && !name.to_s.end_with?("[]") ? "#{name}[]" : name @id = id @multiple = multiple @accept = accept diff --git a/spec/forms/file_input_spec.rb b/spec/forms/file_input_spec.rb new file mode 100644 index 0000000..dcbc6d9 --- /dev/null +++ b/spec/forms/file_input_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "spec_helper" + +# Rails' file_field appends [] to the input name when multiple is set; without +# it the browser collapses a multi-file selection into ONE scalar param, which +# a host app's `params.expect(attr: [])` silently discards — uploads no-op +# with a success response (cosmos production, Aug 2026). +describe Forms::FileInput do + it "keeps the plain name for single-file inputs" do + output = render_component(described_class.new(name: "user[avatar]")) + + expect(output).to include('name="user[avatar]"') + expect(output).not_to include("multiple") + end + + it "appends [] to the name when multiple (Rails file_field parity)" do + output = render_component(described_class.new(name: "retreat[gallery]", multiple: true)) + + expect(output).to include('name="retreat[gallery][]"') + expect(output).to include("multiple") + end + + it "does not double-append when the caller already passed []" do + output = render_component(described_class.new(name: "retreat[gallery][]", multiple: true)) + + expect(output).to include('name="retreat[gallery][]"') + expect(output).not_to include("[][]") + end + + it "appends [] through the form builder too" do + user = build_model(:user, name: "Ada") + output = PhlexHelpers::FormContext.new( + model: user, form_args: {}, + form_block: ->(f) { f.FileInput(:name, multiple: true) } + ).call + + expect(output).to include('name="user[name][]"') + end + + it "applies the same normalization to the Plain variant" do + output = render_component(Forms::Plain::FileInput.new(name: "retreat[gallery]", multiple: true)) + + expect(output).to include('name="retreat[gallery][]"') + end +end