mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-24 00:32:14 +01:00
711 lines
23 KiB
ReStructuredText
711 lines
23 KiB
ReStructuredText
How to Embed a Collection of Forms
|
||
==================================
|
||
|
||
Symfony Forms can embed a collection of many other forms, which is useful to
|
||
edit related entities in a single form. In this article, you'll create a form to
|
||
edit a ``Task`` class and, right inside the same form, you'll be able to edit,
|
||
create and remove many ``Tag`` objects related to that Task.
|
||
|
||
Let's start by creating a ``Task`` entity::
|
||
|
||
// src/Entity/Task.php
|
||
namespace App\Entity;
|
||
|
||
use Doctrine\Common\Collections\Collection;
|
||
|
||
class Task
|
||
{
|
||
protected string $description;
|
||
protected Collection $tags;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->tags = new ArrayCollection();
|
||
}
|
||
|
||
public function getDescription(): string
|
||
{
|
||
return $this->description;
|
||
}
|
||
|
||
public function setDescription(string $description): void
|
||
{
|
||
$this->description = $description;
|
||
}
|
||
|
||
public function getTags(): Collection
|
||
{
|
||
return $this->tags;
|
||
}
|
||
}
|
||
|
||
.. note::
|
||
|
||
The `ArrayCollection`_ is specific to Doctrine and is similar to a PHP array
|
||
but provides many utility methods.
|
||
|
||
Now, create a ``Tag`` class. As you saw above, a ``Task`` can have many ``Tag``
|
||
objects::
|
||
|
||
// src/Entity/Tag.php
|
||
namespace App\Entity;
|
||
|
||
class Tag
|
||
{
|
||
private string $name;
|
||
|
||
public function getName(): string
|
||
{
|
||
return $this->name;
|
||
}
|
||
|
||
public function setName(string $name): void
|
||
{
|
||
$this->name = $name;
|
||
}
|
||
}
|
||
|
||
Then, create a form class so that a ``Tag`` object can be modified by the user::
|
||
|
||
// src/Form/TagType.php
|
||
namespace App\Form;
|
||
|
||
use App\Entity\Tag;
|
||
use Symfony\Component\Form\AbstractType;
|
||
use Symfony\Component\Form\FormBuilderInterface;
|
||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||
|
||
class TagType extends AbstractType
|
||
{
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||
{
|
||
$builder->add('name');
|
||
}
|
||
|
||
public function configureOptions(OptionsResolver $resolver): void
|
||
{
|
||
$resolver->setDefaults([
|
||
'data_class' => Tag::class,
|
||
]);
|
||
}
|
||
}
|
||
|
||
Next, let's create a form for the ``Task`` entity, using a
|
||
:doc:`CollectionType </reference/forms/types/collection>` field of ``TagType``
|
||
forms. This will allow us to modify all the ``Tag`` elements of a ``Task`` right
|
||
inside the task form itself::
|
||
|
||
// src/Form/TaskType.php
|
||
namespace App\Form;
|
||
|
||
use App\Entity\Task;
|
||
use Symfony\Component\Form\AbstractType;
|
||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||
use Symfony\Component\Form\FormBuilderInterface;
|
||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||
|
||
class TaskType extends AbstractType
|
||
{
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||
{
|
||
$builder->add('description');
|
||
|
||
$builder->add('tags', CollectionType::class, [
|
||
'entry_type' => TagType::class,
|
||
'entry_options' => ['label' => false],
|
||
]);
|
||
}
|
||
|
||
public function configureOptions(OptionsResolver $resolver): void
|
||
{
|
||
$resolver->setDefaults([
|
||
'data_class' => Task::class,
|
||
]);
|
||
}
|
||
}
|
||
|
||
In your controller, you'll create a new form from the ``TaskType``::
|
||
|
||
// src/Controller/TaskController.php
|
||
namespace App\Controller;
|
||
|
||
use App\Entity\Tag;
|
||
use App\Entity\Task;
|
||
use App\Form\TaskType;
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||
use Symfony\Component\HttpFoundation\Request;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
class TaskController extends AbstractController
|
||
{
|
||
public function new(Request $request): Response
|
||
{
|
||
$task = new Task();
|
||
|
||
// dummy code - add some example tags to the task
|
||
// (otherwise, the template will render an empty list of tags)
|
||
$tag1 = new Tag();
|
||
$tag1->setName('tag1');
|
||
$task->getTags()->add($tag1);
|
||
$tag2 = new Tag();
|
||
$tag2->setName('tag2');
|
||
$task->getTags()->add($tag2);
|
||
// end dummy code
|
||
|
||
$form = $this->createForm(TaskType::class, $task);
|
||
|
||
$form->handleRequest($request);
|
||
|
||
if ($form->isSubmitted() && $form->isValid()) {
|
||
// ... do your form processing, like saving the Task and Tag entities
|
||
}
|
||
|
||
return $this->render('task/new.html.twig', [
|
||
'form' => $form,
|
||
]);
|
||
}
|
||
}
|
||
|
||
In the template, you can now iterate over the existing ``TagType`` forms
|
||
to render them:
|
||
|
||
.. code-block:: html+twig
|
||
|
||
{# templates/task/new.html.twig #}
|
||
|
||
{# ... #}
|
||
|
||
{{ form_start(form) }}
|
||
{{ form_row(form.description) }}
|
||
|
||
<h3>Tags</h3>
|
||
<ul class="tags">
|
||
{% for tag in form.tags %}
|
||
<li>{{ form_row(tag.name) }}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{{ form_end(form) }}
|
||
|
||
{# ... #}
|
||
|
||
When the user submits the form, the submitted data for the ``tags`` field is
|
||
used to construct an ``ArrayCollection`` of ``Tag`` objects. The collection is
|
||
then set on the ``tag`` field of the ``Task`` and can be accessed via ``$task->getTags()``.
|
||
|
||
So far, this works great, but only to edit *existing* tags. It doesn't allow us
|
||
yet to add new tags or delete existing ones.
|
||
|
||
.. warning::
|
||
|
||
You can embed nested collections as many levels down as you like. However,
|
||
if you use Xdebug, you may receive a ``Maximum function nesting level of '100'
|
||
reached, aborting!`` error. To fix this, increase the ``xdebug.max_nesting_level``
|
||
PHP setting, or render each form field by hand using ``form_row()`` instead of
|
||
rendering the whole form at once (e.g ``form_widget(form)``).
|
||
|
||
.. _form-collections-new-prototype:
|
||
|
||
Allowing "new" Tags with the "Prototype"
|
||
----------------------------------------
|
||
|
||
Previously you added two tags to your task in the controller. Now let the users
|
||
add as many tag forms as they need directly in the browser. This requires a bit
|
||
of JavaScript code.
|
||
|
||
.. tip::
|
||
|
||
Instead of writing the needed JavaScript code yourself, you can use Symfony
|
||
UX to implement this feature with only PHP and Twig code. See the
|
||
`Symfony UX Demo of Form Collections`_.
|
||
|
||
But first, you need to let the form collection know that instead of exactly two,
|
||
it will receive an *unknown* number of tags. Otherwise, you'll see a
|
||
*"This form should not contain extra fields"* error. This is done with the
|
||
``allow_add`` option::
|
||
|
||
// src/Form/TaskType.php
|
||
|
||
// ...
|
||
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||
{
|
||
// ...
|
||
|
||
$builder->add('tags', CollectionType::class, [
|
||
'entry_type' => TagType::class,
|
||
'entry_options' => ['label' => false],
|
||
'allow_add' => true,
|
||
]);
|
||
}
|
||
|
||
The ``allow_add`` option also makes a ``prototype`` variable available to you.
|
||
This "prototype" is a little "template" that contains all the HTML needed to
|
||
dynamically create any new "tag" forms with JavaScript.
|
||
|
||
Let's start with plain JavaScript (Vanilla JS) – if you're using Stimulus, see below.
|
||
|
||
To render the prototype, add
|
||
the following ``data-prototype`` attribute to the existing ``<ul>`` in your
|
||
template:
|
||
|
||
.. code-block:: html+twig
|
||
|
||
{# the data-index attribute is required for the JavaScript code below #}
|
||
<ul class="tags"
|
||
data-index="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}"
|
||
data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"
|
||
></ul>
|
||
|
||
On the rendered page, the result will look something like this:
|
||
|
||
.. code-block:: html
|
||
|
||
<ul class="tags"
|
||
data-index="0"
|
||
data-prototype="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>"
|
||
></ul>
|
||
|
||
Now add a button to dynamically add a new tag:
|
||
|
||
.. code-block:: html+twig
|
||
|
||
<button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</button>
|
||
|
||
.. seealso::
|
||
|
||
If you want to customize the HTML code in the prototype, see
|
||
:ref:`form-custom-prototype`.
|
||
|
||
.. tip::
|
||
|
||
The ``form.tags.vars.prototype`` is a form element that looks and feels just
|
||
like the individual ``form_widget(tag.*)`` elements inside your ``for`` loop.
|
||
This means that you can call ``form_widget()``, ``form_row()`` or ``form_label()``
|
||
on it. You could even choose to render only one of its fields (e.g. the
|
||
``name`` field):
|
||
|
||
.. code-block:: twig
|
||
|
||
{{ form_widget(form.tags.vars.prototype.name)|e }}
|
||
|
||
.. note::
|
||
|
||
If you render your whole "tags" sub-form at once (e.g. ``form_row(form.tags)``),
|
||
the ``data-prototype`` attribute is automatically added to the containing ``div``,
|
||
and you need to adjust the following JavaScript accordingly.
|
||
|
||
Now add some JavaScript to read this attribute and dynamically add new tag forms
|
||
when the user clicks the "Add a tag" link. Add a ``<script>`` tag somewhere
|
||
on your page to include the required functionality with JavaScript:
|
||
|
||
.. code-block:: javascript
|
||
|
||
document
|
||
.querySelectorAll('.add_item_link')
|
||
.forEach(btn => {
|
||
btn.addEventListener("click", addFormToCollection)
|
||
});
|
||
|
||
The ``addFormToCollection()`` function's job will be to use the ``data-prototype``
|
||
attribute to dynamically add a new form when this link is clicked. The ``data-prototype``
|
||
HTML contains the tag's ``text`` input element with a name of ``task[tags][__name__][name]``
|
||
and id of ``task_tags___name___name``. The ``__name__`` is a placeholder, which
|
||
you'll replace with a unique, incrementing number (e.g. ``task[tags][3][name]``):
|
||
|
||
.. code-block:: javascript
|
||
|
||
function addFormToCollection(e) {
|
||
const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass);
|
||
|
||
const item = document.createElement('li');
|
||
|
||
item.innerHTML = collectionHolder
|
||
.dataset
|
||
.prototype
|
||
.replace(
|
||
/__name__/g,
|
||
collectionHolder.dataset.index
|
||
);
|
||
|
||
collectionHolder.appendChild(item);
|
||
|
||
collectionHolder.dataset.index++;
|
||
};
|
||
|
||
Now, each time a user clicks the ``Add a tag`` link, a new sub form will
|
||
appear on the page. When the form is submitted, any new tag forms will be converted
|
||
into new ``Tag`` objects and added to the ``tags`` property of the ``Task`` object.
|
||
|
||
.. seealso::
|
||
|
||
You can find a working example in this `JSFiddle`_.
|
||
|
||
JavaScript with Stimulus
|
||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
If you're using `Stimulus`_, wrap everything in a ``<div>``:
|
||
|
||
.. code-block:: html+twig
|
||
|
||
<div {{ stimulus_controller('form-collection') }}
|
||
data-form-collection-index-value="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}"
|
||
data-form-collection-prototype-value="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"
|
||
>
|
||
<ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul>
|
||
<button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add a tag</button>
|
||
</div>
|
||
|
||
Then create the controller:
|
||
|
||
.. code-block:: javascript
|
||
|
||
// assets/controllers/form-collection_controller.js
|
||
|
||
import { Controller } from '@hotwired/stimulus';
|
||
|
||
export default class extends Controller {
|
||
static targets = ["collectionContainer"]
|
||
|
||
static values = {
|
||
index : Number,
|
||
prototype: String,
|
||
}
|
||
|
||
addCollectionElement(event)
|
||
{
|
||
const item = document.createElement('li');
|
||
item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
|
||
this.collectionContainerTarget.appendChild(item);
|
||
this.indexValue++;
|
||
}
|
||
}
|
||
|
||
Handling the new Tags in PHP
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
To make handling these new tags easier, add an "adder" and a "remover" method
|
||
for the tags in the ``Task`` class::
|
||
|
||
// src/Entity/Task.php
|
||
namespace App\Entity;
|
||
|
||
// ...
|
||
class Task
|
||
{
|
||
// ...
|
||
|
||
public function addTag(Tag $tag): void
|
||
{
|
||
$this->tags->add($tag);
|
||
}
|
||
|
||
public function removeTag(Tag $tag): void
|
||
{
|
||
// ...
|
||
}
|
||
}
|
||
|
||
Next, add a ``by_reference`` option to the ``tags`` field and set it to ``false``::
|
||
|
||
// src/Form/TaskType.php
|
||
|
||
// ...
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||
{
|
||
// ...
|
||
|
||
$builder->add('tags', CollectionType::class, [
|
||
// ...
|
||
'by_reference' => false,
|
||
]);
|
||
}
|
||
|
||
With these two changes, when the form is submitted, each new ``Tag`` object
|
||
is added to the ``Task`` class by calling the ``addTag()`` method. Before this
|
||
change, they were added internally by the form by calling ``$task->getTags()->add($tag)``.
|
||
That was fine, but forcing the use of the "adder" method makes handling
|
||
these new ``Tag`` objects easier (especially if you're using Doctrine, which
|
||
you will learn about next!).
|
||
|
||
.. warning::
|
||
|
||
You have to create **both** ``addTag()`` and ``removeTag()`` methods,
|
||
otherwise the form will still use ``setTag()`` even if ``by_reference`` is ``false``.
|
||
You'll learn more about the ``removeTag()`` method later in this article.
|
||
|
||
.. warning::
|
||
|
||
Symfony can only make the plural-to-singular conversion (e.g. from the
|
||
``tags`` property to the ``addTag()`` method) for English words. Code
|
||
written in any other language won't work as expected.
|
||
|
||
.. sidebar:: Doctrine: Cascading Relations and saving the "Inverse" side
|
||
|
||
To save the new tags with Doctrine, you need to consider a couple more
|
||
things. First, unless you iterate over all of the new ``Tag`` objects and
|
||
call ``$entityManager->persist($tag)`` on each, you'll receive an error from
|
||
Doctrine:
|
||
|
||
.. code-block:: text
|
||
|
||
A new entity was found through the relationship
|
||
``App\Entity\Task#tags`` that was not configured to
|
||
cascade persist operations for entity...
|
||
|
||
To fix this, you may choose to "cascade" the persist operation automatically
|
||
from the ``Task`` object to any related tags. To do this, add the ``cascade``
|
||
option to your ``ManyToMany`` metadata:
|
||
|
||
.. configuration-block::
|
||
|
||
.. code-block:: php-attributes
|
||
|
||
// src/Entity/Task.php
|
||
|
||
// ...
|
||
|
||
#[ORM\ManyToMany(targetEntity: Tag::class, cascade: ['persist'])]
|
||
protected Collection $tags;
|
||
|
||
.. code-block:: yaml
|
||
|
||
# src/Resources/config/doctrine/Task.orm.yaml
|
||
App\Entity\Task:
|
||
type: entity
|
||
# ...
|
||
oneToMany:
|
||
tags:
|
||
targetEntity: App\Entity\Tag
|
||
cascade: [persist]
|
||
|
||
.. code-block:: xml
|
||
|
||
<!-- src/Resources/config/doctrine/Task.orm.xml -->
|
||
<?xml version="1.0" encoding="UTF-8" ?>
|
||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||
https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||
|
||
<entity name="App\Entity\Task">
|
||
<!-- ... -->
|
||
<one-to-many field="tags" target-entity="Tag">
|
||
<cascade>
|
||
<cascade-persist/>
|
||
</cascade>
|
||
</one-to-many>
|
||
</entity>
|
||
</doctrine-mapping>
|
||
|
||
A second potential issue deals with the `Owning Side and Inverse Side`_
|
||
of Doctrine relationships. In this example, if the "owning" side of the
|
||
relationship is "Task", then persistence will work fine as the tags are
|
||
properly added to the Task. However, if the owning side is on "Tag", then
|
||
you'll need to do a little bit more work to ensure that the correct side
|
||
of the relationship is modified.
|
||
|
||
The solution is to make sure that the single "Task" is set on each "Tag".
|
||
One way to do this is to add some extra logic to ``addTag()``, which
|
||
is called by the form type since ``by_reference`` is set to ``false``::
|
||
|
||
// src/Entity/Task.php
|
||
|
||
// ...
|
||
public function addTag(Tag $tag): void
|
||
{
|
||
// for a many-to-many association:
|
||
$tag->addTask($this);
|
||
|
||
// for a many-to-one association:
|
||
$tag->setTask($this);
|
||
|
||
$this->tags->add($tag);
|
||
}
|
||
|
||
If you're going for ``addTask()``, make sure you have an appropriate method
|
||
that looks something like this::
|
||
|
||
// src/Entity/Tag.php
|
||
|
||
// ...
|
||
public function addTask(Task $task): void
|
||
{
|
||
if (!$this->tasks->contains($task)) {
|
||
$this->tasks->add($task);
|
||
}
|
||
}
|
||
|
||
.. _form-collections-remove:
|
||
|
||
Allowing Tags to be Removed
|
||
---------------------------
|
||
|
||
The next step is to allow the deletion of a particular item in the collection.
|
||
The solution is similar to allowing tags to be added.
|
||
|
||
Start by adding the ``allow_delete`` option in the form Type::
|
||
|
||
// src/Form/TaskType.php
|
||
|
||
// ...
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||
{
|
||
// ...
|
||
|
||
$builder->add('tags', CollectionType::class, [
|
||
// ...
|
||
'allow_delete' => true,
|
||
]);
|
||
}
|
||
|
||
Now, you need to put some code into the ``removeTag()`` method of ``Task``::
|
||
|
||
// src/Entity/Task.php
|
||
|
||
// ...
|
||
class Task
|
||
{
|
||
// ...
|
||
|
||
public function removeTag(Tag $tag): void
|
||
{
|
||
$this->tags->removeElement($tag);
|
||
}
|
||
}
|
||
|
||
The ``allow_delete`` option means that if an item of a collection
|
||
isn't sent on submission, the related data is removed from the collection
|
||
on the server. In order for this to work in an HTML form, you must remove
|
||
the DOM element for the collection item to be removed, before submitting
|
||
the form.
|
||
|
||
In the JavaScript code, add a "delete" button to each existing tag on the page.
|
||
Then, append the "add delete button" method in the function that adds the new tags:
|
||
|
||
.. code-block:: javascript
|
||
|
||
document
|
||
.querySelectorAll('ul.tags li')
|
||
.forEach((tag) => {
|
||
addTagFormDeleteLink(tag)
|
||
})
|
||
|
||
// ... the rest of the block from above
|
||
|
||
function addFormToCollection(e) {
|
||
// ...
|
||
|
||
// add a delete link to the new form
|
||
addTagFormDeleteLink(item);
|
||
}
|
||
|
||
The ``addTagFormDeleteLink()`` function will look something like this:
|
||
|
||
.. code-block:: javascript
|
||
|
||
function addTagFormDeleteLink(item) {
|
||
const removeFormButton = document.createElement('button');
|
||
removeFormButton.innerText = 'Delete this tag';
|
||
|
||
item.append(removeFormButton);
|
||
|
||
removeFormButton.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
// remove the li for the tag form
|
||
item.remove();
|
||
});
|
||
}
|
||
|
||
When a tag form is removed from the DOM and submitted, the removed ``Tag`` object
|
||
will not be included in the collection passed to ``setTags()``. Depending on
|
||
your persistence layer, this may or may not be enough to actually remove
|
||
the relationship between the removed ``Tag`` and ``Task`` object.
|
||
|
||
.. sidebar:: Doctrine: Ensuring the database persistence
|
||
|
||
When removing objects in this way, you may need to do a little bit more
|
||
work to ensure that the relationship between the ``Task`` and the removed
|
||
``Tag`` is properly removed.
|
||
|
||
In Doctrine, you have two sides of the relationship: the owning side and the
|
||
inverse side. Normally in this case you'll have a many-to-one relationship
|
||
and the deleted tags will disappear and persist correctly (adding new
|
||
tags also works effortlessly).
|
||
|
||
But if you have a one-to-many relationship or a many-to-many relationship with a
|
||
``mappedBy`` on the Task entity (meaning Task is the "inverse" side),
|
||
you'll need to do more work for the removed tags to persist correctly.
|
||
|
||
In this case, you can modify the controller to remove the relationship
|
||
on the removed tag. This assumes that you have some ``edit()`` action which
|
||
is handling the "update" of your Task::
|
||
|
||
// src/Controller/TaskController.php
|
||
|
||
// ...
|
||
use App\Entity\Task;
|
||
use Doctrine\Common\Collections\ArrayCollection;
|
||
|
||
class TaskController extends AbstractController
|
||
{
|
||
public function edit(Task $task, Request $request, EntityManagerInterface $entityManager): Response
|
||
{
|
||
$originalTags = new ArrayCollection();
|
||
|
||
// Create an ArrayCollection of the current Tag objects in the database
|
||
foreach ($task->getTags() as $tag) {
|
||
$originalTags->add($tag);
|
||
}
|
||
|
||
$editForm = $this->createForm(TaskType::class, $task);
|
||
|
||
$editForm->handleRequest($request);
|
||
|
||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||
// remove the relationship between the tag and the Task
|
||
foreach ($originalTags as $tag) {
|
||
if (false === $task->getTags()->contains($tag)) {
|
||
// remove the Task from the Tag
|
||
$tag->getTasks()->removeElement($task);
|
||
|
||
// if it was a many-to-one relationship, remove the relationship like this
|
||
// $tag->setTask(null);
|
||
|
||
$entityManager->persist($tag);
|
||
|
||
// if you wanted to delete the Tag entirely, you can also do that
|
||
// $entityManager->remove($tag);
|
||
}
|
||
}
|
||
|
||
$entityManager->persist($task);
|
||
$entityManager->flush();
|
||
|
||
// redirect back to some edit page
|
||
return $this->redirectToRoute('task_edit', ['id' => $id]);
|
||
}
|
||
|
||
// ... render some form template
|
||
}
|
||
}
|
||
|
||
As you can see, adding and removing the elements correctly can be tricky.
|
||
Unless you have a many-to-many relationship where Task is the "owning" side,
|
||
you'll need to do extra work to make sure that the relationship is properly
|
||
updated (whether you're adding new tags or removing existing tags) on
|
||
each Tag object itself.
|
||
|
||
.. seealso::
|
||
|
||
The Symfony community has created some JavaScript packages that provide the
|
||
functionality needed to add, edit and delete elements of the collection.
|
||
Check out the `@a2lix/symfony-collection`_ or search on GitHub for other
|
||
recent packages.
|
||
|
||
.. _`Owning Side and Inverse Side`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/unitofwork-associations.html
|
||
.. _`JSFiddle`: https://jsfiddle.net/ey8ozh6n/
|
||
.. _`@a2lix/symfony-collection`: https://github.com/a2lix/symfony-collection
|
||
.. _`ArrayCollection`: https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html
|
||
.. _`Symfony UX Demo of Form Collections`: https://ux.symfony.com/live-component/demos/form-collection-type
|
||
.. _`Stimulus`: https://symfony.com/doc/current/frontend/encore/simple-example.html#stimulus-symfony-ux
|