Skip to content

Commit

Permalink
FIO-9360: validate current page only on wizard change (#5920)
Browse files Browse the repository at this point in the history
* validate current page on wizard change

* add tests

* fix tests

* Specify correct flag for form validation after failed wizard submission

- this.validate() is only called in onChange() if the form previously failed to submit. Because of this, the form will already be dirty, so it makes sense to reiterate this by passing dirty: true instead of false.

---------

Co-authored-by: Blake Krammes <[email protected]>
  • Loading branch information
2 people authored and lane-formio committed Nov 26, 2024
1 parent b916fd4 commit 86ae433
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 21 deletions.
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased: 5.0.0-rc.100]
- FIO-9360: validate current page only on wizard change

## 5.0.0-rc.99
### Changed
- Updated @formio/core@2.3.0-rc.22
Expand Down
17 changes: 4 additions & 13 deletions src/Wizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -824,8 +824,9 @@ export default class Wizard extends Webform {
}

validateCurrentPage(flags = {}) {
const components = this.currentPage?.components.map((component) => component.component);
// Accessing the parent ensures the right instance (whether it's the parent Wizard or a nested Wizard) performs its validation
return this.currentPage?.parent.validateComponents(this.currentPage.component.components, this.currentPage.parent.data, flags);
return this.currentPage?.parent.validateComponents(components, this.currentPage.parent.data, flags);
}

emitPrevPage() {
Expand Down Expand Up @@ -1000,7 +1001,8 @@ export default class Wizard extends Webform {

onChange(flags, changed, modified, changes) {
super.onChange(flags, changed, modified, changes);
const errors = this.validate(this.localData, { dirty: false });
// The onChange loop doesn't need all components for wizards
const errors = this.submitted ? this.validate(this.localData, { dirty: true }) : this.validateCurrentPage();
if (this.alert) {
this.showErrors(errors, true, true);
}
Expand Down Expand Up @@ -1073,17 +1075,6 @@ export default class Wizard extends Webform {
return super.errors;
}

showErrors(errors, triggerEvent) {
if (this.hasExtraPages) {
this.subWizards.forEach((subWizard) => {
if(Array.isArray(subWizard.errors)) {
errors = [...errors, ...subWizard.errors]
}
})
};
return super.showErrors(errors, triggerEvent)
}

focusOnComponent(key) {
const component = this.getComponent(key);
if (component) {
Expand Down
152 changes: 145 additions & 7 deletions src/Wizard.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import formsWithAllowOverride from '../test/forms/formsWithAllowOverrideComps';
import WizardWithCheckboxes from '../test/forms/wizardWithCheckboxes';
import WizardWithRequiredFields from '../test/forms/wizardWithRequiredFields';
import formWithNestedWizardAndRequiredFields from '../test/forms/formWithNestedWizardAndRequiredFields';
import { wait } from '../test/util';

// eslint-disable-next-line max-statements
describe('Wizard tests', () => {
Expand Down Expand Up @@ -392,7 +393,7 @@ describe('Wizard tests', () => {
.catch((err) => done(err));
}).timeout(6000);

it('Should trigger validation of nested wizard before going to the next page', function(done) {
it('Should trigger validation of nested wizard before going to the next page', function (done) {
const formElement = document.createElement('div');
const wizard = new Wizard(formElement);
const nestedWizard = _.cloneDeep(WizardWithRequiredFields);
Expand All @@ -417,7 +418,7 @@ describe('Wizard tests', () => {
wizard.setForm(parentWizard).then(() => {
const nestedFormComp = wizard.getComponent('formNested');

nestedFormComp.loadSubForm = ()=> {
nestedFormComp.loadSubForm = () => {
nestedFormComp.formObj = nestedWizard;
nestedFormComp.subFormLoading = false;
return new Promise((resolve) => resolve(nestedWizard));
Expand All @@ -441,15 +442,152 @@ describe('Wizard tests', () => {
assert.equal(errors.length, 2, 'Must err before next page');
errors.forEach((error) => {
assert.equal(error.ruleName, 'required');
assert.equal(error.message, 'Text Field is required' , 'Should set correct lebel in the error message');
assert.equal(error.message, 'Text Field is required', 'Should set correct lebel in the error message');
});
done();
}, 300)
}, 300)
}, 300)
})
.catch((err) => done(err));
})
.catch((err) => done(err));
});

it('Should only validate the current page components when the form has not been submitted', async function () {
const wizardDefinition = {
display: 'wizard',
components: [
{
title: 'Page 1',
collapsible: false,
key: 'page1',
type: 'panel',
label: 'Panel',
input: false,
tableView: false,
components: [
{
type: 'checkbox',
label: 'Trigger Change',
input: true,
key: 'triggerChange',
},
{
label: 'Text Field',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textField',
type: 'textfield',
input: true,
validate: {
required: true
}
},
],
},
{
title: 'Page 2',
collapsible: false,
key: 'page2',
type: 'panel',
label: 'Panel',
input: false,
tableView: false,
components: [
{
label: 'Text Field',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textField1',
type: 'textfield',
validate: {
required: true
},
input: true,
},
],
},
],
};

const form = await Formio.createForm(document.createElement('div'), wizardDefinition);
assert(form, 'Form should be created');
const checkbox = form.getComponent('triggerChange');
const clickEvent = new Event('click');
checkbox.refs.input[0].dispatchEvent(clickEvent);
await wait(200);
assert.equal(form.errors.length, 1, 'Should have one error from the first page');
});

it('Should validate the entire wizard\'s components when the form has been submitted', async function () {
const wizardDefinition = {
display: 'wizard',
components: [
{
title: 'Page 1',
collapsible: false,
key: 'page1',
type: 'panel',
label: 'Panel',
input: false,
tableView: false,
components: [
{
type: 'checkbox',
label: 'Trigger Change',
input: true,
key: 'triggerChange',
},
{
label: 'Text Field',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textField',
type: 'textfield',
input: true,
validate: {
required: true
}
},
],
},
{
title: 'Page 2',
collapsible: false,
key: 'page2',
type: 'panel',
label: 'Panel',
input: false,
tableView: false,
components: [
{
label: 'Text Field',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textField1',
type: 'textfield',
validate: {
required: true
},
input: true,
},
],
},
],
};

const form = await Formio.createForm(document.createElement('div'), wizardDefinition);
assert(form, 'Form should be created');
form.submitted = true;
form.setSubmission({ data: { triggerChange: true } });
await wait(200);
assert(form.alert, 'Form should have an error list');
assert.equal(form.alert.querySelectorAll("span[ref=errorRef]").length, 2, 'Should have two errors total');
});


it('Should have validation errors when parent form is valid but nested wizard is not', function(done) {
const formElement = document.createElement('div');
Expand Down Expand Up @@ -484,11 +622,11 @@ describe('Wizard tests', () => {
assert.equal(wizard.submission.data.textField, 'test');
const nestedWizardBreadcrumbBtn = _.get(wizard.refs, `${wizard.wizardKey}-link[4]`);
nestedWizardBreadcrumbBtn.dispatchEvent(clickEvent);

setTimeout(() => {
checkPage(4);
_.get(wizard.refs, `${wizard.wizardKey}-submit`).dispatchEvent(clickEvent);

setTimeout(() => {
assert.equal(wizard.errors.length, 2);
done();
Expand Down
2 changes: 1 addition & 1 deletion src/components/form/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ export default class FormComponent extends Component {
options = options || {};
const silentCheck = options.silentCheck || false;

if (this.subForm && !this.isNestedWizard) {
if (this.subForm) {
return this.subForm.checkValidity(this.subFormData, dirty, null, silentCheck, errors);
}

Expand Down

0 comments on commit 86ae433

Please sign in to comment.