Skip to content

Commit abb76c5

Browse files
committed
minor #944 LiveComponents: nested dependent fields example (bendavies)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- LiveComponents: nested dependent fields example | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | Tickets | instead --> | License | MIT This PR update the LiveComponents dependant form fields example to include a 3rd dependent field which is dependent on the 2nd. Here, we need to use `FormFactoryInterface::createNamedBuilder()` to get access to `addEventListener()`, as that method is not available on `FormInterface`. Some refactoring was done to introduce enums to make this a bit more managable. ![pizza-size](https://github.com/symfony/ux/assets/625392/20b1a2bd-8e25-4eed-9100-f800ae6f0e77) Commits ------- 166500d LiveComponents: nested dependent fields example
2 parents cb4e91d + 166500d commit abb76c5

File tree

8 files changed

+284
-68
lines changed

8 files changed

+284
-68
lines changed

ux.symfony.com/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"symfony/asset-mapper": "6.3.x-dev",
2121
"symfony/console": "6.3.*",
2222
"symfony/dotenv": "6.3.*",
23+
"symfony/expression-language": "6.3.*",
2324
"symfony/flex": "^2",
2425
"symfony/form": "6.3.*",
2526
"symfony/framework-bundle": "6.3.x-dev",

ux.symfony.com/composer.lock

Lines changed: 65 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ux.symfony.com/src/Enum/Food.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace App\Enum;
4+
5+
enum Food: string
6+
{
7+
case Eggs = 'eggs';
8+
case Bacon = 'bacon';
9+
case Strawberries = 'strawberries';
10+
case Croissant = 'croissant';
11+
case Bagel = 'bagel';
12+
case Kiwi = 'kiwi';
13+
case Avocado = 'avocado';
14+
case Waffles = 'waffles';
15+
case Pancakes = 'pancakes';
16+
case Salad = 'salad';
17+
case Tea = 'tea️';
18+
case Sandwich = 'sandwich';
19+
case Cheese = 'cheese';
20+
case Sushi = 'sushi';
21+
case Pizza = 'pizza';
22+
case Pint = 'pint';
23+
case Pasta = 'pasta';
24+
25+
public function getReadable(): string
26+
{
27+
return match ($this) {
28+
self::Eggs => 'Eggs 🍳',
29+
self::Bacon => 'Bacon 🥓',
30+
self::Strawberries => 'Strawberries 🍓',
31+
self::Croissant => 'Croissant 🥐',
32+
self::Bagel => 'Bagel 🥯',
33+
self::Kiwi => 'Kiwi 🥝',
34+
self::Avocado => 'Avocado 🥑',
35+
self::Waffles => 'Waffles 🧇',
36+
self::Pancakes => 'Pancakes 🥞',
37+
self::Salad => 'Salad 🥙',
38+
self::Tea => 'Tea ☕️',
39+
self::Sandwich => 'Sandwich 🥪',
40+
self::Cheese => 'Cheese 🧀',
41+
self::Sushi => 'Sushi 🍱',
42+
self::Pizza => 'Pizza 🍕',
43+
self::Pint => 'A Pint 🍺',
44+
self::Pasta => 'Pasta 🍝',
45+
};
46+
}
47+
}

ux.symfony.com/src/Enum/Meal.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Enum;
4+
5+
enum Meal: string
6+
{
7+
case Breakfast = 'breakfast';
8+
case SecondBreakfast = 'second_breakfast';
9+
case Elevenses = 'elevenses';
10+
case Lunch = 'lunch';
11+
case Dinner = 'dinner';
12+
13+
public function getReadable(): string
14+
{
15+
return match ($this) {
16+
self::Breakfast => 'Breakfast',
17+
self::SecondBreakfast => 'Second Breakfast',
18+
self::Elevenses => 'Elevenses',
19+
self::Lunch => 'Lunch',
20+
self::Dinner => 'Dinner',
21+
};
22+
}
23+
24+
/**
25+
* @return list<Food>
26+
*/
27+
public function getFoodChoices(): array
28+
{
29+
return match ($this) {
30+
self::Breakfast => [Food::Eggs, Food::Bacon, Food::Strawberries, Food::Croissant],
31+
self::SecondBreakfast => [Food::Bagel, Food::Kiwi, Food::Avocado, Food::Waffles],
32+
self::Elevenses => [Food::Pancakes, Food::Strawberries, Food::Tea],
33+
self::Lunch => [Food::Sandwich, Food::Cheese, Food::Sushi],
34+
self::Dinner => [Food::Pizza, Food::Pint, Food::Pasta],
35+
};
36+
}
37+
}

ux.symfony.com/src/Enum/PizzaSize.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Enum;
4+
5+
enum PizzaSize: int
6+
{
7+
case Small = 12;
8+
case Medium = 14;
9+
case Large = 16;
10+
11+
public function getReadable(): string
12+
{
13+
return match ($this) {
14+
self::Small => '12 inch',
15+
self::Medium => '14 inch',
16+
self::Large => '16 inch',
17+
};
18+
}
19+
}

ux.symfony.com/src/Form/MealPlannerForm.php

Lines changed: 82 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,118 @@
22

33
namespace App\Form;
44

5+
use App\Enum\Food;
6+
use App\Enum\Meal;
7+
use App\Enum\PizzaSize;
58
use App\Model\MealPlan;
69
use Symfony\Component\Form\AbstractType;
7-
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
10+
use Symfony\Component\Form\Extension\Core\Type\EnumType;
811
use Symfony\Component\Form\FormBuilderInterface;
912
use Symfony\Component\Form\FormEvent;
1013
use Symfony\Component\Form\FormEvents;
14+
use Symfony\Component\Form\FormFactoryInterface;
1115
use Symfony\Component\Form\FormInterface;
1216
use Symfony\Component\OptionsResolver\OptionsResolver;
1317

1418
class MealPlannerForm extends AbstractType
1519
{
16-
public const MEAL_BREAKFAST = 'breakfast';
17-
public const MEAL_SECOND_BREAKFAST = 'second breakfast';
18-
public const MEAL_ELEVENSES = 'elevenses';
19-
public const MEAL_LUNCH = 'lunch';
20-
public const MEAL_DINNER = 'dinner';
20+
private FormFactoryInterface $factory;
21+
22+
/**
23+
* @var array<string, mixed>
24+
*/
25+
private $dependencies = [];
2126

2227
public function buildForm(FormBuilderInterface $builder, array $options)
2328
{
24-
$choices = [
25-
'Breakfast' => self::MEAL_BREAKFAST,
26-
'Second Breakfast' => self::MEAL_SECOND_BREAKFAST,
27-
'Elevenses' => self::MEAL_ELEVENSES,
28-
'Lunch' => self::MEAL_LUNCH,
29-
'Dinner' => self::MEAL_DINNER,
30-
];
31-
$builder->add('meal', ChoiceType::class, [
32-
'choices' => $choices,
29+
$this->factory = $builder->getFormFactory();
30+
31+
$builder->add('meal', EnumType::class, [
32+
'class' => Meal::class,
33+
'choice_label' => fn (Meal $meal): string => $meal->getReadable(),
3334
'placeholder' => 'Which meal is it?',
3435
'autocomplete' => true,
3536
]);
3637

37-
$builder->addEventListener(
38-
FormEvents::PRE_SET_DATA,
39-
function (FormEvent $event) {
40-
// the object tied to your form
41-
/** @var ?MealPlan $data */
42-
$data = $event->getData();
43-
44-
$meal = $data?->getMeal();
45-
$this->addFoodField($event->getForm(), $meal);
46-
}
47-
);
38+
$builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'onPreSetData']);
39+
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmit']);
4840

49-
$builder->get('meal')->addEventListener(
50-
FormEvents::POST_SUBMIT,
51-
function (FormEvent $event) {
52-
// It's important here to fetch $event->getForm()->getData(), as
53-
// $event->getData() will get you the client data (that is, the ID)
54-
$meal = $event->getForm()->getData();
55-
56-
// since we've added the listener to the child, we'll have to pass on
57-
// the parent to the callback functions!
58-
$this->addFoodField($event->getForm()->getParent(), $meal);
59-
}
60-
);
41+
$builder->get('meal')->addEventListener(FormEvents::POST_SUBMIT, [$this, 'storeDependencies']);
42+
$builder->get('meal')->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmitMeal']);
6143
}
6244

63-
public function configureOptions(OptionsResolver $resolver)
45+
public function configureOptions(OptionsResolver $resolver): void
6446
{
6547
$resolver->setDefaults(['data_class' => MealPlan::class]);
6648
}
6749

68-
private function getAvailableFoodChoices(string $meal): array
50+
public function onPreSetData(FormEvent $event): void
6951
{
70-
$foods = match ($meal) {
71-
self::MEAL_BREAKFAST => ['Eggs 🍳', 'Bacon 🥓', 'Strawberries 🍓', 'Croissant 🥐'],
72-
self::MEAL_SECOND_BREAKFAST => ['Bagel 🥯', 'Kiwi 🥝', 'Avocado 🥑', 'Waffles 🧇'],
73-
self::MEAL_ELEVENSES => ['Pancakes 🥞', 'Salad 🥙', 'Tea ☕️'],
74-
self::MEAL_LUNCH => ['Sandwich 🥪', 'Cheese 🧀', 'Sushi 🍱'],
75-
self::MEAL_DINNER => ['Pizza 🍕', 'A Pint 🍺', 'Pasta 🍝'],
76-
};
52+
// the object tied to your form
53+
/** @var ?MealPlan $data */
54+
$data = $event->getData();
55+
56+
$this->addFoodField($event->getForm(), $data?->getMeal());
57+
$this->addPizzaSizeField($event->getForm(), $data?->getPizzaSize());
58+
}
7759

78-
$foods = array_combine($foods, $foods);
60+
public function onPostSubmit(FormEvent $event): void
61+
{
62+
$this->dependencies = [];
63+
}
7964

80-
return $foods;
65+
public function storeDependencies(FormEvent $event): void
66+
{
67+
$this->dependencies[$event->getForm()->getName()] = $event->getForm()->getData();
8168
}
8269

83-
public function addFoodField(FormInterface $form, ?string $meal)
70+
public function onPostSubmitMeal(FormEvent $event): void
8471
{
85-
$foodChoices = null === $meal ? [] : $this->getAvailableFoodChoices($meal);
86-
87-
$form->add('mainFood', ChoiceType::class, [
88-
'placeholder' => null === $meal ? 'Select a meal first' : sprintf('What\'s for %s?', $meal),
89-
'choices' => $foodChoices,
90-
'disabled' => null === $meal,
91-
// silence real-time "invalid" message when switching "meals"
92-
'invalid_message' => false,
72+
$this->addFoodField(
73+
$event->getForm()->getParent(),
74+
$this->dependencies['meal'],
75+
);
76+
}
77+
78+
public function onPostSubmitFood(FormEvent $event): void
79+
{
80+
$this->addPizzaSizeField(
81+
$event->getForm()->getParent(),
82+
$this->dependencies['mainFood'],
83+
);
84+
}
85+
86+
public function addFoodField(FormInterface $form, ?Meal $meal): void
87+
{
88+
$mainFood = $this->factory
89+
->createNamedBuilder('mainFood', EnumType::class, $meal, [
90+
'class' => Food::class,
91+
'placeholder' => null === $meal ? 'Select a meal first' : sprintf('What\'s for %s?', $meal->getReadable()),
92+
'choices' => $meal?->getFoodChoices(),
93+
'choice_label' => fn (Food $food): string => $food->getReadable(),
94+
'disabled' => null === $meal,
95+
// silence real-time "invalid" message when switching "meals"
96+
'invalid_message' => false,
97+
'autocomplete' => true,
98+
'auto_initialize' => false,
99+
])
100+
->addEventListener(FormEvents::POST_SUBMIT, [$this, 'storeDependencies'])
101+
->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmitFood']);
102+
103+
$form->add($mainFood->getForm());
104+
}
105+
106+
public function addPizzaSizeField(FormInterface $form, ?Food $food): void
107+
{
108+
if (Food::Pizza !== $food) {
109+
return;
110+
}
111+
112+
$form->add('pizzaSize', EnumType::class, [
113+
'class' => PizzaSize::class,
114+
'placeholder' => 'What size pizza?',
115+
'choice_label' => fn (PizzaSize $pizzaSize): string => $pizzaSize->getReadable(),
116+
'required' => true,
93117
'autocomplete' => true,
94118
]);
95119
}

0 commit comments

Comments
 (0)