Skip to content

Commit 15c4c46

Browse files
committed
Add LiveCollectionType
1 parent ca7f933 commit 15c4c46

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Form\Type;
13+
14+
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
16+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
17+
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\Form\FormInterface;
19+
use Symfony\Component\Form\FormView;
20+
use Symfony\Component\OptionsResolver\Options;
21+
use Symfony\Component\OptionsResolver\OptionsResolver;
22+
23+
/**
24+
* @author Gábor Egyed <[email protected]>
25+
*
26+
* @experimental
27+
*/
28+
final class LiveCollectionType extends AbstractType
29+
{
30+
public function buildForm(FormBuilderInterface $builder, array $options): void
31+
{
32+
if ($options['allow_add']) {
33+
$prototype = $builder->create('add', $options['button_add_type'], $options['button_add_options']);
34+
$builder->setAttribute('button_add_prototype', $prototype->getForm());
35+
}
36+
37+
if ($options['allow_delete']) {
38+
$prototype = $builder->create('delete', $options['button_delete_type'], $options['button_delete_options']);
39+
$builder->setAttribute('button_delete_prototype', $prototype->getForm());
40+
}
41+
}
42+
43+
public function buildView(FormView $view, FormInterface $form, array $options): void
44+
{
45+
if ($form->getConfig()->hasAttribute('button_add_prototype')) {
46+
$prototype = $form->getConfig()->getAttribute('button_add_prototype');
47+
$view->vars['button_add_prototype'] = $prototype->setParent($form)->createView($view);
48+
}
49+
}
50+
51+
public function finishView(FormView $view, FormInterface $form, array $options): void
52+
{
53+
$prefixOffset = -2;
54+
// check if the entry type also defines a block prefix
55+
/** @var FormInterface $entry */
56+
foreach ($form as $entry) {
57+
if ($entry->getConfig()->getOption('block_prefix')) {
58+
--$prefixOffset;
59+
}
60+
61+
break;
62+
}
63+
64+
foreach ($view as $entryView) {
65+
array_splice($entryView->vars['block_prefixes'], $prefixOffset, 0, 'live_collection_entry');
66+
}
67+
68+
if ($form->getConfig()->hasAttribute('button_delete_prototype')) {
69+
$prototype = $form->getConfig()->getAttribute('button_delete_prototype');
70+
71+
$prototypes = [];
72+
foreach ($form as $k => $entry) {
73+
$prototypes[$k] = clone $prototype;
74+
$prototypes[$k]->setParent($entry);
75+
}
76+
77+
foreach ($view as $k => $entryView) {
78+
$entryView->vars['button_delete_prototype'] = $prototypes[$k]->createView($entryView);
79+
}
80+
}
81+
}
82+
83+
public function configureOptions(OptionsResolver $resolver): void
84+
{
85+
$buttonAddNormalizer = function (Options $options, $value) {
86+
$value['block_prefix'] = 'live_collection_button_add';
87+
88+
return $value;
89+
};
90+
$buttonDeleteNormalizer = function (Options $options, $value) {
91+
$value['block_prefix'] = 'live_collection_button_delete';
92+
93+
return $value;
94+
};
95+
$resolver->setDefaults([
96+
'prototype' => false,
97+
'button_add_type' => ButtonType::class,
98+
'button_add_options' => [],
99+
'button_delete_type' => ButtonType::class,
100+
'button_delete_options' => [],
101+
]);
102+
103+
$resolver->setNormalizer('button_add_options', $buttonAddNormalizer);
104+
$resolver->setNormalizer('button_delete_options', $buttonDeleteNormalizer);
105+
}
106+
107+
public function getBlockPrefix(): string
108+
{
109+
return 'live_collection';
110+
}
111+
112+
public function getParent(): string
113+
{
114+
return CollectionType::class;
115+
}
116+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent;
13+
14+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
15+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
16+
use Symfony\UX\LiveComponent\Attribute\LiveArg;
17+
18+
/**
19+
* @author Gábor Egyed <[email protected]>
20+
*
21+
* @experimental
22+
*/
23+
trait LiveCollectionTrait
24+
{
25+
use ComponentWithFormTrait;
26+
27+
#[LiveAction]
28+
public function addCollectionItem(PropertyAccessorInterface $propertyAccessor, #[LiveArg] string $name): void
29+
{
30+
if (str_starts_with($name, $this->formName)) {
31+
$name = substr_replace($name, '', 0, mb_strlen($this->formName));
32+
}
33+
34+
$data = $propertyAccessor->getValue($this->formValues, $name);
35+
36+
if (!is_array($data)) {
37+
$propertyAccessor->setValue($this->formValues, $name, []);
38+
$data = [];
39+
}
40+
41+
$index = [] !== $data ? max(array_keys($data)) + 1 : 0;
42+
$propertyAccessor->setValue($this->formValues, $name."[$index]", []);
43+
}
44+
45+
#[LiveAction]
46+
public function removeCollectionItem(PropertyAccessorInterface $propertyAccessor, #[LiveArg] string $name, #[LiveArg] int $index): void
47+
{
48+
if (str_starts_with($name, $this->formName)) {
49+
$name = substr_replace($name, '', 0, mb_strlen($this->formName));
50+
}
51+
52+
$data = $propertyAccessor->getValue($this->formValues, $name);
53+
unset($data[$index]);
54+
$propertyAccessor->setValue($this->formValues, $name, $data);
55+
}
56+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{%- block live_collection_widget -%}
2+
{% set attr = attr|merge({'class': attr.class|default('space-y-4')}) %}
3+
<div {{ block('widget_container_attributes') }}>
4+
{%- if form is rootform -%}
5+
{{ form_errors(form) }}
6+
{%- endif -%}
7+
{{- block('form_rows') -}}
8+
{{- form_rest(form) -}}
9+
10+
{% if button_add_prototype is defined and not button_add_prototype.rendered %}
11+
{{ form_row(button_add_prototype, { attr: button_add_prototype.vars.attr|merge({
12+
'data-action': 'live#action',
13+
'data-action-name': 'addCollectionItem(name=' ~ form.vars.full_name ~ ', index=' ~ (form.children is empty ? 0 : max(form.children|keys)) ~ ')'
14+
}) }) }}
15+
{% endif %}
16+
</div>
17+
{%- endblock live_collection_widget -%}
18+
19+
{% block live_collection_entry_row %}
20+
{{ block('form_row') }}
21+
{% if button_delete_prototype is defined and not button_delete_prototype.rendered %}
22+
{{ form_row(button_delete_prototype, { attr: button_delete_prototype.vars.attr|merge({
23+
'data-action': 'live#action',
24+
'data-action-name': 'removeCollectionItem(name=' ~ form.parent.vars.full_name ~ ', index=' ~ form.vars.name ~ ')'
25+
}) }) }}
26+
{% endif %}
27+
{% endblock live_collection_entry_row %}
28+
29+
{% block live_collection_button_add_widget %}
30+
{{ block('button_widget') }}
31+
{% endblock live_collection_button_add_widget %}
32+
33+
{% block live_collection_button_delete_widget %}
34+
{{ block('button_widget') }}
35+
{% endblock live_collection_button_delete_widget %}

0 commit comments

Comments
 (0)