1111
1212use Symfony \Component \EventDispatcher \EventDispatcherInterface ;
1313use Symfony \Component \EventDispatcher \EventSubscriberInterface ;
14+ use Symfony \Component \Form \ClearableErrorsInterface ;
1415use Symfony \Component \Form \DataMapperInterface ;
1516use Symfony \Component \Form \DataTransformerInterface ;
17+ use Symfony \Component \Form \Extension \Core \Type \HiddenType ;
1618use Symfony \Component \Form \FormBuilderInterface ;
1719use Symfony \Component \Form \FormConfigInterface ;
20+ use Symfony \Component \Form \FormError ;
1821use Symfony \Component \Form \FormEvent ;
1922use Symfony \Component \Form \FormEvents ;
2023use Symfony \Component \Form \FormFactoryInterface ;
@@ -49,11 +52,29 @@ public function __construct(private FormBuilderInterface $builder)
4952 $ this ->form = $ event ->getForm ();
5053 $ this ->preSetDataDependencyData = [];
5154 $ this ->initializeListeners ();
55+
56+ // A fake hidden field where we can "store" an error if a dependent form
57+ // field is suddenly invalid because its previous data was invalid
58+ // and a field it depends on just changed (e.g. user selected "Michigan"
59+ // as a state, then the user changed "Country" from "USA" to "Mexico"
60+ // and so now "Michigan" is invalid). In this case, we clear the error
61+ // on the actual field, but store a "fake" error here, which won't be
62+ // rendered, but will prevent the form from being valid.
63+ if (!$ this ->form ->has ('__dynamic_error ' )) {
64+ $ this ->form ->add ('__dynamic_error ' , HiddenType::class, [
65+ 'mapped ' => false ,
66+ 'error_bubbling ' => false ,
67+ ]);
68+ }
5269 }, 100 );
5370
54- $ builder ->addEventListener (FormEvents::POST_SUBMIT , function () {
71+ $ builder ->addEventListener (FormEvents::POST_SUBMIT , function (FormEvent $ event ) {
5572 $ this ->postSubmitDependencyData = [];
5673 });
74+ // guarantee later than core ValidationListener
75+ $ builder ->addEventListener (FormEvents::POST_SUBMIT , function (FormEvent $ event ) {
76+ $ this ->clearDataOnTransformationError ($ event );
77+ }, -1 );
5778 }
5879
5980 public function addDependent (string $ name , array $ dependencies , callable $ callback ): self
@@ -79,6 +100,34 @@ public function storePostSubmitDependencyData(FormEvent $event): void
79100 $ this ->executeReadyCallbacks ($ this ->postSubmitDependencyData , FormEvents::POST_SUBMIT );
80101 }
81102
103+ public function clearDataOnTransformationError (FormEvent $ event ): void
104+ {
105+ $ form = $ event ->getForm ();
106+ $ transformationErrorsCleared = false ;
107+ foreach ($ this ->dependentFieldConfigs as $ dependentFieldConfig ) {
108+ if (!$ form ->has ($ dependentFieldConfig ->name )) {
109+ continue ;
110+ }
111+
112+ $ subForm = $ form ->get ($ dependentFieldConfig ->name );
113+ if ($ subForm ->getTransformationFailure () && $ subForm instanceof ClearableErrorsInterface) {
114+ $ subForm ->clearErrors ();
115+ $ transformationErrorsCleared = true ;
116+ }
117+ }
118+
119+ if ($ transformationErrorsCleared ) {
120+ // We've cleared the error, but the bad data remains on the field.
121+ // We need to make sure that the form doesn't submit successfully,
122+ // but we also don't want to render a validation error on any field.
123+ // So, we jam the error into a hidden field, which doesn't render errors.
124+ if ($ form ->get ('__dynamic_error ' )->isValid ()) {
125+ $ form ->get ('__dynamic_error ' )->addError (new FormError ('Some dynamic fields have errors. ' ));
126+ }
127+ dump ($ form );
128+ }
129+ }
130+
82131 private function executeReadyCallbacks (array $ availableDependencyData , string $ eventName ): void
83132 {
84133 foreach ($ this ->dependentFieldConfigs as $ dependentFieldConfig ) {
@@ -129,7 +178,11 @@ private function initializeListeners(array $fieldsToConsider = null): void
129178 }
130179
131180 /*
132- * Pure method declarations below
181+ * ----------------------------------------
182+ *
183+ * Pure decoration methods below.
184+ *
185+ * ----------------------------------------
133186 */
134187
135188 public function count (): int
0 commit comments