diff --git a/src/Form/Step.php b/src/Form/Step.php index b8b9accb..888c3b5e 100644 --- a/src/Form/Step.php +++ b/src/Form/Step.php @@ -83,18 +83,6 @@ public function hasErrors(): bool public function validate(): bool { - /** - * The error bag is reset when the validation of the current step passes. - * This leads to error messages of other steps being reset as well. - * To prevent this, we can just return early if the current step has errors. - * The fields of the step are still being validated with validateOnly() in the updatedFields() method. - */ - if ($this->hasErrors()) { - return false; - } - - Livewire::current()->storeAllStepErrors(); - $rules = $this->fields() ->mapWithKeys(fn (Field $field) => $field->rules()) ->toArray(); @@ -102,11 +90,11 @@ public function validate(): bool Livewire::current()->validate($rules); /* - * The error bag is reset when the current step is validated. + * The error bag is reset when the validation of a step passes. * This leads to error messages of other steps being reset as well. - * To prevent this, we restore the previous error bag after the validation. + * To prevent this, we restore the previous error bag. */ - Livewire::current()->restoreAllStepErrors(); + Livewire::current()->setStepErrors(); return true; } diff --git a/src/Livewire/Concerns/HandlesValidation.php b/src/Livewire/Concerns/HandlesValidation.php index 541783fb..73d9cd40 100644 --- a/src/Livewire/Concerns/HandlesValidation.php +++ b/src/Livewire/Concerns/HandlesValidation.php @@ -2,45 +2,73 @@ namespace Aerni\LivewireForms\Livewire\Concerns; -use Illuminate\Support\MessageBag; use Livewire\Attributes\Locked; +use Aerni\LivewireForms\Enums\StepStatus; +use Illuminate\Contracts\Validation\Validator; trait HandlesValidation { + // TODO: Should this only be added for the WizardForm? #[Locked] - public array $allStepErrors = []; + public array $stepErrors = []; public function bootHandlesValidation(): void { - /** - * Remove all fields that are not submittable from the data before validation to replicate - * Statamic's suggested validation pattern: https://statamic.dev/conditional-fields#validation - * This allows us to conditionally apply validation to conditionally shown fields using the 'sometimes' rule. - */ $this->withValidator(function ($validator) { + + /** + * Remove all fields that are not submittable from the data before validation to replicate + * Statamic's suggested validation pattern: https://statamic.dev/conditional-fields#validation + * This allows us to conditionally apply validation to conditionally shown fields using the 'sometimes' rule. + */ collect($validator->getValue('fields')) ->filter(fn ($value, $field) => $this->submittableFields[$field]) ->pipe(fn ($fields) => $validator->setValue('fields', $fields)); + + /** + * Validation errors in a WizardForm need special treatment. + */ + if (property_exists($this, 'currentStep')) { + $validator->after(function ($validator) { + /* Store the current errors so that we can restore them later. */ + $this->storeStepErrors($validator); + + /** + * If the validation of the current step fails, we need to merge all previously stored errors + * to ensure that we don't reset the validation state of other steps in the process. + */ + if ($validator->errors()->hasAny($this->currentStep()->fields()->map->key()->all())) { + /* Ensure we don't add errors that already exist. */ + $errors = array_diff_key($this->stepErrors, $validator->errors()->messages()); + $validator->errors()->merge($errors); + } + }); + } + }); } - public function storeAllStepErrors(): void + protected function storeStepErrors(Validator $validator): void { - $currentErrors = $this->getErrorBag()->messages(); + $currentErrors = $validator->errors()->messages(); + + $currentStepFields = $this->currentStep()->fields()->map->key()->flip(); - $stepFields = $this->currentStep()->fields()->map->key()->flip(); + $hiddenStepFields = $this->steps->where('status', StepStatus::Invisible)->flatMap->fields()->map->key()->flip(); - $resolvedErrors = collect($stepFields)->diffKeys($currentErrors); + $fieldsWithNoErrors = $currentStepFields->merge($hiddenStepFields)->diffKeys($currentErrors); - $this->allStepErrors = collect($this->allStepErrors) + $this->stepErrors = collect($this->stepErrors) ->merge($currentErrors) - ->diffKeys($resolvedErrors) + ->diffKeys($fieldsWithNoErrors) /* Ensure we remove resolved errors */ ->toArray(); + + $this->setStepErrors(); } - public function restoreAllStepErrors(): void + public function setStepErrors(): void { - $this->setErrorBag(new MessageBag($this->allStepErrors)); + $this->setErrorBag($this->stepErrors); } protected function rules(): array