@ukhomeoffice/formio-gds-template 3.0.0-delta → 3.0.0-echo

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ interface ComponentInstance {
2
+ component: {
3
+ calculateValue?: string;
4
+ calculateValueVariable?: string;
5
+ redrawOn?: string | string[];
6
+ refreshOn?: string | string[];
7
+ };
8
+ }
9
+ interface ComponentPrototype {
10
+ __gdsCalculatedValueRedrawGuardInstalled?: boolean;
11
+ checkRefreshOn: (this: ComponentInstance, changes: unknown[], flags?: unknown) => void;
12
+ }
13
+ interface ComponentConstructor {
14
+ prototype: ComponentPrototype;
15
+ }
16
+ /**
17
+ * Avoid a Form.io v5 redraw loop for calculated display fields.
18
+ *
19
+ * Older forms commonly combine `calculateValue` with `redrawOn: "data"` so a
20
+ * disabled summary field reflects the latest calculation. In Form.io v5 every
21
+ * input event now refreshes and redraws those components before immediately
22
+ * calculating them. `calculateComponentValue` already calls `setValue` when
23
+ * the calculated result changes, so the full DOM redraw is redundant and can
24
+ * make a page visibly flicker while entering a multi-part value such as a date.
25
+ */
26
+ export declare const installCalculatedValueRedrawGuard: (ComponentClass?: ComponentConstructor) => void;
27
+ export {};
28
+ //# sourceMappingURL=avoidCalculatedValueRedraw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avoidCalculatedValueRedraw.d.ts","sourceRoot":"","sources":["../../../src/components/util/avoidCalculatedValueRedraw.ts"],"names":[],"mappings":"AAEA,UAAU,iBAAiB;IACvB,SAAS,EAAE;QACP,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;QAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KACjC,CAAC;CACL;AAED,UAAU,kBAAkB;IACxB,wCAAwC,CAAC,EAAE,OAAO,CAAC;IACnD,cAAc,EAAE,CACZ,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,OAAO,EAAE,EAClB,KAAK,CAAC,EAAE,OAAO,KACd,IAAI,CAAC;CACb;AAED,UAAU,oBAAoB;IAC1B,SAAS,EAAE,kBAAkB,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,oBAC1B,oBAAoB,KACrC,IA2BF,CAAC"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installCalculatedValueRedrawGuard = void 0;
4
+ const js_1 = require("@formio/js");
5
+ /**
6
+ * Avoid a Form.io v5 redraw loop for calculated display fields.
7
+ *
8
+ * Older forms commonly combine `calculateValue` with `redrawOn: "data"` so a
9
+ * disabled summary field reflects the latest calculation. In Form.io v5 every
10
+ * input event now refreshes and redraws those components before immediately
11
+ * calculating them. `calculateComponentValue` already calls `setValue` when
12
+ * the calculated result changes, so the full DOM redraw is redundant and can
13
+ * make a page visibly flicker while entering a multi-part value such as a date.
14
+ */
15
+ const installCalculatedValueRedrawGuard = (ComponentClass = js_1.Components.components.component) => {
16
+ const prototype = ComponentClass.prototype;
17
+ if (prototype.__gdsCalculatedValueRedrawGuardInstalled) {
18
+ return;
19
+ }
20
+ prototype.__gdsCalculatedValueRedrawGuardInstalled = true;
21
+ const originalCheckRefreshOn = prototype.checkRefreshOn;
22
+ prototype.checkRefreshOn = function (changes, flags) {
23
+ const component = this.component;
24
+ const hasCalculation = Boolean(component.calculateValue || component.calculateValueVariable);
25
+ const redrawsForAllData = component.redrawOn === 'data';
26
+ if (hasCalculation && redrawsForAllData && !component.refreshOn) {
27
+ return;
28
+ }
29
+ originalCheckRefreshOn.call(this, changes, flags);
30
+ };
31
+ };
32
+ exports.installCalculatedValueRedrawGuard = installCalculatedValueRedrawGuard;
@@ -0,0 +1,47 @@
1
+ type ValidationResult = unknown;
2
+ interface ComponentInstance {
3
+ __gdsValidationAttempted?: boolean;
4
+ component?: {
5
+ clearOnHide?: boolean;
6
+ };
7
+ pristine: boolean;
8
+ root?: ValidationRoot;
9
+ setPristine?: (pristine: boolean) => void;
10
+ setCustomValidity: (messages: unknown) => ValidationResult;
11
+ visible?: boolean;
12
+ }
13
+ interface ValidationRoot {
14
+ __gdsValidationAttemptInProgress?: boolean;
15
+ submitting?: boolean;
16
+ }
17
+ interface ComponentPrototype {
18
+ __gdsDeferPristineErrorsInstalled?: boolean;
19
+ checkComponentConditions: (this: ComponentInstance, data?: unknown, flags?: unknown, row?: unknown) => boolean;
20
+ setComponentValidity: (this: ComponentInstance, errors: unknown[], dirty: boolean, silentCheck: boolean) => ValidationResult;
21
+ }
22
+ interface ComponentConstructor {
23
+ prototype: ComponentPrototype;
24
+ }
25
+ interface WizardPrototype extends ValidationRoot {
26
+ __gdsDeferPristineErrorsInstalled?: boolean;
27
+ nextPage: (...args: unknown[]) => unknown;
28
+ }
29
+ interface WizardConstructor {
30
+ prototype: WizardPrototype;
31
+ }
32
+ /**
33
+ * Restore Form.io v4's user-facing validation timing for Form.io v5.
34
+ *
35
+ * Form.io v5 validates the whole component tree after a value changes. Some
36
+ * pristine components have non-empty structural default values (objects,
37
+ * arrays, or conditionally-created container data), which makes the base
38
+ * implementation present their errors during an unrelated change.
39
+ *
40
+ * Validation still runs and remains available through component.errors. This
41
+ * patch only defers setCustomValidity, which renders the error, until the
42
+ * component has changed or Form.io explicitly validates it as dirty during
43
+ * wizard navigation or submission.
44
+ */
45
+ export declare const installDeferredPristineErrors: (ComponentClass?: ComponentConstructor, WizardClass?: WizardConstructor) => void;
46
+ export {};
47
+ //# sourceMappingURL=deferPristineErrors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferPristineErrors.d.ts","sourceRoot":"","sources":["../../../src/components/util/deferPristineErrors.ts"],"names":[],"mappings":"AAEA,KAAK,gBAAgB,GAAG,OAAO,CAAC;AAEhC,UAAU,iBAAiB;IACvB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,SAAS,CAAC,EAAE;QACR,WAAW,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,iBAAiB,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,gBAAgB,CAAC;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,cAAc;IACpB,gCAAgC,CAAC,EAAE,OAAO,CAAC;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,kBAAkB;IACxB,iCAAiC,CAAC,EAAE,OAAO,CAAC;IAC5C,wBAAwB,EAAE,CACtB,IAAI,EAAE,iBAAiB,EACvB,IAAI,CAAC,EAAE,OAAO,EACd,KAAK,CAAC,EAAE,OAAO,EACf,GAAG,CAAC,EAAE,OAAO,KACZ,OAAO,CAAC;IACb,oBAAoB,EAAE,CAClB,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,OAAO,EAAE,EACjB,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,OAAO,KACnB,gBAAgB,CAAC;CACzB;AAED,UAAU,oBAAoB;IAC1B,SAAS,EAAE,kBAAkB,CAAC;CACjC;AAED,UAAU,eAAgB,SAAQ,cAAc;IAC5C,iCAAiC,CAAC,EAAE,OAAO,CAAC;IAC5C,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;CAC7C;AAED,UAAU,iBAAiB;IACvB,SAAS,EAAE,eAAe,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,oBACtB,oBAAoB,gBACvB,iBAAiB,KAC/B,IAkFF,CAAC"}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installDeferredPristineErrors = void 0;
4
+ const js_1 = require("@formio/js");
5
+ /**
6
+ * Restore Form.io v4's user-facing validation timing for Form.io v5.
7
+ *
8
+ * Form.io v5 validates the whole component tree after a value changes. Some
9
+ * pristine components have non-empty structural default values (objects,
10
+ * arrays, or conditionally-created container data), which makes the base
11
+ * implementation present their errors during an unrelated change.
12
+ *
13
+ * Validation still runs and remains available through component.errors. This
14
+ * patch only defers setCustomValidity, which renders the error, until the
15
+ * component has changed or Form.io explicitly validates it as dirty during
16
+ * wizard navigation or submission.
17
+ */
18
+ const installDeferredPristineErrors = (ComponentClass = js_1.Components.components.component, WizardClass = js_1.Displays.getDisplay('wizard')) => {
19
+ const prototype = ComponentClass.prototype;
20
+ if (!prototype.__gdsDeferPristineErrorsInstalled) {
21
+ prototype.__gdsDeferPristineErrorsInstalled = true;
22
+ const originalSetComponentValidity = prototype.setComponentValidity;
23
+ const originalCheckComponentConditions = prototype.checkComponentConditions;
24
+ prototype.checkComponentConditions = function (data, flags, row) {
25
+ var _a;
26
+ const wasVisible = this.visible !== false;
27
+ const result = originalCheckComponentConditions.call(this, data, flags, row);
28
+ if (wasVisible &&
29
+ this.visible === false &&
30
+ ((_a = this.component) === null || _a === void 0 ? void 0 : _a.clearOnHide) !== false) {
31
+ delete this.__gdsValidationAttempted;
32
+ if (this.setPristine) {
33
+ this.setPristine(true);
34
+ }
35
+ else {
36
+ this.pristine = true;
37
+ }
38
+ this.setCustomValidity('');
39
+ }
40
+ return result;
41
+ };
42
+ prototype.setComponentValidity = function (errors, dirty, silentCheck) {
43
+ var _a, _b;
44
+ const validationAttemptInProgress = Boolean(((_a = this.root) === null || _a === void 0 ? void 0 : _a.submitting) || ((_b = this.root) === null || _b === void 0 ? void 0 : _b.__gdsValidationAttemptInProgress));
45
+ if (dirty && validationAttemptInProgress && this.visible !== false) {
46
+ this.__gdsValidationAttempted = true;
47
+ }
48
+ if (this.pristine && !this.__gdsValidationAttempted) {
49
+ return silentCheck ? [] : this.setCustomValidity('');
50
+ }
51
+ return originalSetComponentValidity.call(this, errors, dirty, silentCheck);
52
+ };
53
+ }
54
+ const wizardPrototype = WizardClass.prototype;
55
+ if (!wizardPrototype.__gdsDeferPristineErrorsInstalled) {
56
+ wizardPrototype.__gdsDeferPristineErrorsInstalled = true;
57
+ const originalNextPage = wizardPrototype.nextPage;
58
+ wizardPrototype.nextPage = function (...args) {
59
+ const previousState = this.__gdsValidationAttemptInProgress;
60
+ this.__gdsValidationAttemptInProgress = true;
61
+ try {
62
+ return originalNextPage.apply(this, args);
63
+ }
64
+ finally {
65
+ this.__gdsValidationAttemptInProgress = previousState;
66
+ }
67
+ };
68
+ }
69
+ };
70
+ exports.installDeferredPristineErrors = installDeferredPristineErrors;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,wBAIE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,wBAIE"}
package/lib/index.js CHANGED
@@ -4,7 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const components_1 = __importDefault(require("./components"));
7
+ const avoidCalculatedValueRedraw_1 = require("./components/util/avoidCalculatedValueRedraw");
8
+ const deferPristineErrors_1 = require("./components/util/deferPristineErrors");
7
9
  const templates_1 = __importDefault(require("./templates"));
10
+ (0, avoidCalculatedValueRedraw_1.installCalculatedValueRedrawGuard)();
11
+ (0, deferPristineErrors_1.installDeferredPristineErrors)();
8
12
  exports.default = {
9
13
  framework: 'gds',
10
14
  templates: templates_1.default,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ukhomeoffice/formio-gds-template",
3
- "version": "3.0.0-delta",
3
+ "version": "3.0.0-echo",
4
4
  "description": "Gov UK design system formio templates — upgraded to @formio/js@5.x",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,64 @@
1
+ import assert from 'assert';
2
+ import sinon from 'sinon';
3
+ import { installCalculatedValueRedrawGuard } from './avoidCalculatedValueRedraw';
4
+
5
+ class TestComponent {
6
+ public component: {
7
+ calculateValue?: string;
8
+ calculateValueVariable?: string;
9
+ redrawOn?: string | string[];
10
+ refreshOn?: string | string[];
11
+ } = {};
12
+
13
+ public refreshed = sinon.stub();
14
+
15
+ public checkRefreshOn(changes: unknown[], flags?: unknown): void {
16
+ this.refreshed(changes, flags);
17
+ }
18
+ }
19
+
20
+ describe('installCalculatedValueRedrawGuard', () => {
21
+ before(() => {
22
+ installCalculatedValueRedrawGuard(TestComponent);
23
+ });
24
+
25
+ it('skips the redundant data redraw for a calculated field', () => {
26
+ const component = new TestComponent();
27
+ component.component.calculateValue = 'value = row.number';
28
+ component.component.redrawOn = 'data';
29
+
30
+ component.checkRefreshOn([{ value: 'new value' }], {});
31
+
32
+ assert.strictEqual(component.refreshed.notCalled, true);
33
+ });
34
+
35
+ it('preserves redrawOn data for a field without a calculation', () => {
36
+ const component = new TestComponent();
37
+ component.component.redrawOn = 'data';
38
+ const changes = [{ value: 'new value' }];
39
+ const flags = { changed: true };
40
+
41
+ component.checkRefreshOn(changes, flags);
42
+
43
+ assert.strictEqual(component.refreshed.calledOnceWithExactly(changes, flags), true);
44
+ });
45
+
46
+ it('preserves an explicit refreshOn dependency', () => {
47
+ const component = new TestComponent();
48
+ component.component.calculateValue = 'value = row.number';
49
+ component.component.redrawOn = 'data';
50
+ component.component.refreshOn = 'number';
51
+
52
+ component.checkRefreshOn([], {});
53
+
54
+ assert.strictEqual(component.refreshed.calledOnce, true);
55
+ });
56
+
57
+ it('installs only once', () => {
58
+ const installedMethod = TestComponent.prototype.checkRefreshOn;
59
+
60
+ installCalculatedValueRedrawGuard(TestComponent);
61
+
62
+ assert.strictEqual(TestComponent.prototype.checkRefreshOn, installedMethod);
63
+ });
64
+ });
@@ -0,0 +1,64 @@
1
+ import { Components } from '@formio/js';
2
+
3
+ interface ComponentInstance {
4
+ component: {
5
+ calculateValue?: string;
6
+ calculateValueVariable?: string;
7
+ redrawOn?: string | string[];
8
+ refreshOn?: string | string[];
9
+ };
10
+ }
11
+
12
+ interface ComponentPrototype {
13
+ __gdsCalculatedValueRedrawGuardInstalled?: boolean;
14
+ checkRefreshOn: (
15
+ this: ComponentInstance,
16
+ changes: unknown[],
17
+ flags?: unknown,
18
+ ) => void;
19
+ }
20
+
21
+ interface ComponentConstructor {
22
+ prototype: ComponentPrototype;
23
+ }
24
+
25
+ /**
26
+ * Avoid a Form.io v5 redraw loop for calculated display fields.
27
+ *
28
+ * Older forms commonly combine `calculateValue` with `redrawOn: "data"` so a
29
+ * disabled summary field reflects the latest calculation. In Form.io v5 every
30
+ * input event now refreshes and redraws those components before immediately
31
+ * calculating them. `calculateComponentValue` already calls `setValue` when
32
+ * the calculated result changes, so the full DOM redraw is redundant and can
33
+ * make a page visibly flicker while entering a multi-part value such as a date.
34
+ */
35
+ export const installCalculatedValueRedrawGuard = (
36
+ ComponentClass: ComponentConstructor = Components.components.component,
37
+ ): void => {
38
+ const prototype = ComponentClass.prototype;
39
+
40
+ if (prototype.__gdsCalculatedValueRedrawGuardInstalled) {
41
+ return;
42
+ }
43
+
44
+ prototype.__gdsCalculatedValueRedrawGuardInstalled = true;
45
+ const originalCheckRefreshOn = prototype.checkRefreshOn;
46
+
47
+ prototype.checkRefreshOn = function (
48
+ this: ComponentInstance,
49
+ changes: unknown[],
50
+ flags?: unknown,
51
+ ): void {
52
+ const component = this.component;
53
+ const hasCalculation = Boolean(
54
+ component.calculateValue || component.calculateValueVariable,
55
+ );
56
+ const redrawsForAllData = component.redrawOn === 'data';
57
+
58
+ if (hasCalculation && redrawsForAllData && !component.refreshOn) {
59
+ return;
60
+ }
61
+
62
+ originalCheckRefreshOn.call(this, changes, flags);
63
+ };
64
+ };
@@ -0,0 +1,201 @@
1
+ import assert from 'assert';
2
+ import sinon from 'sinon';
3
+ import { installDeferredPristineErrors } from './deferPristineErrors';
4
+
5
+ class TestComponent {
6
+ public __gdsValidationAttempted?: boolean;
7
+
8
+ public component: { clearOnHide?: boolean } = {};
9
+
10
+ public pristine = true;
11
+
12
+ public root?: TestWizard;
13
+
14
+ public setCustomValidity = sinon.stub().returns([]);
15
+
16
+ public visible = true;
17
+
18
+ public nextVisible?: boolean;
19
+
20
+ public checkComponentConditions(): boolean {
21
+ if (this.nextVisible !== undefined) {
22
+ this.visible = this.nextVisible;
23
+ }
24
+
25
+ return this.visible;
26
+ }
27
+
28
+ public setPristine(pristine: boolean): void {
29
+ this.pristine = pristine;
30
+ }
31
+
32
+ public setComponentValidity(
33
+ errors: unknown[],
34
+ dirty: boolean,
35
+ silentCheck: boolean,
36
+ ): unknown {
37
+ return { errors, dirty, silentCheck };
38
+ }
39
+ }
40
+
41
+ class TestWizard {
42
+ public __gdsValidationAttemptInProgress?: boolean;
43
+
44
+ public component?: TestComponent;
45
+
46
+ public submitting = false;
47
+
48
+ public nextPage(): unknown {
49
+ return this.component?.setComponentValidity(
50
+ [{ level: 'error', message: 'Required' }],
51
+ true,
52
+ false,
53
+ );
54
+ }
55
+ }
56
+
57
+ describe('installDeferredPristineErrors', () => {
58
+ before(() => {
59
+ installDeferredPristineErrors(TestComponent, TestWizard);
60
+ });
61
+
62
+ it('does not present errors for a pristine component during an unrelated change', () => {
63
+ const component = new TestComponent();
64
+
65
+ const result = component.setComponentValidity(
66
+ [{ level: 'error', message: 'Required' }],
67
+ false,
68
+ false,
69
+ );
70
+
71
+ assert.deepStrictEqual(result, []);
72
+ assert.strictEqual(component.setCustomValidity.calledOnceWithExactly(''), true);
73
+ });
74
+
75
+ it('delegates dirty validation used by Next', () => {
76
+ const component = new TestComponent();
77
+ const wizard = new TestWizard();
78
+ component.root = wizard;
79
+ wizard.component = component;
80
+ const errors = [{ level: 'error', message: 'Required' }];
81
+
82
+ const result = wizard.nextPage();
83
+
84
+ assert.deepStrictEqual(result, { errors, dirty: true, silentCheck: false });
85
+ assert.strictEqual(component.setCustomValidity.notCalled, true);
86
+ assert.strictEqual(component.__gdsValidationAttempted, true);
87
+ });
88
+
89
+ it('delegates dirty validation while a form is submitting', () => {
90
+ const component = new TestComponent();
91
+ const wizard = new TestWizard();
92
+ wizard.submitting = true;
93
+ component.root = wizard;
94
+ const errors = [{ level: 'error', message: 'Required' }];
95
+
96
+ const result = component.setComponentValidity(errors, true, false);
97
+
98
+ assert.deepStrictEqual(result, { errors, dirty: true, silentCheck: false });
99
+ assert.strictEqual(component.__gdsValidationAttempted, true);
100
+ });
101
+
102
+ it('does not apply an earlier submission attempt to a newly revealed component', () => {
103
+ const component = new TestComponent();
104
+ component.root = new TestWizard();
105
+
106
+ const result = component.setComponentValidity(
107
+ [{ level: 'error', message: 'Required' }],
108
+ true,
109
+ false,
110
+ );
111
+
112
+ assert.deepStrictEqual(result, []);
113
+ assert.strictEqual(component.setCustomValidity.calledOnceWithExactly(''), true);
114
+ assert.strictEqual(component.__gdsValidationAttempted, undefined);
115
+ });
116
+
117
+ it('does not mark a conditionally hidden field during Next before it is revealed', () => {
118
+ const component = new TestComponent();
119
+ const wizard = new TestWizard();
120
+ component.root = wizard;
121
+ component.visible = false;
122
+ wizard.component = component;
123
+
124
+ const hiddenResult = wizard.nextPage();
125
+
126
+ assert.deepStrictEqual(hiddenResult, []);
127
+ assert.strictEqual(component.__gdsValidationAttempted, undefined);
128
+
129
+ component.visible = true;
130
+ component.setCustomValidity.resetHistory();
131
+
132
+ const revealedResult = component.setComponentValidity(
133
+ [{ level: 'error', message: 'Mode of transport is required' }],
134
+ true,
135
+ false,
136
+ );
137
+
138
+ assert.deepStrictEqual(revealedResult, []);
139
+ assert.strictEqual(component.setCustomValidity.calledOnceWithExactly(''), true);
140
+ assert.strictEqual(component.__gdsValidationAttempted, undefined);
141
+ });
142
+
143
+ it('resets a clear-on-hide field when it is hidden and revealed again', () => {
144
+ const component = new TestComponent();
145
+ component.pristine = false;
146
+ component.__gdsValidationAttempted = true;
147
+ component.nextVisible = false;
148
+
149
+ const hiddenResult = component.checkComponentConditions();
150
+
151
+ assert.strictEqual(hiddenResult, false);
152
+ assert.strictEqual(component.pristine, true);
153
+ assert.strictEqual(component.__gdsValidationAttempted, undefined);
154
+ assert.strictEqual(component.setCustomValidity.calledOnceWithExactly(''), true);
155
+
156
+ component.visible = true;
157
+ component.setCustomValidity.resetHistory();
158
+
159
+ const revealedResult = component.setComponentValidity(
160
+ [{ level: 'error', message: 'Required' }],
161
+ false,
162
+ false,
163
+ );
164
+
165
+ assert.deepStrictEqual(revealedResult, []);
166
+ assert.strictEqual(component.setCustomValidity.calledOnceWithExactly(''), true);
167
+ });
168
+
169
+ it('preserves validation state when clearOnHide is disabled', () => {
170
+ const component = new TestComponent();
171
+ component.component.clearOnHide = false;
172
+ component.pristine = false;
173
+ component.__gdsValidationAttempted = true;
174
+ component.nextVisible = false;
175
+
176
+ component.checkComponentConditions();
177
+
178
+ assert.strictEqual(component.pristine, false);
179
+ assert.strictEqual(component.__gdsValidationAttempted, true);
180
+ assert.strictEqual(component.setCustomValidity.notCalled, true);
181
+ });
182
+
183
+ it('delegates validation after a component has changed', () => {
184
+ const component = new TestComponent();
185
+ component.pristine = false;
186
+ const errors = [{ level: 'error', message: 'Required' }];
187
+
188
+ const result = component.setComponentValidity(errors, false, false);
189
+
190
+ assert.deepStrictEqual(result, { errors, dirty: false, silentCheck: false });
191
+ assert.strictEqual(component.setCustomValidity.notCalled, true);
192
+ });
193
+
194
+ it('installs only once', () => {
195
+ const installedMethod = TestComponent.prototype.setComponentValidity;
196
+
197
+ installDeferredPristineErrors(TestComponent, TestWizard);
198
+
199
+ assert.strictEqual(TestComponent.prototype.setComponentValidity, installedMethod);
200
+ });
201
+ });
@@ -0,0 +1,149 @@
1
+ import { Components, Displays } from '@formio/js';
2
+
3
+ type ValidationResult = unknown;
4
+
5
+ interface ComponentInstance {
6
+ __gdsValidationAttempted?: boolean;
7
+ component?: {
8
+ clearOnHide?: boolean;
9
+ };
10
+ pristine: boolean;
11
+ root?: ValidationRoot;
12
+ setPristine?: (pristine: boolean) => void;
13
+ setCustomValidity: (messages: unknown) => ValidationResult;
14
+ visible?: boolean;
15
+ }
16
+
17
+ interface ValidationRoot {
18
+ __gdsValidationAttemptInProgress?: boolean;
19
+ submitting?: boolean;
20
+ }
21
+
22
+ interface ComponentPrototype {
23
+ __gdsDeferPristineErrorsInstalled?: boolean;
24
+ checkComponentConditions: (
25
+ this: ComponentInstance,
26
+ data?: unknown,
27
+ flags?: unknown,
28
+ row?: unknown,
29
+ ) => boolean;
30
+ setComponentValidity: (
31
+ this: ComponentInstance,
32
+ errors: unknown[],
33
+ dirty: boolean,
34
+ silentCheck: boolean,
35
+ ) => ValidationResult;
36
+ }
37
+
38
+ interface ComponentConstructor {
39
+ prototype: ComponentPrototype;
40
+ }
41
+
42
+ interface WizardPrototype extends ValidationRoot {
43
+ __gdsDeferPristineErrorsInstalled?: boolean;
44
+ nextPage: (...args: unknown[]) => unknown;
45
+ }
46
+
47
+ interface WizardConstructor {
48
+ prototype: WizardPrototype;
49
+ }
50
+
51
+ /**
52
+ * Restore Form.io v4's user-facing validation timing for Form.io v5.
53
+ *
54
+ * Form.io v5 validates the whole component tree after a value changes. Some
55
+ * pristine components have non-empty structural default values (objects,
56
+ * arrays, or conditionally-created container data), which makes the base
57
+ * implementation present their errors during an unrelated change.
58
+ *
59
+ * Validation still runs and remains available through component.errors. This
60
+ * patch only defers setCustomValidity, which renders the error, until the
61
+ * component has changed or Form.io explicitly validates it as dirty during
62
+ * wizard navigation or submission.
63
+ */
64
+ export const installDeferredPristineErrors = (
65
+ ComponentClass: ComponentConstructor = Components.components.component,
66
+ WizardClass: WizardConstructor = Displays.getDisplay('wizard'),
67
+ ): void => {
68
+ const prototype = ComponentClass.prototype;
69
+
70
+ if (!prototype.__gdsDeferPristineErrorsInstalled) {
71
+ prototype.__gdsDeferPristineErrorsInstalled = true;
72
+
73
+ const originalSetComponentValidity = prototype.setComponentValidity;
74
+ const originalCheckComponentConditions = prototype.checkComponentConditions;
75
+
76
+ prototype.checkComponentConditions = function (
77
+ this: ComponentInstance,
78
+ data?: unknown,
79
+ flags?: unknown,
80
+ row?: unknown,
81
+ ): boolean {
82
+ const wasVisible = this.visible !== false;
83
+ const result = originalCheckComponentConditions.call(this, data, flags, row);
84
+
85
+ if (
86
+ wasVisible &&
87
+ this.visible === false &&
88
+ this.component?.clearOnHide !== false
89
+ ) {
90
+ delete this.__gdsValidationAttempted;
91
+
92
+ if (this.setPristine) {
93
+ this.setPristine(true);
94
+ }
95
+ else {
96
+ this.pristine = true;
97
+ }
98
+
99
+ this.setCustomValidity('');
100
+ }
101
+
102
+ return result;
103
+ };
104
+
105
+ prototype.setComponentValidity = function (
106
+ this: ComponentInstance,
107
+ errors: unknown[],
108
+ dirty: boolean,
109
+ silentCheck: boolean,
110
+ ): ValidationResult {
111
+ const validationAttemptInProgress = Boolean(
112
+ this.root?.submitting || this.root?.__gdsValidationAttemptInProgress,
113
+ );
114
+
115
+ if (dirty && validationAttemptInProgress && this.visible !== false) {
116
+ this.__gdsValidationAttempted = true;
117
+ }
118
+
119
+ if (this.pristine && !this.__gdsValidationAttempted) {
120
+ return silentCheck ? [] : this.setCustomValidity('');
121
+ }
122
+
123
+ return originalSetComponentValidity.call(this, errors, dirty, silentCheck);
124
+ };
125
+ }
126
+
127
+ const wizardPrototype = WizardClass.prototype;
128
+
129
+ if (!wizardPrototype.__gdsDeferPristineErrorsInstalled) {
130
+ wizardPrototype.__gdsDeferPristineErrorsInstalled = true;
131
+
132
+ const originalNextPage = wizardPrototype.nextPage;
133
+
134
+ wizardPrototype.nextPage = function (
135
+ this: WizardPrototype,
136
+ ...args: unknown[]
137
+ ): unknown {
138
+ const previousState = this.__gdsValidationAttemptInProgress;
139
+ this.__gdsValidationAttemptInProgress = true;
140
+
141
+ try {
142
+ return originalNextPage.apply(this, args);
143
+ }
144
+ finally {
145
+ this.__gdsValidationAttemptInProgress = previousState;
146
+ }
147
+ };
148
+ }
149
+ };
package/src/index.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import components from './components';
2
+ import { installCalculatedValueRedrawGuard } from './components/util/avoidCalculatedValueRedraw';
3
+ import { installDeferredPristineErrors } from './components/util/deferPristineErrors';
2
4
  import templates from './templates';
3
5
 
6
+ installCalculatedValueRedrawGuard();
7
+ installDeferredPristineErrors();
8
+
4
9
  export default {
5
10
  framework: 'gds',
6
11
  templates,