-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Form] Ability to add/remove rows in a Collection without Javascript #5231
Comments
It is possible to support that without javascript. Simply render some additional empty inputs for the extra collection items and write a form listener that removes items that were empty. This way users can also delete items by deleting the corresponding text inside the field. How else would you image it to work? |
Well should work like the javascript versions where you have buttons, would think with some clever naming of thoose buttons and their positions it could be possible to have working out the box. |
Huh? Any button that does something dynamically requires javascript. I don't see how your buttons work without javascript. |
You have a submit button with a cleaver name like |
Well, that is basically what I suggested in my first comment only that you do it upon submitting with the new button. It can easily be implemented in your own controller. I don't see how the framework can handle that automatically in a standard way. |
I agree with @henrikbjorn, we should see if we can provide a more "out of the box" way of implementing this feature without JS. |
I would like to reconsider this issue, as I believe this can be another huge DX improvement. This is one of the few features where Symfony provides a back-end mechanism, but still requires a developer to come up with quite a lot of custom code to get things working. Of course, solving this with JavaScript would make things a lot easier. But I believe Symfony has to provide a built-in solution too. Using the form buttons might seem obvious, but there are quite some "hacks" to do before making it work: {# app/Resources/views/form/collection.html.twig #}
{# disable HTML5 validation, as it should be possible to add fields to a not yet validate form #}
{{ form(form, { attr: { novalidate: 'novalidate' } }) }} // src/AppBundle/Form/TestCollectionType.php
// ...
$builder
->add('some_field', 'textarea')
->add('texts', 'collection', array(
'type' => 'text',
'allow_add' => true,
))
// it is confusing to make add_text a submit button
->add('add_text', 'submit')
->add('submit', 'submit')
; // src/AppBundle/Form/FormController.php
// ...
public function collectionAction(Request $request)
{
$entity = new TestEntity();
$form = $this->createForm(new TestCollectionType(), $entity);
$form->handleRequest($request);
// do only check for submitted, as the form might not be validate.
if ($form->isSubmitted()) {
if ($form->get('add_text')->isClicked()) {
// create a new form, as submitted forms can't be changed
// for no obvious reason, already added fields are also added to this new form instance
$form = $this->createForm(new TestCollectionType(), $entity);
$form->get('texts')->add(uniqid(), 'text');
} elseif ($form->isValid()) {
// ... form is submitted and valid
}
}
return $this->render('form/collection.html.twig', ['form' => $form->createView()]);
} I can see however, that's also not that easy to built into the Form component itself. It can be built into |
I agree, this would be a nice feature. At the moment I'm doing more or less what you described in your code sample:
I don't know if this is really "gross" or not, and it's arguably inefficient, but it certainly feels like a lot less work than the recommended javascript approach... and I'm under a severe deadline, so this approach is a huge win for that reason alone. I may regret it, but... that's for another day. Unless there's a clear reason to not do it this way, I think it may be worth describing this approach in the docs on handling collections in forms. |
I am closing this old issue. I don't see a way to implement this easily inside the Form component in a generic way. If someone still wants to work on this feature, I suggest to do it in a third-party bundle first. If such an implementation proves to be stable, we can still consider to integrate it into the core component. |
Sorry to reply to an old closed ticket but I've had a crack at this. My rough implementation is as follows: First we need to override the form theme. This adds a {%- block collection_widget -%}
{% if prototype is defined and not prototype.rendered %}
{%- set attr = attr|merge({'data-prototype': form_row(prototype) }) -%}
{% endif %}
{{- block('form_widget') -}}
{% if form.vars.allow_add %}
<button type="submit" value="_addRow" name="{{ form.vars.full_name }}[_addRow]" formnovalidate>Add Row</button>
{% endif %}
{%- endblock -%}
{%- block collection_entry_row -%}
{{- block('form_row') -}}
{% if form.parent.vars.allow_delete %}
<button type="submit" value="_deleteRow" name="{{ form.vars.full_name }}" formnovalidate>Delete Row</button>
{% endif %}
{% endblock %}
Here's the There's probably an issue appending a new item to the data without checking the previous keys. This could kick in when the user has deleted a row. This needs more testing. It also adds a fake error to mark the form as invalid and stop processing in the controller. <?php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Event\PreSubmitEvent;
use Symfony\Component\Form\Event\SubmitEvent;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvents;
final class CollectionTypeListener implements EventSubscriberInterface
{
private $allowAdd = false;
private $allowDelete = false;
private $isAdding = false;
private $isDeleting = false;
public function __construct(
bool $allowAdd = false,
bool $allowDelete = false
)
{
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
}
public static function getSubscribedEvents(): array
{
return [
FormEvents::PRE_SUBMIT => ['preSubmit', 1],
FormEvents::SUBMIT => ['onSubmit', 51],
];
}
public function preSubmit(PreSubmitEvent $event): void
{
$data = $event->getData();
// Remove the dynamic error field if submitted
unset($data['__dynamic_error']);
if ($this->allowAdd) {
// If `_addRow` is found, we add a new empty value to the data.
// `_addRow` is the "Add Row" button value.
if (isset($data['_addRow'])) {
unset($data['_addRow']);
$index = [] !== $data ? max(array_keys($data)) + 1 : 0;
$data[$index] = null;
$this->isAdding = true;
}
}
if ($this->allowDelete) {
// Loop over all the data and check for `_deleteRow`.
// `_deleteRow` is the "Delete Row" button value.
// If `_deleteRow` is found, we unset the data.
foreach ($data as $key => $value) {
if ($value === "_deleteRow") {
unset($data[$key]);
$this->isDeleting = true;
}
}
}
$event->setData($data);
}
public function onSubmit(SubmitEvent $event): void
{
// If the collection was updated, we add a fake error to the form.
// This will trigger the form to be invalid
// The error message will not be shown because the field is hidden
// See: https://github.com/SymfonyCasts/dynamic-forms/blob/main/src/DynamicFormBuilder.php
if ($this->isAdding || $this->isDeleting) {
// Get the parent form
$form = $event->getForm();
// A fake hidden field where we can "store" an error
if (!$form->has('__dynamic_error')) {
$form->add('__dynamic_error', HiddenType::class, [
'mapped' => false,
'error_bubbling' => false,
]);
$form->get('__dynamic_error')->addError(new FormError('Collection updated'));
}
}
}
} I've created a form extension to apply it to all <?php
namespace App\Form\Extension;
use App\EventListener\CollectionTypeListener;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
class CollectionExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
// Extend the CollectionType
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CollectionTypeListener(
$options['allow_add'],
$options['allow_delete'],
));
}
} |
@wouterj @xabbuh Is there a better place than a closed ticket to discuss my rough idea: #5231 (comment) |
Can you create a pull request with the changes you have in mind? |
No description provided.
The text was updated successfully, but these errors were encountered: