Skip to content

[LiveComponent] Tweak live collection rendering #421

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

Merged
merged 1 commit into from
Aug 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/LiveComponent/src/Form/Type/LiveCollectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ public function buildView(FormView $view, FormInterface $form, array $options):
{
if ($form->getConfig()->hasAttribute('button_add_prototype')) {
$prototype = $form->getConfig()->getAttribute('button_add_prototype');
$view->vars['button_add_prototype'] = $prototype->setParent($form)->createView($view);
array_splice($view->vars['button_add_prototype']->vars['block_prefixes'], 1, 0, 'live_collection_button_add');
$view->vars['button_add'] = $prototype->setParent($form)->createView($view);

$attr = $view->vars['button_add']->vars['attr'];
$attr['data-action'] ??= 'live#action';
$attr['data-action-name'] ??= sprintf('addCollectionItem(name=%s)', $view->vars['full_name']);
$view->vars['button_add']->vars['attr'] = $attr;

array_splice($view->vars['button_add']->vars['block_prefixes'], 1, 0, 'live_collection_button_add');
}
}

Expand Down Expand Up @@ -75,8 +81,14 @@ public function finishView(FormView $view, FormInterface $form, array $options):
}

foreach ($view as $k => $entryView) {
$entryView->vars['button_delete_prototype'] = $prototypes[$k]->createView($entryView);
array_splice($entryView->vars['button_delete_prototype']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
$entryView->vars['button_delete'] = $prototypes[$k]->createView($entryView);

$attr = $entryView->vars['button_delete']->vars['attr'];
$attr['data-action'] ??= 'live#action';
$attr['data-action-name'] ??= sprintf('removeCollectionItem(name=%s, index=%s)', $view->vars['full_name'], $k);
$entryView->vars['button_delete']->vars['attr'] = $attr;

array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
}
}
}
Expand All @@ -92,6 +104,9 @@ public function configureOptions(OptionsResolver $resolver): void
'button_add_options' => [],
'button_delete_type' => ButtonType::class,
'button_delete_options' => [],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
}

Expand Down
115 changes: 114 additions & 1 deletion src/LiveComponent/src/Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ There is no need for a custom template just render the form as usual:

The ``add`` and ``delete`` buttons are rendered as separate ``ButtonType`` form
types and can be customized like a normal form type via the ``live_collection_button_add``
and ``live_collection_button_delete`` respectively:
and ``live_collection_button_delete`` block prefix respectively:

.. code-block:: twig

Expand All @@ -1282,6 +1282,118 @@ and ``live_collection_button_delete`` respectively:
{{ block('button_widget') }}
{% endblock live_collection_button_add_widget %}

If you only want to customize some attributes maybe simpler to use the options in the form type:

// ...
->add('comments', LiveCollectionType::class, [
'entry_type' => CommentFormType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'label' => false,
'button_delete_options' => [
'label' => 'X',
'attr' => [
'class' => 'btn btn-outline-danger',
],
]
])
// ...

If you want more control over how each row is rendered you can override the blocks
related to the ``LiveCollectionType``. This works the same way as for `the traditional
collection type`_, but you should use ``live_collection_*`` and ``live_collection_entry_*``
as prefixes instead.

For example, let's continue our previous example and customize the rendering of the blog post comments form.
By default the add comment button is placed after the comments, let's move it before them.

.. code-block:: twig

{%- block live_collection_widget -%}
{%- if button_add is defined and not button_add.rendered -%}
{{ form_row(button_add) }}
{%- endif -%}
{{ block('form_widget') }}
{%- endblock -%}

Now add a div around each row:

.. code-block:: twig

{%- block live_collection_entry_row -%}
<div>
{{ block('form_row') }}
{%- if button_delete is defined and not button_delete.rendered -%}
{{ form_row(button_delete) }}
{%- endif -%}
</div>
{%- endblock -%}

.. note::

Under the hood, ``LiveCollectionType`` adds ``button_add`` and
``button_delete`` fields to the form in a special way. These fields
are not added as regular form fields, so they are not part of the form
tree, but only the form view. The ``button_add`` is added to the
collection view variables and a ``button_delete`` is added to each
item view variables.

As an another example, now let's create a general bootstrap 5 theme for the live
collection type, rendering every item in a table row:

.. code-block:: twig

{%- block live_collection_widget -%}
<table class="table table-borderless form-no-mb">
<thead>
<tr>
{% for child in form|last %}
<td>{{ form_label(child) }}</td>
{% endfor %}
<td></td>
</tr>
</thead>
<tbody>
{{ block('form_widget') }}
</tbody>
</table>
{%- if skip_add_button|default(false) is same as false and button_add is defined and not button_add.rendered -%}
{{ form_widget(button_add, { label: '+ Add Item', class: 'btn btn-outline-primary' }) }}
{%- endif -%}
{%- endblock -%}

{%- block live_collection_entry_row -%}
<tr>
{% for child in form %}
<td>{{- form_row(child, { label: false }) -}}</td>
{% endfor %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a clever way to loop over all of the fields in the sub-form and render them? If so, what's the purpose of the |filter(child => not child.rendered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this just renders the children in a <td>. It's safer to use in this way, because if a field was rendered already, rendering it again will throw an exception. But in this case it's not really necessary, so I leave it out.

<td>
{{- form_row(button_delete, { label: 'X', attr: { class: 'btn btn-outline-danger' } }) -}}
</td>
</tr>
{%- endblock -%}

To render the add button later in the template, you can skip rendering it initially with ``skip_add_button``,
then render it manually after:

.. code-block:: twig

<table class="table table-borderless form-no-mb">
<thead>
<tr>
<td>Item</td>
<td>Priority</td>
<td></td>
</tr>
</thead>
<tbody>
{{ form_row(form.todoItems, { skip_add_button: true }) }}
</tbody>
</table>

{{ form_widget(form.todoItems.vars.button_add, { label: '+ Add Item', class: 'btn btn-outline-primary' }) }}

Modifying Nested Object Properties with the "exposed" Option
------------------------------------------------------------

Expand Down Expand Up @@ -1768,3 +1880,4 @@ bound to Symfony's BC policy for the moment.
.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html
.. _`attributes variable`: https://symfony.com/bundles/ux-twig-component/current/index.html#component-attributes
.. _`CollectionType`: https://symfony.com/doc/current/form/form_collections.html
.. _`the traditional collection type`: https://symfony.com/doc/current/form/form_themes.html#fragment-naming-for-collections
14 changes: 4 additions & 10 deletions src/LiveComponent/src/Resources/views/form_theme.html.twig
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
{%- block live_collection_widget -%}
{{ block('form_widget') }}
{%- if button_add_prototype is defined and not button_add_prototype.rendered -%}
{{ form_row(button_add_prototype, { attr: button_add_prototype.vars.attr|merge({
'data-action': 'live#action',
'data-action-name': 'addCollectionItem(name=' ~ form.vars.full_name ~ ')'
}) }) }}
{%- if skip_add_button|default(false) is same as(false) and button_add is defined and not button_add.rendered -%}
{{ form_row(button_add) }}
{%- endif -%}
{%- endblock live_collection_widget -%}

{%- block live_collection_entry_row -%}
{{ block('form_row') }}
{%- if button_delete_prototype is defined and not button_delete_prototype.rendered -%}
{{ form_row(button_delete_prototype, { attr: button_delete_prototype.vars.attr|merge({
'data-action': 'live#action',
'data-action-name': 'removeCollectionItem(name=' ~ form.parent.vars.full_name ~ ', index=' ~ form.vars.name ~ ')'
}) }) }}
{%- if button_delete is defined and not button_delete.rendered -%}
{{ form_row(button_delete) }}
{%- endif -%}
{%- endblock live_collection_entry_row -%}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function testAddButtonPrototypeDefaultBlockPrefixes()
];

$this->assertCount(0, $collectionView);
$this->assertSame($expectedBlockPrefixes, $collectionView->vars['button_add_prototype']->vars['block_prefixes']);
$this->assertSame($expectedBlockPrefixes, $collectionView->vars['button_add']->vars['block_prefixes']);
}

public function testAddButtonPrototypeBlockPrefixesWithCustomBlockPrefix()
Expand All @@ -57,7 +57,7 @@ public function testAddButtonPrototypeBlockPrefixesWithCustomBlockPrefix()
];

$this->assertCount(0, $collectionView);
$this->assertSame($expectedBlockPrefixes, $collectionView->vars['button_add_prototype']->vars['block_prefixes']);
$this->assertSame($expectedBlockPrefixes, $collectionView->vars['button_add']->vars['block_prefixes']);
}

public function testDeleteButtonPrototypeDefaultBlockPrefixes()
Expand All @@ -78,7 +78,7 @@ public function testDeleteButtonPrototypeDefaultBlockPrefixes()
];

$this->assertCount(1, $collectionView);
$this->assertSame($expectedBlockPrefixes, $collectionView['tags']->vars['button_delete_prototype']->vars['block_prefixes']);
$this->assertSame($expectedBlockPrefixes, $collectionView['tags']->vars['button_delete']->vars['block_prefixes']);
}

public function testDeleteButtonPrototypeBlockPrefixesWithCustomBlockPrefix()
Expand All @@ -101,6 +101,6 @@ public function testDeleteButtonPrototypeBlockPrefixesWithCustomBlockPrefix()
];

$this->assertCount(1, $collectionView);
$this->assertSame($expectedBlockPrefixes, $collectionView['tags']->vars['button_delete_prototype']->vars['block_prefixes']);
$this->assertSame($expectedBlockPrefixes, $collectionView['tags']->vars['button_delete']->vars['block_prefixes']);
}
}