Skip to content

LiveComponents: nested dependent fields example #944

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
Jun 19, 2023
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
1 change: 1 addition & 0 deletions ux.symfony.com/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"symfony/asset-mapper": "6.3.x-dev",
"symfony/console": "6.3.*",
"symfony/dotenv": "6.3.*",
"symfony/expression-language": "6.3.*",
"symfony/flex": "^2",
"symfony/form": "6.3.*",
"symfony/framework-bundle": "6.3.x-dev",
Expand Down
66 changes: 65 additions & 1 deletion ux.symfony.com/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions ux.symfony.com/src/Enum/Food.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Enum;

enum Food: string
{
case Eggs = 'eggs';
case Bacon = 'bacon';
case Strawberries = 'strawberries';
case Croissant = 'croissant';
case Bagel = 'bagel';
case Kiwi = 'kiwi';
case Avocado = 'avocado';
case Waffles = 'waffles';
case Pancakes = 'pancakes';
case Salad = 'salad';
case Tea = 'tea️';
case Sandwich = 'sandwich';
case Cheese = 'cheese';
case Sushi = 'sushi';
case Pizza = 'pizza';
case Pint = 'pint';
case Pasta = 'pasta';

public function getReadable(): string
{
return match ($this) {
self::Eggs => 'Eggs 🍳',
self::Bacon => 'Bacon 🥓',
self::Strawberries => 'Strawberries 🍓',
self::Croissant => 'Croissant 🥐',
self::Bagel => 'Bagel 🥯',
self::Kiwi => 'Kiwi 🥝',
self::Avocado => 'Avocado 🥑',
self::Waffles => 'Waffles 🧇',
self::Pancakes => 'Pancakes 🥞',
self::Salad => 'Salad 🥙',
self::Tea => 'Tea ☕️',
self::Sandwich => 'Sandwich 🥪',
self::Cheese => 'Cheese 🧀',
self::Sushi => 'Sushi 🍱',
self::Pizza => 'Pizza 🍕',
self::Pint => 'A Pint 🍺',
self::Pasta => 'Pasta 🍝',
};
}
}
37 changes: 37 additions & 0 deletions ux.symfony.com/src/Enum/Meal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Enum;

enum Meal: string
{
case Breakfast = 'breakfast';
case SecondBreakfast = 'second_breakfast';
case Elevenses = 'elevenses';
case Lunch = 'lunch';
case Dinner = 'dinner';

public function getReadable(): string
{
return match ($this) {
self::Breakfast => 'Breakfast',
self::SecondBreakfast => 'Second Breakfast',
self::Elevenses => 'Elevenses',
self::Lunch => 'Lunch',
self::Dinner => 'Dinner',
};
}

/**
* @return list<Food>
*/
public function getFoodChoices(): array
{
return match ($this) {
self::Breakfast => [Food::Eggs, Food::Bacon, Food::Strawberries, Food::Croissant],
self::SecondBreakfast => [Food::Bagel, Food::Kiwi, Food::Avocado, Food::Waffles],
self::Elevenses => [Food::Pancakes, Food::Strawberries, Food::Tea],
self::Lunch => [Food::Sandwich, Food::Cheese, Food::Sushi],
self::Dinner => [Food::Pizza, Food::Pint, Food::Pasta],
};
}
}
19 changes: 19 additions & 0 deletions ux.symfony.com/src/Enum/PizzaSize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Enum;

enum PizzaSize: int
{
case Small = 12;
case Medium = 14;
case Large = 16;

public function getReadable(): string
{
return match ($this) {
self::Small => '12 inch',
self::Medium => '14 inch',
self::Large => '16 inch',
};
}
}
140 changes: 82 additions & 58 deletions ux.symfony.com/src/Form/MealPlannerForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,118 @@

namespace App\Form;

use App\Enum\Food;
use App\Enum\Meal;
use App\Enum\PizzaSize;
use App\Model\MealPlan;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MealPlannerForm extends AbstractType
{
public const MEAL_BREAKFAST = 'breakfast';
public const MEAL_SECOND_BREAKFAST = 'second breakfast';
public const MEAL_ELEVENSES = 'elevenses';
public const MEAL_LUNCH = 'lunch';
public const MEAL_DINNER = 'dinner';
private FormFactoryInterface $factory;

/**
* @var array<string, mixed>
*/
private $dependencies = [];

public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'Breakfast' => self::MEAL_BREAKFAST,
'Second Breakfast' => self::MEAL_SECOND_BREAKFAST,
'Elevenses' => self::MEAL_ELEVENSES,
'Lunch' => self::MEAL_LUNCH,
'Dinner' => self::MEAL_DINNER,
];
$builder->add('meal', ChoiceType::class, [
'choices' => $choices,
$this->factory = $builder->getFormFactory();

$builder->add('meal', EnumType::class, [
'class' => Meal::class,
'choice_label' => fn (Meal $meal): string => $meal->getReadable(),
'placeholder' => 'Which meal is it?',
'autocomplete' => true,
]);

$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
// the object tied to your form
/** @var ?MealPlan $data */
$data = $event->getData();

$meal = $data?->getMeal();
$this->addFoodField($event->getForm(), $meal);
}
);
$builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'onPreSetData']);
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmit']);

$builder->get('meal')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) {
// It's important here to fetch $event->getForm()->getData(), as
// $event->getData() will get you the client data (that is, the ID)
$meal = $event->getForm()->getData();

// since we've added the listener to the child, we'll have to pass on
// the parent to the callback functions!
$this->addFoodField($event->getForm()->getParent(), $meal);
}
);
$builder->get('meal')->addEventListener(FormEvents::POST_SUBMIT, [$this, 'storeDependencies']);
$builder->get('meal')->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmitMeal']);
}

public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => MealPlan::class]);
}

private function getAvailableFoodChoices(string $meal): array
public function onPreSetData(FormEvent $event): void
{
$foods = match ($meal) {
self::MEAL_BREAKFAST => ['Eggs 🍳', 'Bacon 🥓', 'Strawberries 🍓', 'Croissant 🥐'],
self::MEAL_SECOND_BREAKFAST => ['Bagel 🥯', 'Kiwi 🥝', 'Avocado 🥑', 'Waffles 🧇'],
self::MEAL_ELEVENSES => ['Pancakes 🥞', 'Salad 🥙', 'Tea ☕️'],
self::MEAL_LUNCH => ['Sandwich 🥪', 'Cheese 🧀', 'Sushi 🍱'],
self::MEAL_DINNER => ['Pizza 🍕', 'A Pint 🍺', 'Pasta 🍝'],
};
// the object tied to your form
/** @var ?MealPlan $data */
$data = $event->getData();

$this->addFoodField($event->getForm(), $data?->getMeal());
$this->addPizzaSizeField($event->getForm(), $data?->getPizzaSize());
}

$foods = array_combine($foods, $foods);
public function onPostSubmit(FormEvent $event): void
{
$this->dependencies = [];
}

return $foods;
public function storeDependencies(FormEvent $event): void
{
$this->dependencies[$event->getForm()->getName()] = $event->getForm()->getData();
}

public function addFoodField(FormInterface $form, ?string $meal)
public function onPostSubmitMeal(FormEvent $event): void
{
$foodChoices = null === $meal ? [] : $this->getAvailableFoodChoices($meal);

$form->add('mainFood', ChoiceType::class, [
'placeholder' => null === $meal ? 'Select a meal first' : sprintf('What\'s for %s?', $meal),
'choices' => $foodChoices,
'disabled' => null === $meal,
// silence real-time "invalid" message when switching "meals"
'invalid_message' => false,
$this->addFoodField(
$event->getForm()->getParent(),
$this->dependencies['meal'],
);
}

public function onPostSubmitFood(FormEvent $event): void
{
$this->addPizzaSizeField(
$event->getForm()->getParent(),
$this->dependencies['mainFood'],
);
}

public function addFoodField(FormInterface $form, ?Meal $meal): void
{
$mainFood = $this->factory
->createNamedBuilder('mainFood', EnumType::class, $meal, [
'class' => Food::class,
'placeholder' => null === $meal ? 'Select a meal first' : sprintf('What\'s for %s?', $meal->getReadable()),
'choices' => $meal?->getFoodChoices(),
'choice_label' => fn (Food $food): string => $food->getReadable(),
'disabled' => null === $meal,
// silence real-time "invalid" message when switching "meals"
'invalid_message' => false,
'autocomplete' => true,
'auto_initialize' => false,
])
->addEventListener(FormEvents::POST_SUBMIT, [$this, 'storeDependencies'])
->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmitFood']);

$form->add($mainFood->getForm());
}

public function addPizzaSizeField(FormInterface $form, ?Food $food): void
{
if (Food::Pizza !== $food) {
return;
}

$form->add('pizzaSize', EnumType::class, [
'class' => PizzaSize::class,
'placeholder' => 'What size pizza?',
'choice_label' => fn (PizzaSize $pizzaSize): string => $pizzaSize->getReadable(),
'required' => true,
'autocomplete' => true,
]);
}
Expand Down
Loading