Skip to content

[Dropzone] Add a placeholder option to DropzoneType - #4148

Merged
Kocal merged 1 commit into
symfony:3.xfrom
ker0x:feat/dropzone-placeholder-option
Nov 2, 2026
Merged

Kocal merged 1 commit into
symfony:3.xfrom
ker0x:feat/dropzone-placeholder-option

Conversation

@ker0x

@ker0x ker0x commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor
Q A
Bug fix? no
New feature? yes
Deprecations? yes
Documentation? yes
Issues #4142
License MIT

Fifth step of the RFC in #4142.

The placeholder of the drop zone comes from attr.placeholder. This has two problems:

  • attr is also rendered on the <input type="file">, so the input gets a placeholder attribute, which is not valid on a file input.
  • The default text is the default value of the whole attr option, so the text and the input's attributes cannot be set apart.

This adds a placeholder option (string, TranslatableInterface or false), translated with the form's translation domain like before (a TranslatableInterface is always translated, also with translation_domain: false). false shows no text. The text stays in attr.placeholder of the form view, where the theme reads it and leaves it out of the input's attributes: the placeholder is no longer rendered on the input, while a placeholder passed to form_widget() and themes copied from an older version keep working. Its default keeps today's behavior: 'Drag and drop or browse' when the field has no attr, and attr.placeholder (or no text) when the field passes its own attr, which used to replace the default text.

attr.placeholder keeps working as the text when the placeholder option is not passed, and is deprecated in favor of the new option. The deprecation is triggered once per field, when the options are resolved. src/Dropzone/UPGRADE.md explains the migration.

Field options Before After
none default text, placeholder attribute on input default text
'attr' => ['data-controller' => 'x'] no text no text
'attr' => ['placeholder' => 'Drop it'] Drop it, placeholder attribute on input Drop it, deprecation
'placeholder' => 'Drop it' not possible Drop it
'placeholder' => false not possible no text

symfony/deprecation-contracts is added to require, for trigger_deprecation(). It was already installed through symfony/form.

Tests: PHPUnit tests for the default text, with and without attr, a string, a TranslatableInterface (also without translation domain), false, true and a non-string attr.placeholder (rejected), the text in the view's attr, attr variables passed to form_widget() with and without a placeholder, and the deprecated attr.placeholder, alone, next to the new option, and with the view created twice. They fail on the base branch, except the one checking that attr variables without a placeholder keep the default text, as before. The deprecation tests catch it with their own error handler, which keeps only the Dropzone deprecations: the test bootstrap registers Symfony's ErrorHandler, and PHPUnit ignores a deprecation triggered from an options normalizer because the OptionsResolver calls it (ignoreIndirectDeprecations).

@carsonbot carsonbot added Deprecation Documentation Improvements or additions to documentation Dropzone Feature New Feature Status: Needs Review Needs to be reviewed labels Oct 26, 2026
@ker0x
ker0x force-pushed the feat/dropzone-placeholder-option branch from 5747c65 to 9952081 Compare October 26, 2026 11:24
ker0x added a commit to ker0x/symfony-ux that referenced this pull request Oct 26, 2026
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | no
| Documentation? | yes
| Issues         | Fix symfony#4142
| License        | MIT

Sixth and last step of the RFC in symfony#4142. Based on symfony#4143, symfony#4145, symfony#4144, symfony#4146, symfony#4148 and symfony#4149, replayed in that order: only the last commit belongs to this PR.

The two modes of the controller had grown into two code paths. Single-file mode showed one file with the preview of the form theme, cleared the input when the controller connected, and dispatched `dropzone:clear`; `multiple` mode kept a list, re-added the files already in the input on connect, and never dispatched `dropzone:clear`. Each feature of the RFC had to be written twice, and the modes differed in ways nobody chose: a prevented or rejected selection emptied the drop zone in single-file mode but kept the files in `multiple` mode, and the preview template only worked in `multiple` mode.

Single-file mode is now the stored list of files capped at one, and both modes go through the same steps for each selection: the input is put back to the stored files right away (also while `dropzone:pre-add` listeners are processing), the dropped files are checked against `accept`, the `dropzone:pre-add` listeners run, then the files are added (single-file mode: the new file replaces the previous one, and the files over the first one are rejected with `max_files`), the input is updated, the preview is rendered and `dropzone:change` is dispatched.

| Case                                              | Before                                         | After
| ------------------------------------------------- | ---------------------------------------------- | ---
| Single-file mode, controller connects with a file | input emptied, `dropzone:clear`                | file kept and shown
| `multiple` mode, last file removed                | no `dropzone:clear`                            | `dropzone:clear`
| Form reset                                        | drop zone keeps showing the files              | cleared, `dropzone:clear`
| Single-file mode, selection prevented by a `dropzone:pre-add` listener | drop zone emptied     | previous file kept, as in `multiple` mode
| Single-file mode, preview template                | not used                                       | rendered in the preview, its remove slot clears
| `multiple` mode, selection of files already in the list | `dropzone:change`          | no event
| Drag of text or a link over the drop zone         | `dragging`                                     | ignored
| Drag over a child of the drop zone in Safari      | `dragging` ends (`relatedTarget` is `null`)    | `dragging` kept
| Image previews                                    | read again on every render, an outdated read can show the image of a replaced file | read once per file, never shown for another file

`dropzone:change` keeps its payload (a `File` in single-file mode, a `FileList` in `multiple` mode), to stay compatible: changing it is left for 4.0. The form theme now renders the `dropzone_preview_item_template` block and the `remove_label` value in both modes. The drag state keeps the elements a drag has entered, from `dragenter` and `dragleave`, instead of reading `relatedTarget`, which WebKit does not set, and forgets the elements removed during the drag. `onMultipleChange()` is merged into `onInputChange()`. `src/Dropzone/UPGRADE.md` explains the changes for code listening to `dropzone:clear` on connect or to `dropzone:change`, and for controllers that extend this one.

Tests: unit tests for a file kept on connect without `dropzone:clear`, a prevented selection and a pending one in single-file mode, a selection of several files in single-file mode, an outdated image read, a form reset, a prevented form reset, a disconnect during a form reset, the clear button after a failed connect, the preview template in single-file mode, `dropzone:clear` after the last removal in `multiple` mode, a selection that adds no file in `multiple` mode, one read per image, an image read again after a failed read, a drag without files, a drag over a child without `relatedTarget`, and a drag over an element removed during the drag; PHPUnit tests for the template block and the `remove_label` value in single-file mode; Playwright tests for a form reset and the last removal in `multiple` mode. The new tests fail on the base of this PR. Two tests of the base changed: the single-file mode `file chosen` test now picks the file with a writable `files` property like the other tests, as `@testing-library/user-event` makes it read-only while the controller now writes the input on every selection; and the test of a prevented selection after a pending one now expects the previous file to be kept.
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | yes
| Documentation? | yes
| Issues         | symfony#4142
| License        | MIT

Fifth step of the RFC in symfony#4142.

The placeholder of the drop zone comes from `attr.placeholder`. This has two problems:

- `attr` is also rendered on the `<input type="file">`, so the input gets a `placeholder` attribute, which is not valid on a file input.
- The default text is the default value of the whole `attr` option, so the text and the input's attributes cannot be set apart.

This adds a `placeholder` option (`string`, `TranslatableInterface` or `false`), translated with the form's translation domain like before (a `TranslatableInterface` is always translated, also with `translation_domain: false`). `false` shows no text. The text stays in `attr.placeholder` of the form view, where the theme reads it and leaves it out of the input's attributes: the placeholder is no longer rendered on the input, while a placeholder passed to `form_widget()` and themes copied from an older version keep working. Its default keeps today's behavior: `'Drag and drop or browse'` when the field has no `attr`, and `attr.placeholder` (or no text) when the field passes its own `attr`, which used to replace the default text.

`attr.placeholder` keeps working as the text when the `placeholder` option is not passed, and is deprecated in favor of the new option. The deprecation is triggered once per field, when the options are resolved. `src/Dropzone/UPGRADE.md` explains the migration.

| Field options                              | Before                                         | After
| ------------------------------------------ | ---------------------------------------------- | ---
| none                                       | default text, `placeholder` attribute on input | default text
| `'attr' => ['data-controller' => 'x']`     | no text                                        | no text
| `'attr' => ['placeholder' => 'Drop it']`   | `Drop it`, `placeholder` attribute on input    | `Drop it`, deprecation
| `'placeholder' => 'Drop it'`               | not possible                                   | `Drop it`
| `'placeholder' => false`                   | not possible                                   | no text

`symfony/deprecation-contracts` is added to `require`, for `trigger_deprecation()`. It was already installed through `symfony/form`.

Tests: PHPUnit tests for the default text, with and without `attr`, a string, a `TranslatableInterface` (also without translation domain), `false`, `true` and a non-string `attr.placeholder` (rejected), the text in the view's `attr`, `attr` variables passed to `form_widget()` with and without a placeholder, and the deprecated `attr.placeholder`, alone, next to the new option, and with the view created twice. They fail on the base branch, except the one checking that `attr` variables without a placeholder keep the default text, as before. The deprecation tests catch it with their own error handler, which keeps only the Dropzone deprecations: the test bootstrap registers Symfony's `ErrorHandler`, and PHPUnit ignores a deprecation triggered from an options normalizer because the OptionsResolver calls it (`ignoreIndirectDeprecations`).
@Kocal
Kocal force-pushed the feat/dropzone-placeholder-option branch from 9952081 to 085d56c Compare November 2, 2026 21:46
@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Nov 2, 2026
@Kocal

Kocal commented Nov 2, 2026

Copy link
Copy Markdown
Member

Thank you @ker0x.

@Kocal
Kocal merged commit 0815db6 into symfony:3.x Nov 2, 2026
37 checks passed
Kocal pushed a commit to ker0x/symfony-ux that referenced this pull request Nov 2, 2026
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | no
| Documentation? | yes
| Issues         | Fix symfony#4142
| License        | MIT

Sixth and last step of the RFC in symfony#4142. Based on symfony#4143, symfony#4145, symfony#4144, symfony#4146, symfony#4148 and symfony#4149, replayed in that order: only the last commit belongs to this PR.

The two modes of the controller had grown into two code paths. Single-file mode showed one file with the preview of the form theme, cleared the input when the controller connected, and dispatched `dropzone:clear`; `multiple` mode kept a list, re-added the files already in the input on connect, and never dispatched `dropzone:clear`. Each feature of the RFC had to be written twice, and the modes differed in ways nobody chose: a prevented or rejected selection emptied the drop zone in single-file mode but kept the files in `multiple` mode, and the preview template only worked in `multiple` mode.

Single-file mode is now the stored list of files capped at one, and both modes go through the same steps for each selection: the input is put back to the stored files right away (also while `dropzone:pre-add` listeners are processing), the dropped files are checked against `accept`, the `dropzone:pre-add` listeners run, then the files are added (single-file mode: the new file replaces the previous one, and the files over the first one are rejected with `max_files`), the input is updated, the preview is rendered and `dropzone:change` is dispatched.

| Case                                              | Before                                         | After
| ------------------------------------------------- | ---------------------------------------------- | ---
| Single-file mode, controller connects with a file | input emptied, `dropzone:clear`                | file kept and shown
| `multiple` mode, last file removed                | no `dropzone:clear`                            | `dropzone:clear`
| Form reset                                        | drop zone keeps showing the files              | cleared, `dropzone:clear`
| Single-file mode, selection prevented by a `dropzone:pre-add` listener | drop zone emptied     | previous file kept, as in `multiple` mode
| Single-file mode, preview template                | not used                                       | rendered in the preview, its remove slot clears
| `multiple` mode, selection of files already in the list | `dropzone:change`          | no event
| Drag of text or a link over the drop zone         | `dragging`                                     | ignored
| Drag over a child of the drop zone in Safari      | `dragging` ends (`relatedTarget` is `null`)    | `dragging` kept
| Image previews                                    | read again on every render, an outdated read can show the image of a replaced file | read once per file, never shown for another file

`dropzone:change` keeps its payload (a `File` in single-file mode, a `FileList` in `multiple` mode), to stay compatible: changing it is left for 4.0. The form theme now renders the `dropzone_preview_item_template` block and the `remove_label` value in both modes. The drag state keeps the elements a drag has entered, from `dragenter` and `dragleave`, instead of reading `relatedTarget`, which WebKit does not set, and forgets the elements removed during the drag. `onMultipleChange()` is merged into `onInputChange()`. `src/Dropzone/UPGRADE.md` explains the changes for code listening to `dropzone:clear` on connect or to `dropzone:change`, and for controllers that extend this one.

Tests: unit tests for a file kept on connect without `dropzone:clear`, a prevented selection and a pending one in single-file mode, a selection of several files in single-file mode, an outdated image read, a form reset, a prevented form reset, a disconnect during a form reset, the clear button after a failed connect, the preview template in single-file mode, `dropzone:clear` after the last removal in `multiple` mode, a selection that adds no file in `multiple` mode, one read per image, an image read again after a failed read, a drag without files, a drag over a child without `relatedTarget`, and a drag over an element removed during the drag; PHPUnit tests for the template block and the `remove_label` value in single-file mode; Playwright tests for a form reset and the last removal in `multiple` mode. The new tests fail on the base of this PR. Two tests of the base changed: the single-file mode `file chosen` test now picks the file with a writable `files` property like the other tests, as `@testing-library/user-event` makes it read-only while the controller now writes the input on every selection; and the test of a prevented selection after a pending one now expects the previous file to be kept.
@ker0x
ker0x deleted the feat/dropzone-placeholder-option branch November 3, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Deprecation Documentation Improvements or additions to documentation Dropzone Feature New Feature Status: Reviewed Has been reviewed by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants