Skip to content

Commit

Permalink
Introduce validation mode
Browse files Browse the repository at this point in the history
Adds validationMode to the core state supporting
 * Validation & showing the validation result (default)
 * Validation & hiding the validation result
 * No validation

'Validation & showing the validation result' is the default mode and
represents the previous mode. 'Validation & hiding the validation
result' will still keep the errors in the core state up to date, however
they are not given to the respective renderers by default. 'No
validation' will short-circuit the validation, resulting in an always
empty errors array in the core state.

The mode can be set as part of the init action and can later be updated
with the separate setValidationMode action. The action is exposed as a
separate prop in React and as a separate method in the Angular service.

The new modes can be used for various use cases, e.g.
 * save performance by never validating on client side
 * show validation errors separately from the form
 * only show validation errors on submit or after first edit
  • Loading branch information
sdirix committed Jul 16, 2020
1 parent 4c3f710 commit b53bcbd
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 48 deletions.
16 changes: 12 additions & 4 deletions packages/angular/src/jsonforms.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
SetConfigAction,
UISchemaActions,
UISchemaElement,
uischemaRegistryReducer
uischemaRegistryReducer,
ValidationMode,
Actions
} from '@jsonforms/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { JsonFormsBaseRenderer } from './base.renderer';
Expand Down Expand Up @@ -60,21 +62,27 @@ export class JsonFormsAngularService {
this.updateSubject();
}

updateLocale<T extends LocaleActions>(localeAction: T): T {
updateValidationMode(validationMode: ValidationMode): void {
const coreState = coreReducer(this._state.core, Actions.setValidationMode(validationMode));
this._state.core = coreState;
this.updateSubject();
}

updateLocale<T extends LocaleActions>(localeAction: T): T {
const localeState = i18nReducer(this._state.i18n, localeAction);
this._state.i18n = localeState;
this.updateSubject();
return localeAction;
}

updateCore<T extends CoreActions>(coreAction: T): T {
updateCore<T extends CoreActions>(coreAction: T): T {
const coreState = coreReducer(this._state.core, coreAction);
this._state.core = coreState;
this.updateSubject();
return coreAction;
}

updateUiSchema<T extends UISchemaActions>(uischemaAction: T): T {
updateUiSchema<T extends UISchemaActions>(uischemaAction: T): T {
const uischemaState = uischemaRegistryReducer(this._state.uischemas, uischemaAction);
this._state.uischemas = uischemaState;
this.updateSubject();
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { generateDefaultUISchema, generateJsonSchema } from '../generators';
import { RankedTester } from '../testers';
import RefParser from 'json-schema-ref-parser';
import { UISchemaTester } from '../reducers/uischemas';
import { ValidationMode } from '../reducers/core';

export const INIT: 'jsonforms/INIT' = 'jsonforms/INIT';
export const SET_AJV: 'jsonforms/SET_AJV' = 'jsonforms/SET_AJV';
Expand All @@ -46,6 +47,8 @@ export const ADD_UI_SCHEMA: 'jsonforms/ADD_UI_SCHEMA' = `jsonforms/ADD_UI_SCHEMA
export const REMOVE_UI_SCHEMA: 'jsonforms/REMOVE_UI_SCHEMA' = `jsonforms/REMOVE_UI_SCHEMA`;
export const SET_SCHEMA: 'jsonforms/SET_SCHEMA' = `jsonforms/SET_SCHEMA`;
export const SET_UISCHEMA: 'jsonforms/SET_UISCHEMA' = `jsonforms/SET_UISCHEMA`;
export const SET_VALIDATION_MODE: 'jsonforms/SET_VALIDATION_MODE' =
'jsonforms/SET_VALIDATION_MODE';

export const SET_LOCALE: 'jsonforms/SET_LOCALE' = `jsonforms/SET_LOCALE`;
export const SET_LOCALIZED_SCHEMAS: 'jsonforms/SET_LOCALIZED_SCHEMAS' =
Expand All @@ -62,7 +65,8 @@ export type CoreActions =
| UpdateErrorsAction
| SetAjvAction
| SetSchemaAction
| SetUISchemaAction;
| SetUISchemaAction
| SetValidationModeAction;

export interface UpdateAction {
type: 'jsonforms/UPDATE';
Expand All @@ -86,6 +90,12 @@ export interface InitAction {
export interface InitActionOptions {
ajv?: AJV.Ajv;
refParserOptions?: RefParser.Options;
validationMode?: ValidationMode;
}

export interface SetValidationModeAction {
type: 'jsonforms/SET_VALIDATION_MODE'
validationMode: ValidationMode
}

export const init = (
Expand Down Expand Up @@ -206,6 +216,11 @@ export const setConfig = (config: any): SetConfigAction => ({
config
});

export const setValidationMode = (validationMode: ValidationMode): SetValidationModeAction => ({
type: SET_VALIDATION_MODE,
validationMode
})

export type UISchemaActions = AddUISchemaAction | RemoveUISchemaAction;

export interface AddUISchemaAction {
Expand Down
79 changes: 68 additions & 11 deletions packages/core/src/reducers/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import {
SET_UISCHEMA,
UPDATE_DATA,
UPDATE_ERRORS,
CoreActions
CoreActions,
SET_VALIDATION_MODE
} from '../actions';
import { createAjv } from '../util/validator';
import { JsonSchema, UISchemaElement } from '..';
Expand All @@ -53,15 +54,21 @@ const validate = (validator: ValidateFunction, data: any): ErrorObject[] => {
return validator.errors;
};

export const sanitizeErrors = (validator: ValidateFunction, data: any) =>
validate(validator, data).map(error => {
export const sanitizeErrors = (validator: ValidateFunction, data: any) => {
if (validator === alwaysValid) {
return [];
}
return validate(validator, data).map(error => {
error.dataPath = error.dataPath.replace(/\//g, '.').substr(1);

return error;
});
};

const alwaysValid: ValidateFunction = () => true;

export type ValidationMode = 'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation';

export interface JsonFormsCore {
data: any;
schema: JsonSchema;
Expand All @@ -70,6 +77,7 @@ export interface JsonFormsCore {
validator?: ValidateFunction;
ajv?: Ajv;
refParserOptions?: RefParser.Options;
validationMode?: ValidationMode;
}

const initState: JsonFormsCore = {
Expand All @@ -79,7 +87,8 @@ const initState: JsonFormsCore = {
errors: [],
validator: alwaysValid,
ajv: undefined,
refParserOptions: undefined
refParserOptions: undefined,
validationMode: 'ValidateAndShow'
};

const getOrCreateAjv = (state: JsonFormsCore, action?: InitAction): Ajv => {
Expand Down Expand Up @@ -127,17 +136,36 @@ const hasAjvOption = (option: any): option is InitActionOptions => {
return false;
};

const getValidationMode = (
state: JsonFormsCore,
action?: InitAction
): ValidationMode => {
if (action && hasValidationModeOption(action.options)) {
return action.options.validationMode;
}
return state.validationMode;
};

const hasValidationModeOption = (option: any): option is InitActionOptions => {
if (option) {
return option.validationMode !== undefined;
}
return false;
};

export const coreReducer = (
state: JsonFormsCore = initState,
action: CoreActions
): JsonFormsCore => {
switch (action.type) {
case INIT: {
const thisAjv = getOrCreateAjv(state, action);
const v = thisAjv.compile(action.schema);
const e = sanitizeErrors(v, action.data);
const o = getRefParserOptions(state, action);

const validationMode = getValidationMode(state, action);
const v = validationMode === 'NoValidation' ? alwaysValid : thisAjv.compile(action.schema);
const e = sanitizeErrors(v, action.data);

return {
...state,
data: action.data,
Expand All @@ -146,12 +174,13 @@ export const coreReducer = (
errors: e,
validator: v,
ajv: thisAjv,
refParserOptions: o
refParserOptions: o,
validationMode
};
}
case SET_AJV: {
const currentAjv = action.ajv;
const validator = currentAjv.compile(state.schema);
const validator = state.validationMode === 'NoValidation' ? alwaysValid : currentAjv.compile(state.schema);
const errors = sanitizeErrors(validator, state.data);
return {
...state,
Expand All @@ -160,8 +189,8 @@ export const coreReducer = (
};
}
case SET_SCHEMA: {
const v =
action.schema && state.ajv
const needsNewValidator = action.schema && state.ajv && state.validationMode !== 'NoValidation';
const v = needsNewValidator
? state.ajv.compile(action.schema)
: state.validator;
return {
Expand Down Expand Up @@ -209,6 +238,34 @@ export const coreReducer = (
errors: action.errors
};
}
case SET_VALIDATION_MODE: {
if (state.validationMode === action.validationMode) {
return state;
}
if (action.validationMode === 'NoValidation') {
const errors = sanitizeErrors(alwaysValid, state.data);
return {
...state,
validator: alwaysValid,
errors,
validationMode: action.validationMode
};
}
if (state.validationMode === 'NoValidation') {
const validator = state.ajv.compile(state.schema);
const errors = sanitizeErrors(validator, state.data);
return {
...state,
validator,
errors,
validationMode: action.validationMode
};
}
return {
...state,
validationMode: action.validationMode
};
}
default:
return state;
}
Expand Down Expand Up @@ -242,7 +299,7 @@ const getErrorsAt = (
schema: JsonSchema,
matchPath: (path: string) => boolean
) => (state: JsonFormsCore): ErrorObject[] =>
errorsAt(instancePath, schema, matchPath)(state.errors);
errorsAt(instancePath, schema, matchPath)(state.validationMode === 'ValidateAndHide' ? [] : state.errors);

export const errorAt = (instancePath: string, schema: JsonSchema) =>
getErrorsAt(instancePath, schema, path => path === instancePath);
Expand Down
54 changes: 28 additions & 26 deletions packages/core/src/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { ControlElement, UISchemaElement } from '../models/uischema';
import {
JsonFormsCore,
Expand All @@ -8,7 +32,8 @@ import {
extractRefParserOptions,
extractSchema,
extractUiSchema,
subErrorsAt
subErrorsAt,
ValidationMode
} from './core';
import {
JsonFormsDefaultDataRegistryEntry,
Expand All @@ -34,30 +59,7 @@ import { Generate } from '../generators';
import { JsonFormsCellRendererRegistryEntry } from './cells';
import { JsonSchema } from '../models/jsonSchema';
import RefParser from 'json-schema-ref-parser';
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

import { cellReducer } from './cells';
import { configReducer } from './config';
import get from 'lodash/get';
Expand All @@ -72,7 +74,7 @@ export {
uischemaRegistryReducer,
findMatchingUISchema
};
export { JsonFormsCore };
export { JsonFormsCore, ValidationMode };

export const jsonformsReducer = (
additionalReducers = {}
Expand Down

0 comments on commit b53bcbd

Please sign in to comment.