From 46c214600352fb2680c5276f378996e47ade20b9 Mon Sep 17 00:00:00 2001 From: Lukas Boll Date: Mon, 18 Dec 2023 12:27:19 +0100 Subject: [PATCH 1/2] feat: Add Middleware Support for Angular and Vue This commit implements middleware support for both Angular and Vue, addressing the discussion in issue #2174 --- .../angular-material/test/middleware.spec.ts | 89 +++++++++++++++++++ .../src/library/jsonforms-root.component.ts | 34 ++++--- .../angular/src/library/jsonforms.service.ts | 38 +++++--- packages/core/src/reducers/index.ts | 1 + packages/core/src/reducers/middleware.ts | 12 +++ packages/react/src/JsonForms.tsx | 2 +- packages/react/src/JsonFormsContext.tsx | 13 +-- .../react/test/renderers/JsonForms.test.tsx | 2 +- packages/vue/src/components/JsonForms.vue | 22 +++-- 9 files changed, 170 insertions(+), 43 deletions(-) create mode 100644 packages/angular-material/test/middleware.spec.ts create mode 100644 packages/core/src/reducers/middleware.ts diff --git a/packages/angular-material/test/middleware.spec.ts b/packages/angular-material/test/middleware.spec.ts new file mode 100644 index 0000000000..435228b0e4 --- /dev/null +++ b/packages/angular-material/test/middleware.spec.ts @@ -0,0 +1,89 @@ +/* + The MIT License + + Copyright (c) 2023-2023 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 { ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NumberControlRenderer } from '../src'; +import { JsonFormsAngularService, JsonFormsControl } from '@jsonforms/angular'; +import { + baseSetup, + getJsonFormsService, + prepareComponent, +} from '@jsonforms/angular-test'; + +const imports = [ + MatFormFieldModule, + MatInputModule, + NoopAnimationsModule, + ReactiveFormsModule, +]; +const providers = [JsonFormsAngularService]; +const componentUT: any = NumberControlRenderer; +const testConfig = { imports, providers, componentUT }; + +describe('middleware tests', () => { + let component: JsonFormsControl; + const startingValues = { + core: { + data: 'startValue', + schema: { type: 'string' }, + uischema: { + type: 'control', + }, + }, + }; + + baseSetup(testConfig); + + beforeEach(() => { + const preparedComponents = prepareComponent(testConfig, 'input'); + component = preparedComponents.component; + }); + + it('init using middleware', () => { + const jsonFormsService: JsonFormsAngularService = + getJsonFormsService(component); + const spyMiddleware = jasmine.createSpy('spy1').and.returnValue({ + data: 4, + schema: { type: 'number' }, + uischema: { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + }, + ], + }, + }); + jsonFormsService.init(startingValues, spyMiddleware); + expect(spyMiddleware).toHaveBeenCalled(); + const core = jsonFormsService.getState().jsonforms.core; + expect(core?.data).toBe(4); + expect(core?.schema.type).toBe('number'); + expect(core?.uischema.type).toBe('VerticalLayout'); + }); +}); diff --git a/packages/angular/src/library/jsonforms-root.component.ts b/packages/angular/src/library/jsonforms-root.component.ts index 2172a35738..89c2ed133c 100644 --- a/packages/angular/src/library/jsonforms-root.component.ts +++ b/packages/angular/src/library/jsonforms-root.component.ts @@ -37,9 +37,11 @@ import { JsonFormsI18nState, JsonFormsRendererRegistryEntry, JsonSchema, + Middleware, UISchemaElement, UISchemaTester, ValidationMode, + defaultMiddleware, } from '@jsonforms/core'; import Ajv, { ErrorObject } from 'ajv'; import { JsonFormsAngularService, USE_STATE_VALUE } from './jsonforms.service'; @@ -64,6 +66,7 @@ export class JsonForms implements DoCheck, OnChanges, OnInit { @Input() config: any; @Input() i18n: JsonFormsI18nState; @Input() additionalErrors: ErrorObject[]; + @Input() middleware: Middleware = defaultMiddleware; @Output() errors = new EventEmitter(); private previousData: any; @@ -75,21 +78,24 @@ export class JsonForms implements DoCheck, OnChanges, OnInit { constructor(private jsonformsService: JsonFormsAngularService) {} ngOnInit(): void { - this.jsonformsService.init({ - core: { - data: this.data, - uischema: this.uischema, - schema: this.schema, - ajv: this.ajv, - validationMode: this.validationMode, - additionalErrors: this.additionalErrors, + this.jsonformsService.init( + { + core: { + data: this.data, + uischema: this.uischema, + schema: this.schema, + ajv: this.ajv, + validationMode: this.validationMode, + additionalErrors: this.additionalErrors, + }, + uischemas: this.uischemas, + i18n: this.i18n, + renderers: this.renderers, + config: this.config, + readonly: this.readonly, }, - uischemas: this.uischemas, - i18n: this.i18n, - renderers: this.renderers, - config: this.config, - readonly: this.readonly, - }); + this.middleware + ); this.jsonformsService.$state.subscribe((state) => { const data = state?.jsonforms?.core?.data; const errors = state?.jsonforms?.core?.errors; diff --git a/packages/angular/src/library/jsonforms.service.ts b/packages/angular/src/library/jsonforms.service.ts index 36c3a26243..4b299c65ab 100644 --- a/packages/angular/src/library/jsonforms.service.ts +++ b/packages/angular/src/library/jsonforms.service.ts @@ -44,6 +44,8 @@ import { UISchemaTester, ValidationMode, updateI18n, + Middleware, + defaultMiddleware, } from '@jsonforms/core'; import { BehaviorSubject, Observable } from 'rxjs'; import type { JsonFormsBaseRenderer } from './base.renderer'; @@ -56,6 +58,7 @@ export const USE_STATE_VALUE = Symbol('Marker to use state value'); export class JsonFormsAngularService { private _state: JsonFormsSubStates; private state: BehaviorSubject; + private middleware: Middleware; init( initialState: JsonFormsSubStates = { @@ -66,8 +69,10 @@ export class JsonFormsAngularService { validationMode: 'ValidateAndShow', additionalErrors: undefined, }, - } + }, + middleware: Middleware = defaultMiddleware ) { + this.middleware = middleware; this._state = initialState; this._state.config = configReducer( undefined, @@ -143,9 +148,10 @@ export class JsonFormsAngularService { } updateValidationMode(validationMode: ValidationMode): void { - const coreState = coreReducer( + const coreState = this.middleware( this._state.core, - Actions.setValidationMode(validationMode) + Actions.setValidationMode(validationMode), + coreReducer ); this._state.core = coreState; this.updateSubject(); @@ -161,7 +167,11 @@ export class JsonFormsAngularService { } updateCore(coreAction: T): T { - const coreState = coreReducer(this._state.core, coreAction); + const coreState = this.middleware( + this._state.core, + coreAction, + coreReducer + ); if (coreState !== this._state.core) { this._state.core = coreState; this.updateSubject(); @@ -199,13 +209,14 @@ export class JsonFormsAngularService { setUiSchema(uischema: UISchemaElement | undefined): void { const newUiSchema = uischema ?? generateDefaultUISchema(this._state.core.schema); - const coreState = coreReducer( + const coreState = this.middleware( this._state.core, Actions.updateCore( this._state.core.data, this._state.core.schema, newUiSchema - ) + ), + coreReducer ); if (coreState !== this._state.core) { this._state.core = coreState; @@ -214,13 +225,14 @@ export class JsonFormsAngularService { } setSchema(schema: JsonSchema | undefined): void { - const coreState = coreReducer( + const coreState = this.middleware( this._state.core, Actions.updateCore( this._state.core.data, schema ?? generateJsonSchema(this._state.core.data), this._state.core.uischema - ) + ), + coreReducer ); if (coreState !== this._state.core) { this._state.core = coreState; @@ -229,13 +241,14 @@ export class JsonFormsAngularService { } setData(data: any): void { - const coreState = coreReducer( + const coreState = this.middleware( this._state.core, Actions.updateCore( data, this._state.core.schema, this._state.core.uischema - ) + ), + coreReducer ); if (coreState !== this._state.core) { this._state.core = coreState; @@ -257,6 +270,11 @@ export class JsonFormsAngularService { this.updateSubject(); } + setMiddleware(middleware: Middleware): void { + this._state.middleware = middleware; + this.updateSubject(); + } + getState(): JsonFormsState { return cloneDeep({ jsonforms: this._state }); } diff --git a/packages/core/src/reducers/index.ts b/packages/core/src/reducers/index.ts index cae85eeb0e..3748630ab0 100644 --- a/packages/core/src/reducers/index.ts +++ b/packages/core/src/reducers/index.ts @@ -32,3 +32,4 @@ export * from './reducers'; export * from './renderers'; export * from './selectors'; export * from './uischemas'; +export * from './middleware'; diff --git a/packages/core/src/reducers/middleware.ts b/packages/core/src/reducers/middleware.ts new file mode 100644 index 0000000000..44d137ef62 --- /dev/null +++ b/packages/core/src/reducers/middleware.ts @@ -0,0 +1,12 @@ +import { CoreActions } from '../actions'; +import { JsonFormsCore } from './core'; + +export interface Middleware { + ( + state: JsonFormsCore, + action: CoreActions, + defaultReducer: (state: JsonFormsCore, action: CoreActions) => JsonFormsCore + ): JsonFormsCore; +} +export const defaultMiddleware: Middleware = (state, action, defaultReducer) => + defaultReducer(state, action); diff --git a/packages/react/src/JsonForms.tsx b/packages/react/src/JsonForms.tsx index 73abe6bbfe..343064d5ea 100644 --- a/packages/react/src/JsonForms.tsx +++ b/packages/react/src/JsonForms.tsx @@ -38,6 +38,7 @@ import { JsonFormsRendererRegistryEntry, JsonFormsUISchemaRegistryEntry, JsonSchema, + Middleware, OwnPropsOfJsonFormsRenderer, removeId, UISchemaElement, @@ -45,7 +46,6 @@ import { } from '@jsonforms/core'; import { JsonFormsStateProvider, - Middleware, withJsonFormsRendererProps, } from './JsonFormsContext'; diff --git a/packages/react/src/JsonFormsContext.tsx b/packages/react/src/JsonFormsContext.tsx index 5162f922d7..84752883a3 100644 --- a/packages/react/src/JsonFormsContext.tsx +++ b/packages/react/src/JsonFormsContext.tsx @@ -76,6 +76,8 @@ import { LabelProps, mapStateToLabelProps, CoreActions, + Middleware, + defaultMiddleware, } from '@jsonforms/core'; import debounce from 'lodash/debounce'; import React, { @@ -128,17 +130,6 @@ const useEffectAfterFirstRender = ( }, dependencies); }; -export interface Middleware { - ( - state: JsonFormsCore, - action: CoreActions, - defaultReducer: (state: JsonFormsCore, action: CoreActions) => JsonFormsCore - ): JsonFormsCore; -} - -const defaultMiddleware: Middleware = (state, action, defaultReducer) => - defaultReducer(state, action); - export const JsonFormsStateProvider = ({ children, initState, diff --git a/packages/react/test/renderers/JsonForms.test.tsx b/packages/react/test/renderers/JsonForms.test.tsx index 7098ae4115..d863fb6bb9 100644 --- a/packages/react/test/renderers/JsonForms.test.tsx +++ b/packages/react/test/renderers/JsonForms.test.tsx @@ -33,6 +33,7 @@ import type { JsonFormsStore, JsonSchema, Layout, + Middleware, RendererProps, UISchemaElement, } from '@jsonforms/core'; @@ -58,7 +59,6 @@ import { } from '../../src/JsonForms'; import { JsonFormsStateProvider, - Middleware, useJsonForms, withJsonFormsControlProps, } from '../../src/JsonFormsContext'; diff --git a/packages/vue/src/components/JsonForms.vue b/packages/vue/src/components/JsonForms.vue index 5fb555b5bb..54b10e7f00 100644 --- a/packages/vue/src/components/JsonForms.vue +++ b/packages/vue/src/components/JsonForms.vue @@ -23,6 +23,8 @@ import { CoreActions, i18nReducer, JsonFormsI18nState, + defaultMiddleware, + Middleware, } from '@jsonforms/core'; import { JsonFormsChangeEvent, MaybeReadonly } from '../types'; import DispatchRenderer from './DispatchRenderer.vue'; @@ -109,6 +111,11 @@ export default defineComponent({ type: Array as PropType, default: () => EMPTY, }, + middleware: { + required: false, + type: Function as PropType, + default: defaultMiddleware, + }, }, emits: ['change'], data() { @@ -125,13 +132,14 @@ export default defineComponent({ schema: schemaToUse, uischema: uischemaToUse, }; - const core = coreReducer( + const core = this.middleware( initialCore, Actions.init(dataToUse, schemaToUse, uischemaToUse, { validationMode: this.validationMode, ajv: this.ajv, additionalErrors: this.additionalErrors, - }) + }), + coreReducer ); return core; }; @@ -223,7 +231,7 @@ export default defineComponent({ this.jsonforms.readonly = newReadonly; }, coreDataToUpdate() { - this.jsonforms.core = coreReducer( + this.jsonforms.core = this.middleware( this.jsonforms.core as JsonFormsCore, Actions.updateCore( this.dataToUse, @@ -234,7 +242,8 @@ export default defineComponent({ ajv: this.ajv, additionalErrors: this.additionalErrors, } - ) + ), + coreReducer ); }, eventToEmit(newEvent) { @@ -263,9 +272,10 @@ export default defineComponent({ }, methods: { dispatch(action: CoreActions) { - this.jsonforms.core = coreReducer( + this.jsonforms.core = this.middleware( this.jsonforms.core as JsonFormsCore, - action + action, + coreReducer ); }, }, From 38c780630a0d91de043ac619fcf48859a8dea3c9 Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Mon, 22 Jan 2024 13:37:33 +0100 Subject: [PATCH 2/2] Update packages/core/src/reducers/middleware.ts --- packages/core/src/reducers/middleware.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/core/src/reducers/middleware.ts b/packages/core/src/reducers/middleware.ts index 44d137ef62..d7886bc6e0 100644 --- a/packages/core/src/reducers/middleware.ts +++ b/packages/core/src/reducers/middleware.ts @@ -1,3 +1,27 @@ +/* + The MIT License + + Copyright (c) 2023 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 { CoreActions } from '../actions'; import { JsonFormsCore } from './core';