|
1 | | -# Dynamic Symfony Forms! Woh! |
| 1 | +# Dynamic / Dependent Symfony Form Fields |
2 | 2 |
|
3 | | -TODO |
| 3 | +Ever have a form field that depends on another? |
| 4 | + |
| 5 | +* Show a field only if another field is set to a specific value; |
| 6 | +* Change the options of a field based on the value of another field; |
| 7 | +* Have multiple-level dependencies (e.g. field A depends on field B |
| 8 | + which depends on field C). |
| 9 | + |
| 10 | +``` |
| 11 | +public function buildForm(FormBuilderInterface $builder, array $options): void |
| 12 | +{ |
| 13 | + $builder = new DynamicFormBuilder($builder); |
| 14 | +
|
| 15 | + $builder->add('meal', ChoiceType::class, [ |
| 16 | + 'choices' => [ |
| 17 | + 'Breakfast' => 'breakfast', |
| 18 | + 'Lunch' => 'lunch', |
| 19 | + 'Dinner' => 'dinner', |
| 20 | + ], |
| 21 | + ]); |
| 22 | +
|
| 23 | + $builder->addDependent('mainFood', ['meal'], function(DependentField $field, string $meal) { |
| 24 | + // dynamically add choices based on the meal! |
| 25 | + $choices = ['...']; |
| 26 | + |
| 27 | + $field->add(ChoiceType::class, [ |
| 28 | + 'placeholder' => null === $meal ? 'Select a meal first' : sprintf('What is for %s?', $meal->getReadable()), |
| 29 | + 'choices' => $choices, |
| 30 | + 'disabled' => null === $meal, |
| 31 | + ]); |
| 32 | + }); |
| 33 | +``` |
| 34 | + |
| 35 | +## Installation |
| 36 | + |
| 37 | +Install the package with: |
| 38 | + |
| 39 | +```bash |
| 40 | +composer require symfonycasts/dynamic-forms |
| 41 | +``` |
| 42 | + |
| 43 | +Done - you're ready to build dynamic forms! |
| 44 | + |
| 45 | +## Usage |
| 46 | + |
| 47 | +Setting up a dependent field is two parts: |
| 48 | + |
| 49 | +1. [Usage in PHP](#usage-in-php) - set up your Symfony form to handle |
| 50 | + the dynamic fields; |
| 51 | +2. [Updating the Frontend](#updating-the-frontend) - adding code to your |
| 52 | + frontend so that when one field changes, part of the form is re-rendered. |
| 53 | + |
| 54 | +## Usage in PHP |
| 55 | + |
| 56 | +Start by wrapping your `FormBuilderInterface` with a `DynamicFormBuilder`: |
| 57 | + |
| 58 | +```php |
| 59 | +use Symfonycasts\DynamicForms\DynamicFormBuilder; |
| 60 | +// ... |
| 61 | + |
| 62 | +public function buildForm(FormBuilderInterface $builder, array $options): void |
| 63 | +{ |
| 64 | + $builder = new DynamicFormBuilder($builder); |
| 65 | + |
| 66 | + // ... |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +`DynamicFormBuilder` has all the same methods as `FormBuilderInterface` plus |
| 71 | +one extra: `addDependent()`. If a field depends on another, use this method |
| 72 | +instead of `add()` |
| 73 | + |
| 74 | +```php |
| 75 | +// src/Form/FeedbackForm.php |
| 76 | + |
| 77 | +// ... |
| 78 | +use Symfonycasts\DynamicForms\DependentField; |
| 79 | +use Symfonycasts\DynamicForms\DynamicFormBuilder; |
| 80 | + |
| 81 | +class FeedbackForm extends AbstractType |
| 82 | +{ |
| 83 | + public function buildForm(FormBuilderInterface $builder, array $options) |
| 84 | + { |
| 85 | + $builder = new DynamicFormBuilder($builder); |
| 86 | + |
| 87 | + $builder->add('rating', ChoiceType::class, [ |
| 88 | + 'choices' => [ |
| 89 | + 'Select a rating' => null, |
| 90 | + 'Great' => 5, |
| 91 | + 'Good' => 4, |
| 92 | + 'Okay' => 3, |
| 93 | + 'Bad' => 2, |
| 94 | + 'Terrible' => 1 |
| 95 | + ], |
| 96 | + ]); |
| 97 | + |
| 98 | + $builder->addDependent('badRatingNotes', 'rating', function(DependentField $field, ?int $rating) { |
| 99 | + if (null === $rating || $rating >= 3) { |
| 100 | + return; // field not needed |
| 101 | + } |
| 102 | + |
| 103 | + $field->add(TextareaType::class, [ |
| 104 | + 'label' => 'What went wrong?', |
| 105 | + 'attr' => ['rows' => 3], |
| 106 | + 'help' => sprintf('Because you gave a %d rating, we\'d love to know what went wrong.', $rating), |
| 107 | + ]); |
| 108 | + }); |
| 109 | + } |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +The `addDependent()` method takes 3 arguments: |
| 114 | + |
| 115 | +1. The name of the field to add; |
| 116 | +2. The name (or names) of the field that this field depends on; |
| 117 | +3. A callback that will be called when the form is submitted. This callback |
| 118 | + receives a `DependentField` object as the first argument then the |
| 119 | + value of each dependent field as the next arguments. |
| 120 | + |
| 121 | +Behind the scenes, this works by registering several form event listeners. |
| 122 | +The callback be executed when the form is first created (using the initial |
| 123 | +data) and then again when the form is submitted. This means that the callback |
| 124 | +may be called multiple times. |
| 125 | + |
| 126 | +Rendering the field is the same - just be sure to make sure the field exists |
| 127 | +if it's conditionally added: |
| 128 | + |
| 129 | +```twig |
| 130 | +{{ form_start(form) }} |
| 131 | + {{ form_row(form.rating) }} |
| 132 | +
|
| 133 | + {% if form.badRatingNotes is defined %} |
| 134 | + {{ form_row(form.badRatingNotes) }} |
| 135 | + {% endif %} |
| 136 | + |
| 137 | + <button>Send Feedback</button> |
| 138 | +{{ form_end(form) }} |
| 139 | +``` |
| 140 | + |
| 141 | +## Updating the Frontend |
| 142 | + |
| 143 | +In the previous example, when the `rating` field changes, the form (or part of |
| 144 | +the form) needs to be re-rendered so the `badRatingNotes` field can be added. |
| 145 | + |
| 146 | +This library doesn't handle this for you, but here are the 2 main options: |
| 147 | + |
| 148 | +### A) Use [Live Components](https://symfony.com/bundles/ux-live-component/current/index.html) |
| 149 | + |
| 150 | +This is the easiest method: by rendering your form inside a live component, |
| 151 | +it will automatically re-render when the form changes. |
| 152 | + |
| 153 | +### B) Write custom JavaScript |
| 154 | + |
| 155 | +If you're not using Live Components, you'll need to write some custom |
| 156 | +JavaScript to listen to the `change` event on the `rating` field and then |
| 157 | +make an AJAX call to re-render the form. The AJAX call should submit the |
| 158 | +form to its usual endpoint (or any endpoint that will submit the form), take |
| 159 | +the HTML response, extract the parts that need to be re-rendered and then replace |
| 160 | +the HTML on the page. |
| 161 | + |
| 162 | +This is a non-trivial task and there may be room for improvement in this |
| 163 | +library to make this easier. If you have ideas, please open an issue! |
0 commit comments