@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.
- package/lib/components/util/avoidCalculatedValueRedraw.d.ts +28 -0
- package/lib/components/util/avoidCalculatedValueRedraw.d.ts.map +1 -0
- package/lib/components/util/avoidCalculatedValueRedraw.js +32 -0
- package/lib/components/util/deferPristineErrors.d.ts +47 -0
- package/lib/components/util/deferPristineErrors.d.ts.map +1 -0
- package/lib/components/util/deferPristineErrors.js +70 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/package.json +1 -1
- package/src/components/util/avoidCalculatedValueRedraw.spec.ts +64 -0
- package/src/components/util/avoidCalculatedValueRedraw.ts +64 -0
- package/src/components/util/deferPristineErrors.spec.ts +201 -0
- package/src/components/util/deferPristineErrors.ts +149 -0
- package/src/index.ts +5 -0
|
@@ -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;
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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
|
@@ -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,
|