├── src ├── assets │ ├── .gitkeep │ ├── autocomplete-simple.json │ └── autocomplete-complex.json ├── app │ ├── app.component.css │ ├── custom │ │ ├── custom.component.css │ │ ├── custom.component.html │ │ ├── custom.component.spec.ts │ │ └── custom.component.ts │ ├── form.directive.ts │ ├── app-routing.module.ts │ ├── app.component.spec.ts │ ├── app.module.ts │ ├── app.component.html │ └── app.component.ts ├── favicon.ico ├── styles.css ├── main.ts └── index.html ├── projects └── dashjoin │ └── json-schema-form │ ├── src │ ├── lib │ │ ├── base │ │ │ ├── base.component.css │ │ │ ├── base.component.html │ │ │ ├── base.component.spec.ts │ │ │ └── base.component.ts │ │ ├── chips │ │ │ ├── chips.component.css │ │ │ ├── chips.component.spec.ts │ │ │ ├── chips.component.html │ │ │ └── chips.component.ts │ │ ├── date │ │ │ ├── date.component.css │ │ │ ├── date.component.html │ │ │ ├── date.component.spec.ts │ │ │ └── date.component.ts │ │ ├── input │ │ │ ├── input.component.css │ │ │ ├── input.component.html │ │ │ ├── input.component.spec.ts │ │ │ └── input.component.ts │ │ ├── tab │ │ │ ├── tab.component.css │ │ │ ├── tab.component.ts │ │ │ ├── tab.component.html │ │ │ └── tab.component.spec.ts │ │ ├── table │ │ │ ├── table.component.css │ │ │ ├── table.component.spec.ts │ │ │ ├── table.component.html │ │ │ └── table.component.ts │ │ ├── boolean │ │ │ ├── boolean.component.css │ │ │ ├── boolean.component.html │ │ │ ├── boolean.component.ts │ │ │ └── boolean.component.spec.ts │ │ ├── select │ │ │ ├── select.component.css │ │ │ ├── select.component.html │ │ │ ├── select.component.ts │ │ │ └── select.component.spec.ts │ │ ├── upload │ │ │ ├── upload.component.css │ │ │ ├── upload.component.html │ │ │ ├── upload.component.spec.ts │ │ │ └── upload.component.ts │ │ ├── wrapper │ │ │ ├── wrapper.component.css │ │ │ ├── wrapper.component.html │ │ │ ├── comp.directive.ts │ │ │ ├── wrapper.component.spec.ts │ │ │ └── wrapper.component.ts │ │ ├── textarea │ │ │ ├── textarea.component.css │ │ │ ├── textarea.component.html │ │ │ ├── textarea.component.ts │ │ │ └── textarea.component.spec.ts │ │ ├── autocomplete │ │ │ ├── autocomplete.component.css │ │ │ ├── autocomplete.component.ts │ │ │ ├── autocomplete.component.html │ │ │ └── autocomplete.component.spec.ts │ │ ├── additional-properties │ │ │ ├── additional-properties.component.css │ │ │ ├── additional-properties.component.html │ │ │ ├── additional-properties.component.spec.ts │ │ │ └── additional-properties.component.ts │ │ ├── array │ │ │ ├── array.component.css │ │ │ ├── array.component.html │ │ │ ├── array.component.spec.ts │ │ │ └── array.component.ts │ │ ├── object │ │ │ ├── object.component.css │ │ │ ├── object.component.spec.ts │ │ │ ├── object.component.html │ │ │ └── object.component.ts │ │ ├── choice.ts │ │ ├── state.ts │ │ ├── json-schema-form.service.spec.ts │ │ ├── json-schema-form.service.ts │ │ ├── json-schema-form.component.spec.ts │ │ ├── json-pointer.ts │ │ ├── json-schema-form.component.ts │ │ ├── json-schema-form.module.ts │ │ └── schema.ts │ └── public-api.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ └── README.md ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── package.json ├── angular.json ├── LICENSE └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/custom/custom.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/base/base.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/chips/chips.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/date/date.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/input/input.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/tab/tab.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/table/table.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/boolean/boolean.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/select/select.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/upload/upload.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/wrapper/wrapper.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/textarea/textarea.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/autocomplete/autocomplete.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashjoin/json-schema-form/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/additional-properties/additional-properties.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/array/array.component.css: -------------------------------------------------------------------------------- 1 | .full-width { 2 | flex-basis: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/object/object.component.css: -------------------------------------------------------------------------------- 1 | .full-width { 2 | flex-basis: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/boolean/boolean.component.html: -------------------------------------------------------------------------------- 1 | {{state.name}} -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "@angular/material/prebuilt-themes/indigo-pink.css"; 3 | @import "https://fonts.googleapis.com/icon?family=Material+Icons"; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/dashjoin/json-schema-form", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/form.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ViewContainerRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[formHost]', 5 | }) 6 | export class FormDirective { 7 | constructor(public viewContainerRef: ViewContainerRef) { } 8 | } 9 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/wrapper/wrapper.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/textarea/textarea.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{state.name}} 3 | 4 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/choice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * class backing a select / autocomplete option 3 | */ 4 | export interface Choice { 5 | 6 | /** 7 | * select value 8 | */ 9 | value: any; 10 | 11 | /** 12 | * display name 13 | */ 14 | name: string; 15 | } 16 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/date/date.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{state.name}} 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/select/select.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{state.name}} 3 | 4 | {{choice.name}} 5 | 6 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/wrapper/comp.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ViewContainerRef } from '@angular/core'; 2 | 3 | /** 4 | * directive to dynamically add form elements 5 | */ 6 | @Directive({ 7 | selector: '[compHost]', 8 | }) 9 | export class CompDirective { 10 | constructor(public viewContainerRef: ViewContainerRef) { } 11 | } 12 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/tab/tab.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ArrayComponent } from '../array/array.component'; 3 | 4 | @Component({ 5 | selector: 'app-tab', 6 | templateUrl: './tab.component.html', 7 | styleUrls: ['./tab.component.css'] 8 | }) 9 | export class TabComponent extends ArrayComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/upload/upload.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 |
8 | 9 |
-------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/textarea/textarea.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | 4 | @Component({ 5 | selector: 'app-textarea', 6 | templateUrl: './textarea.component.html', 7 | styleUrls: ['./textarea.component.css'] 8 | }) 9 | export class TextareaComponent extends BaseComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/base/base.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of json-schema-form 3 | */ 4 | 5 | export * from './lib/json-schema-form.service'; 6 | export * from './lib/json-schema-form.component'; 7 | export * from './lib/json-schema-form.module'; 8 | export * from './lib/schema'; 9 | export * from './lib/choice'; 10 | export * from './lib/state'; 11 | export * from './lib/base/base.component' -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/autocomplete/autocomplete.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | 4 | @Component({ 5 | selector: 'app-autocomplete', 6 | templateUrl: './autocomplete.component.html', 7 | styleUrls: ['./autocomplete.component.css'] 8 | }) 9 | export class AutocompleteComponent extends BaseComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/select/select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | 4 | /** 5 | * select input 6 | */ 7 | @Component({ 8 | selector: 'app-select', 9 | templateUrl: './select.component.html', 10 | styleUrls: ['./select.component.css'] 11 | }) 12 | export class SelectComponent extends BaseComponent { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/boolean/boolean.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | 4 | /** 5 | * use checkbox for boolean 6 | */ 7 | @Component({ 8 | selector: 'app-boolean', 9 | templateUrl: './boolean.component.html', 10 | styleUrls: ['./boolean.component.css'] 11 | }) 12 | export class BooleanComponent extends BaseComponent { 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/autocomplete-simple.json: -------------------------------------------------------------------------------- 1 | [ 2 | "China", 3 | "India", 4 | "United States", 5 | "Indonesia", 6 | "Brazil", 7 | "Pakistan", 8 | "Nigeria", 9 | "Bangladesh", 10 | "Russia", 11 | "Mexico", 12 | "Japan", 13 | "Philippines", 14 | "Egypt", 15 | "Ethiopia", 16 | "Vietnam", 17 | "DR Congo", 18 | "Iran", 19 | "Turkey", 20 | "Germany", 21 | "France" 22 | ] -------------------------------------------------------------------------------- /src/app/custom/custom.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ state.name }}
3 | 4 | 5 | 11 |
-------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/autocomplete/autocomplete.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{state.name}} 3 | 4 | 5 | 6 | {{option.name}} 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { MainComponent } from './app.component'; 4 | 5 | const routes: Routes = [ 6 | { path: 'example/:id', component: MainComponent }, 7 | { path: '**', component: MainComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forRoot(routes, { useHash: true })], 12 | exports: [RouterModule] 13 | }) 14 | export class AppRoutingModule { } 15 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/state.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from "@angular/forms"; 2 | import { Schema } from "./schema"; 3 | 4 | export class State { 5 | /** 6 | * form control handling this state 7 | */ 8 | control!: AbstractControl; 9 | 10 | /** 11 | * name of the state 12 | */ 13 | name!: string; 14 | 15 | /** 16 | * schema for this state 17 | */ 18 | schema!: Schema; 19 | 20 | /** 21 | * current value 22 | */ 23 | value: any; 24 | } 25 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/json-schema-form.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { JsonSchemaFormService } from './json-schema-form.service'; 4 | 5 | describe('JsonSchemaFormService', () => { 6 | let service: JsonSchemaFormService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(JsonSchemaFormService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JsonSchemaForm Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | Fork me on GitHub 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/tab/tab.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/autocomplete-complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": [ 3 | { 4 | "name": "China", 5 | "url": "https://en.wikipedia.org/wiki/Demographics_of_China" 6 | }, 7 | { 8 | "name": "India", 9 | "url": "https://en.wikipedia.org/wiki/Demographics_of_India" 10 | }, 11 | { 12 | "name": "United States", 13 | "url": "https://en.wikipedia.org/wiki/Demographics_of_the_United_States" 14 | }, 15 | { 16 | "name": "Indonesia", 17 | "url": "https://en.wikipedia.org/wiki/Indonesia" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/input/input.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{state.name}} 3 | 5 | 7 | {{state.schema.errorMessage}} 8 | -------------------------------------------------------------------------------- /src/app/custom/custom.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomComponent } from './custom.component'; 4 | 5 | describe('CustomComponent', () => { 6 | let component: CustomComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CustomComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CustomComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/tab/tab.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TabComponent } from './tab.component'; 4 | 5 | describe('TabComponent', () => { 6 | let component: TabComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TabComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TabComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/base/base.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BaseComponent } from './base.component'; 4 | 5 | describe('BaseComponent', () => { 6 | let component: BaseComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BaseComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(BaseComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/date/date.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DateComponent } from './date.component'; 4 | 5 | describe('DateComponent', () => { 6 | let component: DateComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DateComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DateComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/array/array.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | 5 | 8 |
9 | 12 |
13 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/array/array.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArrayComponent } from './array.component'; 4 | 5 | describe('ArrayComponent', () => { 6 | let component: ArrayComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ArrayComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ArrayComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/chips/chips.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChipsComponent } from './chips.component'; 4 | 5 | describe('ChipsComponent', () => { 6 | let component: ChipsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChipsComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ChipsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/input/input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { InputComponent } from './input.component'; 4 | 5 | describe('InputComponent', () => { 6 | let component: InputComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ InputComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(InputComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/table/table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TableComponent } from './table.component'; 4 | 5 | describe('TableComponent', () => { 6 | let component: TableComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TableComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TableComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/select/select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SelectComponent } from './select.component'; 4 | 5 | describe('InputComponent', () => { 6 | let component: SelectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SelectComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SelectComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/object/object.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ObjectComponent } from './object.component'; 4 | 5 | describe('ObjectComponent', () => { 6 | let component: ObjectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ObjectComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ObjectComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/upload/upload.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UploadComponent } from './upload.component'; 4 | 5 | describe('UploadComponent', () => { 6 | let component: UploadComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UploadComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(UploadComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/boolean/boolean.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BooleanComponent } from './boolean.component'; 4 | 5 | describe('BooleanComponent', () => { 6 | let component: BooleanComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BooleanComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(BooleanComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/wrapper/wrapper.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WrapperComponent } from './wrapper.component'; 4 | 5 | describe('WrapperComponent', () => { 6 | let component: WrapperComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ WrapperComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(WrapperComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/textarea/textarea.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TextareaComponent } from './textarea.component'; 4 | 5 | describe('TextareaComponent', () => { 6 | let component: TextareaComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TextareaComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TextareaComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/chips/chips.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{state.name}} 3 | 4 | 6 | {{fruit}} 7 | 10 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dashjoin/json-schema-form", 3 | "version": "1.0.3", 4 | "license": "Apache-2.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/dashjoin/json-schema-form" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/dashjoin/json-schema-form/issues" 11 | }, 12 | "keywords": [ 13 | "json-schema", 14 | "angular", 15 | "angular2", 16 | "form-builder", 17 | "autocomplete", 18 | "json pointer" 19 | ], 20 | "peerDependencies": { 21 | "jsonata": "^2.0.4", 22 | "@angular/material": "^16.2.12", 23 | "@angular/common": "^16.2.12", 24 | "@angular/core": "^16.2.12" 25 | }, 26 | "dependencies": { 27 | "tslib": "^2.3.0" 28 | }, 29 | "sideEffects": false 30 | } -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/additional-properties/additional-properties.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{state.name}} 5 | 6 | 7 |   8 | 9 | 12 |   13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/autocomplete/autocomplete.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AutocompleteComponent } from './autocomplete.component'; 4 | 5 | describe('AutocompleteComponent', () => { 6 | let component: AutocompleteComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AutocompleteComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(AutocompleteComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/json-schema-form.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class JsonSchemaFormService { 7 | 8 | constructor() { } 9 | 10 | /** 11 | * registry of custom widgets. The keys are the values used in schema.widgetType, the values 12 | * are the Type of the custom widget component implementing WidgetComponent 13 | */ 14 | registry: any = {}; 15 | 16 | /** 17 | * register custom component 18 | * @param key the name of the component which is used in schema extension: widget=custom, widgetType=key 19 | * @param value the implementation class 20 | */ 21 | registerComponent(key: string, value: Type) { 22 | this.registry[key] = value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/json-schema-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { JsonSchemaFormComponent } from './json-schema-form.component'; 4 | 5 | describe('JsonSchemaFormComponent', () => { 6 | let component: JsonSchemaFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ JsonSchemaFormComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(JsonSchemaFormComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/table/table.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 17 | 18 |
{{x.value.title ? x.value.title : 4 | x.key}} 5 |
10 | 11 | 13 | 16 |
19 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/additional-properties/additional-properties.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdditionalPropertiesComponent } from './additional-properties.component'; 4 | 5 | describe('AdditionalPropertiesComponent', () => { 6 | let component: AdditionalPropertiesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AdditionalPropertiesComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(AdditionalPropertiesComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/input/input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | 4 | /** 5 | * single text field inputs 6 | */ 7 | @Component({ 8 | selector: 'app-input', 9 | templateUrl: './input.component.html', 10 | styleUrls: ['./input.component.css'] 11 | }) 12 | export class InputComponent extends BaseComponent { 13 | 14 | /** 15 | * called from template in the "simple" type. If "type" is "number" or "integer", 16 | * the HTML input type is "number" which avoids normal string input 17 | */ 18 | getInputType(): string { 19 | if (this.state.schema.type === 'number') { 20 | return 'number'; 21 | } 22 | if (this.state.schema.type === 'integer') { 23 | return 'number'; 24 | } 25 | return this.state.schema.widget ? this.state.schema.widget : 'string'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/custom/custom.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Editor, Toolbar } from 'ngx-editor'; 3 | import { BaseComponent } from '@dashjoin/json-schema-form'; 4 | 5 | @Component({ 6 | selector: 'app-custom', 7 | templateUrl: './custom.component.html', 8 | styleUrls: ['./custom.component.css'] 9 | }) 10 | export class CustomComponent extends BaseComponent { 11 | 12 | /** 13 | * editor instance 14 | */ 15 | editor!: Editor; 16 | 17 | /** 18 | * init editor 19 | */ 20 | override ngOnInit() { 21 | super.ngOnInit() 22 | this.state.name = 'Does not update the form at the moment (probably due to Angular 14 / 15)' 23 | this.editor = new Editor(); 24 | } 25 | 26 | /** 27 | * cleanup 28 | */ 29 | ngOnDestroy(): void { 30 | this.editor.destroy(); 31 | } 32 | 33 | /** 34 | * emit value change and error state 35 | */ 36 | onChange(newValue: Record) { 37 | this.state.control.setValue(newValue.toString()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/table/table.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ArrayComponent } from '../array/array.component'; 3 | import { ObjectComponent } from '../object/object.component'; 4 | 5 | @Component({ 6 | selector: 'app-table', 7 | templateUrl: './table.component.html', 8 | styleUrls: ['./table.component.css'] 9 | }) 10 | export class TableComponent extends ArrayComponent { 11 | 12 | rows: ObjectComponent[] = [] 13 | 14 | /** 15 | * populate FormArray control 16 | */ 17 | override ngOnInit(): void { 18 | super.ngOnInit(); 19 | 20 | for (const state of this.states) { 21 | const object = new ObjectComponent(this.http, this.service) 22 | object.state = state 23 | object.ngOnInit() 24 | this.rows.push(object) 25 | } 26 | } 27 | 28 | /** 29 | * add a new element 30 | */ 31 | override add() { 32 | super.add() 33 | const object = new ObjectComponent(this.http, this.service) 34 | object.state = this.states[this.states.length - 1] 35 | object.ngOnInit() 36 | this.rows.push(object) 37 | } 38 | 39 | override remove(i: number): void { 40 | super.remove(i) 41 | this.rows.splice(i, 1) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | // needed to include jsonata 6 | "esModuleInterop": true, 7 | "paths": { 8 | "@dashjoin/json-schema-form": [ 9 | "dist/dashjoin/json-schema-form" 10 | ] 11 | }, 12 | "baseUrl": "./", 13 | "outDir": "./dist/out-tsc", 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitOverride": true, 17 | "noPropertyAccessFromIndexSignature": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "sourceMap": true, 21 | "declaration": false, 22 | "downlevelIteration": true, 23 | "experimentalDecorators": true, 24 | "moduleResolution": "node", 25 | "importHelpers": true, 26 | "target": "ES2022", 27 | "module": "ES2022", 28 | "useDefineForClassFields": false, 29 | "lib": [ 30 | "ES2022", 31 | "dom" 32 | ] 33 | }, 34 | "angularCompilerOptions": { 35 | "enableI18nLegacyMessageIdFormat": false, 36 | "strictInjectionParameters": true, 37 | "strictInputAccessModifiers": true, 38 | "strictTemplates": true 39 | } 40 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'json-schema-form'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('json-schema-form'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('json-schema-form app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/object/object.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | 5 |   6 |
7 |
8 |
10 |
11 | 12 |
14 |
15 | 16 |   17 |
18 |
19 |   20 |
21 |
22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-form", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.2.12", 14 | "@angular/common": "^16.2.12", 15 | "@angular/compiler": "^16.2.12", 16 | "@angular/core": "^16.2.12", 17 | "@angular/forms": "^16.2.12", 18 | "@angular/material": "^16.2.14", 19 | "@angular/platform-browser": "^16.2.12", 20 | "@angular/platform-browser-dynamic": "^16.2.12", 21 | "@angular/router": "^16.2.12", 22 | "jsonata": "^2.0.4", 23 | "ngx-editor": "^15.3.0", 24 | "rxjs": "~7.8.0", 25 | "tslib": "^2.3.0", 26 | "zone.js": "~0.13.3" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^16.2.14", 30 | "@angular/cli": "~16.2.14", 31 | "@angular/compiler-cli": "^16.2.12", 32 | "@types/jasmine": "~4.3.0", 33 | "jasmine-core": "~4.5.0", 34 | "karma": "~6.4.0", 35 | "karma-chrome-launcher": "~3.1.0", 36 | "karma-coverage": "~2.2.0", 37 | "karma-jasmine": "~5.1.0", 38 | "karma-jasmine-html-reporter": "~2.0.0", 39 | "ng-packagr": "^16.2.3", 40 | "typescript": "~4.9.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/upload/upload.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | 4 | @Component({ 5 | selector: 'app-upload', 6 | templateUrl: './upload.component.html', 7 | styleUrls: ['./upload.component.css'] 8 | }) 9 | export class UploadComponent extends BaseComponent { 10 | 11 | /** 12 | * allows for the result of a file upload to be written into a text form element 13 | */ 14 | handleFileInput(base64: boolean, event: any) { 15 | if (10 * 1024 * 1024 <= event.target.files.item(0).size) { 16 | console.log('The file size is limited to 10MB'); 17 | return; 18 | } 19 | let value: any 20 | const reader = new FileReader(); 21 | reader.onload = () => { 22 | value = reader.result; 23 | 24 | if (this.state.schema.type === 'array') { 25 | value = JSON.parse(value); 26 | } 27 | if (this.state.schema.type === 'object') { 28 | try { 29 | value = JSON.parse(value); 30 | } 31 | catch (ignore) { 32 | } 33 | value = { 34 | name: event.target.files.item(0).name, 35 | lastModified: event.target.files.item(0).lastModified, 36 | size: event.target.files.item(0).size, 37 | type: event.target.files.item(0).type, 38 | value: value 39 | }; 40 | } 41 | 42 | this.state.control.setValue(value) 43 | }; 44 | if (base64) { 45 | reader.readAsDataURL(event.target.files.item(0)); 46 | } else { 47 | reader.readAsText(event.target.files.item(0)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { JsonSchemaFormModule } from 'projects/dashjoin/json-schema-form/src/public-api'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { MatExpansionModule } from '@angular/material/expansion'; 7 | import { MatToolbarModule } from '@angular/material/toolbar'; 8 | import { MatCardModule } from '@angular/material/card'; 9 | import { MatTooltipModule } from '@angular/material/tooltip'; 10 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import { MatCheckboxModule } from '@angular/material/checkbox'; 13 | 14 | import { AppRoutingModule } from './app-routing.module'; 15 | import { AppComponent, MainComponent } from './app.component'; 16 | import { FormDirective } from './form.directive'; 17 | import { CustomComponent } from './custom/custom.component'; 18 | import { NgxEditorModule } from 'ngx-editor'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent, MainComponent, FormDirective, CustomComponent 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | BrowserAnimationsModule, 27 | AppRoutingModule, 28 | JsonSchemaFormModule, 29 | MatToolbarModule, 30 | MatCardModule, 31 | MatButtonModule, 32 | MatSlideToggleModule, 33 | MatToolbarModule, 34 | MatExpansionModule, 35 | MatTooltipModule, 36 | MatCheckboxModule, 37 | NgxEditorModule 38 | ], 39 | providers: [], 40 | bootstrap: [AppComponent] 41 | }) 42 | export class AppModule { } 43 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/chips/chips.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatChipEditedEvent, MatChipInputEvent } from '@angular/material/chips'; 3 | import { COMMA, ENTER } from '@angular/cdk/keycodes'; 4 | import { BaseComponent } from '../base/base.component'; 5 | 6 | @Component({ 7 | selector: 'app-chips', 8 | templateUrl: './chips.component.html', 9 | styleUrls: ['./chips.component.css'] 10 | }) 11 | export class ChipsComponent extends BaseComponent implements OnInit { 12 | 13 | override ngOnInit(): void { 14 | super.ngOnInit() 15 | if (!this.state.value) 16 | this.state.value = [] 17 | } 18 | 19 | addOnBlur = true; 20 | readonly separatorKeysCodes = [ENTER, COMMA] as const; 21 | 22 | add(event: MatChipInputEvent): void { 23 | const value = (event.value || '').trim(); 24 | 25 | // Add our fruit 26 | if (value) { 27 | this.state.value.push(value); 28 | } 29 | 30 | // Clear the input value 31 | event.chipInput!.clear(); 32 | 33 | this.state.control.setValue(this.state.value) 34 | } 35 | 36 | remove(fruit: string): void { 37 | const index = this.state.value.indexOf(fruit); 38 | 39 | if (index >= 0) { 40 | this.state.value.splice(index, 1); 41 | } 42 | 43 | this.state.control.setValue(this.state.value) 44 | } 45 | 46 | edit(fruit: string, event: MatChipEditedEvent) { 47 | const value = event.value.trim(); 48 | 49 | // Remove fruit if it no longer has a name 50 | if (!value) { 51 | this.remove(fruit); 52 | return; 53 | } 54 | 55 | // Edit existing fruit 56 | const index = this.state.value.indexOf(fruit); 57 | if (index >= 0) { 58 | this.state.value[index] = value; 59 | } 60 | 61 | this.state.control.setValue(this.state.value) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/array/array.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | import { State } from '../state'; 4 | 5 | /** 6 | * renders arrays as a list of subforms with + and - buttons 7 | */ 8 | @Component({ 9 | selector: 'app-array', 10 | templateUrl: './array.component.html', 11 | styleUrls: ['./array.component.css'] 12 | }) 13 | export class ArrayComponent extends BaseComponent { 14 | 15 | /** 16 | * child states 17 | */ 18 | states: State[] = []; 19 | 20 | /** 21 | * which subform is active 22 | */ 23 | hover: number | null = null 24 | 25 | /** 26 | * populate FormArray control 27 | */ 28 | override ngOnInit(): void { 29 | super.ngOnInit(); 30 | let i = 0; 31 | if (this.state.schema.items && this.state.value) 32 | for (const v of this.state.value) { 33 | 34 | const control = BaseComponent.createControl(this.state.schema.items, v, false); 35 | this.formArray().setControl(i, control); 36 | this.states.push({ 37 | name: this.state.name, 38 | schema: this.state.schema.items, 39 | value: v, 40 | control 41 | }); 42 | i++; 43 | } 44 | } 45 | 46 | /** 47 | * add a new element 48 | */ 49 | add() { 50 | const control = BaseComponent.createControl(this.state.schema.items!, undefined, false); 51 | this.formArray().setControl(this.states.length, control); 52 | this.states.push({ 53 | name: this.state.name, 54 | schema: this.state.schema.items!, 55 | value: undefined, 56 | control 57 | }); 58 | } 59 | 60 | /** 61 | * remove element at index i 62 | */ 63 | remove(i: number) { 64 | this.states.splice(i, 1) 65 | this.formArray().removeAt(i) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/json-pointer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * static JsonPointer implementation 3 | */ 4 | export class JsonPointer { 5 | 6 | /** 7 | * evaluate the JSON pointer on o 8 | */ 9 | static jsonPointer(o: any, pointer: string): any { 10 | return JsonPointer.jsonPointer2(o, JsonPointer.split(pointer)); 11 | } 12 | 13 | /** 14 | * evaluate the JSON pointer (parsed array of paths) on o 15 | */ 16 | static jsonPointer2(o: any, paths: string[]): any { 17 | 18 | if (o === undefined) { 19 | return undefined; 20 | } 21 | 22 | if (paths.length === 0) { 23 | return o; 24 | } 25 | 26 | const path = paths[0]; 27 | const np = Object.assign([], paths); 28 | np.splice(0, 1); 29 | 30 | if (paths[0] === '*') { 31 | const res = []; 32 | for (const f of (typeof (o) === 'object' ? Object.values(o) : o)) { 33 | res.push(this.jsonPointer2(f, np)); 34 | } 35 | return res; 36 | } else { 37 | return this.jsonPointer2(o[path], np); 38 | } 39 | } 40 | 41 | /** 42 | * strip leading / and split the JSON pointer 43 | */ 44 | static split(s: string): string[] { 45 | if (s === '') { 46 | return []; 47 | } 48 | if (s.startsWith('/')) { 49 | s = s.substring(1); 50 | const arr = s.split('/'); 51 | for (const a of arr) { 52 | if (a === '') { 53 | throw new Error('JSON Pointer must not contain an empty reference token'); 54 | } 55 | } 56 | return arr; 57 | } 58 | throw new Error('JSON Pointer must start with /'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/object/object.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | import { State } from '../state'; 4 | 5 | @Component({ 6 | selector: 'app-object', 7 | templateUrl: './object.component.html', 8 | styleUrls: ['./object.component.css'] 9 | }) 10 | export class ObjectComponent extends BaseComponent implements OnInit { 11 | 12 | /** 13 | * child states 14 | */ 15 | states: State[] = []; 16 | 17 | /** 18 | * populate FormGroup 19 | */ 20 | override ngOnInit(): void { 21 | super.ngOnInit(); 22 | if (this.state.schema.properties) 23 | for (const [k, v] of Object.entries(this.state.schema.properties)) { 24 | 25 | const control = BaseComponent.createControl(v, this.state.value?.[k], 26 | this.state.schema.required ? this.state.schema.required.includes(k) : false); 27 | this.formGroup().addControl(k, control); 28 | 29 | this.states.push({ 30 | name: k, 31 | schema: v, 32 | value: this.state.value?.[k], 33 | control 34 | }) 35 | } 36 | } 37 | 38 | show(i: number): boolean { 39 | if (this.state.schema.switch) { 40 | const switc = this.formGroup().get(this.state.schema.switch)?.value 41 | const prop = Object.values(this.state.schema.properties!)[i] 42 | if (!prop.case) 43 | return true 44 | if (!switc) 45 | return false 46 | return (prop.case && prop.case.includes(switc)) 47 | } 48 | return true 49 | } 50 | 51 | getState(o: string | string[]): State | undefined { 52 | for (const state of this.states) 53 | if (state.name === o) 54 | return state 55 | return undefined 56 | } 57 | 58 | getArray(o: string | string[]): string[] | undefined { 59 | if (Array.isArray(o)) 60 | return o 61 | return undefined 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/json-schema-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { FormArray } from '@angular/forms'; 3 | import { BaseComponent } from '../public-api'; 4 | import { AdditionalPropertiesComponent } from './additional-properties/additional-properties.component'; 5 | import { ArrayComponent } from './array/array.component'; 6 | import { JsonPointer } from './json-pointer'; 7 | import { ObjectComponent } from './object/object.component'; 8 | import { Schema } from './schema'; 9 | import { State } from './state'; 10 | import { TabComponent } from './tab/tab.component'; 11 | import { TableComponent } from './table/table.component'; 12 | import { WrapperComponent } from './wrapper/wrapper.component'; 13 | 14 | /** 15 | * entry component 16 | */ 17 | @Component({ 18 | selector: 'lib-json-schema-form', 19 | template: ` 20 | 21 | `, 22 | styles: [ 23 | ] 24 | }) 25 | export class JsonSchemaFormComponent implements OnInit { 26 | 27 | @Input() state!: State; 28 | 29 | resolvedState!: State 30 | 31 | /** 32 | * register container form elements to avoid cyclic imports 33 | */ 34 | ngOnInit(): void { 35 | 36 | const clone: Schema = JSON.parse(JSON.stringify(this.state.schema)) 37 | 38 | this.resolvedState = { 39 | value: this.state.value, 40 | schema: clone, 41 | control: this.state.control, 42 | name: this.state.name 43 | } 44 | 45 | WrapperComponent.arrayComponent = ArrayComponent 46 | WrapperComponent.tabComponent = TabComponent 47 | WrapperComponent.tableComponent = TableComponent 48 | WrapperComponent.objectComponent = ObjectComponent 49 | WrapperComponent.additionalPropertiesComponent = AdditionalPropertiesComponent 50 | 51 | this.resolve(clone) 52 | 53 | BaseComponent.prepareControl(this.state.control, clone, this.state.value, false) 54 | } 55 | 56 | resolve(schema?: Schema) { 57 | if (!schema) 58 | return 59 | if (schema.$ref) { 60 | if (schema.$ref.startsWith('#')) 61 | for (const [k, v] of Object.entries(JsonPointer.jsonPointer(this.state.schema, schema.$ref.substring(1)))) 62 | (schema as any)[k] = v 63 | } 64 | this.resolve(schema.additionalProperties) 65 | this.resolve(schema.items) 66 | if (schema.properties) 67 | for (const prop of Object.values(schema.properties)) 68 | this.resolve(prop) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/additional-properties/additional-properties.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | import { State } from '../state'; 4 | 5 | @Component({ 6 | selector: 'app-additional-properties', 7 | templateUrl: './additional-properties.component.html', 8 | styleUrls: ['./additional-properties.component.css'] 9 | }) 10 | export class AdditionalPropertiesComponent extends BaseComponent { 11 | 12 | hover: number | null = null 13 | 14 | /** 15 | * child states 16 | */ 17 | states: State[] = []; 18 | 19 | /** 20 | * populate FormGroup 21 | */ 22 | override ngOnInit(): void { 23 | super.ngOnInit(); 24 | if (this.state.value) 25 | for (const [k, v] of Object.entries(this.state.value)) { 26 | 27 | const control = BaseComponent.createControl(this.state.schema.additionalProperties!, v, 28 | this.state.schema.required ? this.state.schema.required.includes(k) : false); 29 | this.formGroup().addControl(k, control); 30 | 31 | this.states.push({ 32 | name: k, 33 | schema: this.state.schema.additionalProperties!, 34 | value: v, 35 | control 36 | }) 37 | } 38 | } 39 | 40 | change(event: any, i: number) { 41 | const control = BaseComponent.createControl(this.state.schema.additionalProperties!, undefined, false) 42 | const oldKey = Object.keys(this.formGroup().value)[i] 43 | const newKey = event.target.value 44 | const state = { 45 | name: newKey, 46 | schema: this.state.schema.additionalProperties!, 47 | value: undefined, 48 | control 49 | } 50 | this.states.splice(i, 1, state) 51 | this.formGroup().addControl(newKey, control) 52 | this.formGroup().removeControl(oldKey) 53 | } 54 | 55 | add() { 56 | if (Object.keys(this.formGroup().value).includes('')) 57 | // already contains empty key 58 | return 59 | const control = BaseComponent.createControl(this.state.schema.additionalProperties!, undefined, false) 60 | this.states.push({ 61 | name: '', 62 | schema: this.state.schema.additionalProperties!, 63 | value: undefined, 64 | control 65 | }) 66 | this.formGroup().addControl('', control) 67 | } 68 | 69 | remove(i: number) { 70 | const key = Object.keys(this.formGroup().value)[i] 71 | this.formGroup().removeControl(key) 72 | this.states.splice(i, 1) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/date/date.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { BaseComponent } from '../base/base.component'; 4 | 5 | @Component({ 6 | selector: 'app-date', 7 | templateUrl: './date.component.html', 8 | styleUrls: ['./date.component.css'] 9 | }) 10 | export class DateComponent extends BaseComponent implements OnInit { 11 | 12 | control!: FormControl 13 | 14 | override ngOnInit(): void { 15 | if (this.state.schema.type === 'integer') { 16 | // create a new control and convert between timestamp and Date 17 | this.control = new FormControl(new Date(this.state.value)) 18 | this.control.valueChanges.subscribe(res => { 19 | this.state.control.setValue(res instanceof Date ? res.valueOf() : res) 20 | }) 21 | } else if (this.state.schema.dateFormat) { 22 | // create a new control and convert between Date format string and Date 23 | this.control = this.state.control as FormControl 24 | const pdate = this.state.value.split(this.getDelimiter(this.state.schema.dateFormat)); 25 | const pformat = this.state.schema.dateFormat.split(this.getDelimiter(this.state.schema.dateFormat)); 26 | this.control = new FormControl(new Date(pdate[pformat.indexOf('yyyy')], pdate[pformat.indexOf('MM')] - 1, pdate[pformat.indexOf('dd')])) 27 | this.control.valueChanges.subscribe(date => { 28 | if (!date) { 29 | this.state.control.setValue(null) 30 | return 31 | } 32 | const pformat = this.state.schema.dateFormat!.split(this.getDelimiter(this.state.schema.dateFormat!)); 33 | const pdate = [null, null, null]; 34 | pdate[pformat.indexOf('yyyy')] = date.getFullYear(); 35 | pdate[pformat.indexOf('MM')] = date.getMonth() + 1; 36 | pdate[pformat.indexOf('dd')] = date.getDate(); 37 | this.state.control.setValue(pdate[0] + this.getDelimiter(this.state.schema.dateFormat!) + pdate[1] + this.getDelimiter(this.state.schema.dateFormat!) + pdate[2]) 38 | }) 39 | } 40 | else 41 | // use state.control directly 42 | this.control = this.state.control as FormControl 43 | } 44 | 45 | /** 46 | * find the first non letter character in a date format such as dd/MM/yyyy (returns /) 47 | */ 48 | getDelimiter(format: string): string { 49 | const delim = format.match(/\W/g); 50 | if (!delim?.[0]) { 51 | throw new Error('No delimiter found in date format: ' + format); 52 | } 53 | return delim[0]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/json-schema-form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BaseComponent } from './base/base.component'; 3 | import { BooleanComponent } from './boolean/boolean.component'; 4 | import { InputComponent } from './input/input.component'; 5 | import { JsonSchemaFormComponent } from './json-schema-form.component'; 6 | import { CompDirective } from './wrapper/comp.directive'; 7 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 8 | import { MatCheckboxModule } from '@angular/material/checkbox'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatIconModule } from '@angular/material/icon'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import { MatSelectModule } from '@angular/material/select'; 13 | import { MatDatepickerModule } from '@angular/material/datepicker'; 14 | import { MatNativeDateModule } from '@angular/material/core'; 15 | import { MatTooltipModule } from '@angular/material/tooltip'; 16 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 17 | import { MatTabsModule } from '@angular/material/tabs'; 18 | import { MatChipsModule } from '@angular/material/chips'; 19 | 20 | import { CommonModule } from '@angular/common'; 21 | 22 | import { ObjectComponent } from './object/object.component'; 23 | import { ArrayComponent } from './array/array.component'; 24 | import { WrapperComponent } from './wrapper/wrapper.component'; 25 | import { SelectComponent } from './select/select.component'; 26 | import { HttpClientModule } from '@angular/common/http'; 27 | import { UploadComponent } from './upload/upload.component'; 28 | import { DateComponent } from './date/date.component'; 29 | import { TextareaComponent } from './textarea/textarea.component'; 30 | import { AdditionalPropertiesComponent } from './additional-properties/additional-properties.component'; 31 | import { AutocompleteComponent } from './autocomplete/autocomplete.component'; 32 | import { TabComponent } from './tab/tab.component'; 33 | import { TableComponent } from './table/table.component'; 34 | import { ChipsComponent } from './chips/chips.component'; 35 | 36 | 37 | @NgModule({ 38 | declarations: [ 39 | JsonSchemaFormComponent, 40 | BooleanComponent, 41 | InputComponent, 42 | ObjectComponent, 43 | BaseComponent, 44 | SelectComponent, 45 | ArrayComponent, 46 | UploadComponent, 47 | DateComponent, 48 | TextareaComponent, 49 | AdditionalPropertiesComponent, 50 | AutocompleteComponent, 51 | TabComponent, 52 | TableComponent, 53 | ChipsComponent, 54 | 55 | CompDirective, 56 | WrapperComponent 57 | ], 58 | imports: [ 59 | CommonModule, 60 | HttpClientModule, 61 | FormsModule, 62 | ReactiveFormsModule, 63 | MatCheckboxModule, 64 | MatInputModule, 65 | MatIconModule, 66 | MatButtonModule, 67 | MatSelectModule, 68 | MatDatepickerModule, 69 | MatNativeDateModule, 70 | MatTooltipModule, 71 | MatAutocompleteModule, 72 | MatTabsModule, 73 | MatChipsModule 74 | ], 75 | exports: [ 76 | JsonSchemaFormComponent, 77 | BaseComponent 78 | ] 79 | }) 80 | export class JsonSchemaFormModule { } 81 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/wrapper/wrapper.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Type, ViewChild } from '@angular/core'; 2 | import { BaseComponent } from '../base/base.component'; 3 | import { CompDirective } from './comp.directive'; 4 | import { BooleanComponent } from '../boolean/boolean.component'; 5 | import { InputComponent } from '../input/input.component'; 6 | import { SelectComponent } from '../select/select.component'; 7 | import { UploadComponent } from '../upload/upload.component'; 8 | import { DateComponent } from '../date/date.component'; 9 | import { TextareaComponent } from '../textarea/textarea.component'; 10 | import { AutocompleteComponent } from '../autocomplete/autocomplete.component'; 11 | import { ChipsComponent } from '../chips/chips.component'; 12 | 13 | /** 14 | * determine which form element to use and render it dynamically 15 | */ 16 | @Component({ 17 | selector: 'app-wrapper', 18 | templateUrl: './wrapper.component.html', 19 | styleUrls: ['./wrapper.component.css'] 20 | }) 21 | export class WrapperComponent extends BaseComponent implements OnInit { 22 | 23 | /** 24 | * set from json-schema-form component to avoid cyclic imports 25 | */ 26 | static objectComponent: Type 27 | static arrayComponent: Type 28 | static tabComponent: Type 29 | static tableComponent: Type 30 | static additionalPropertiesComponent: Type 31 | 32 | /** 33 | * dynamically render form element 34 | */ 35 | @ViewChild(CompDirective, { static: true }) compHost!: CompDirective; 36 | 37 | /** 38 | * determine which form element to use 39 | */ 40 | override ngOnInit() { 41 | 42 | const viewContainerRef = this.compHost.viewContainerRef; 43 | viewContainerRef.clear(); 44 | 45 | let type: Type; 46 | if (this.state.schema.additionalProperties) 47 | type = WrapperComponent.additionalPropertiesComponent; 48 | else if (this.state.schema.layout === 'tab') 49 | type = WrapperComponent.tabComponent; 50 | else if (this.state.schema.layout === 'table') 51 | type = WrapperComponent.tableComponent; 52 | else if (this.state.schema.layout === 'select') 53 | type = SelectComponent; 54 | else if (this.state.schema.layout === 'chips') 55 | type = ChipsComponent; 56 | else if (this.state.schema.type === 'object') 57 | type = WrapperComponent.objectComponent; 58 | else if (this.state.schema.type === 'array') 59 | type = WrapperComponent.arrayComponent; 60 | else if (this.state.schema.widget === 'select') 61 | type = SelectComponent; 62 | else if (this.state.schema.choicesUrl || this.state.schema.choices) 63 | type = AutocompleteComponent; 64 | else if (this.state.schema.enum) 65 | type = SelectComponent; 66 | else if (this.state.schema.widget === 'custom') 67 | type = this.service.registry[this.state.schema.widgetType!]; 68 | else if (this.state.schema.widget === 'textarea') 69 | type = TextareaComponent; 70 | else if (this.state.schema.widget === 'date') 71 | type = DateComponent; 72 | else if (this.state.schema.widget === 'upload') 73 | type = UploadComponent; 74 | else if (this.state.schema.widget === 'upload64') 75 | type = UploadComponent; 76 | else if (this.state.schema.type === 'string') 77 | type = InputComponent; 78 | else if (this.state.schema.type === 'number') 79 | type = InputComponent; 80 | else if (this.state.schema.type === 'integer') 81 | type = InputComponent; 82 | else if (this.state.schema.type === 'boolean') 83 | type = BooleanComponent; 84 | else 85 | throw new Error(JSON.stringify(this.state.schema)); 86 | 87 | const componentRef = viewContainerRef.createComponent(type); 88 | componentRef.instance.state = this.state; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "json-schema-form": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "allowedCommonJsDependencies": [ 17 | "jsonata" 18 | ], 19 | "outputPath": "dist/json-schema-form", 20 | "index": "src/index.html", 21 | "main": "src/main.ts", 22 | "polyfills": [ 23 | "zone.js" 24 | ], 25 | "tsConfig": "tsconfig.app.json", 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.css" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "budgets": [ 38 | { 39 | "type": "initial", 40 | "maximumWarning": "2mb", 41 | "maximumError": "10mb" 42 | }, 43 | { 44 | "type": "anyComponentStyle", 45 | "maximumWarning": "2kb", 46 | "maximumError": "4kb" 47 | } 48 | ], 49 | "outputHashing": "all" 50 | }, 51 | "development": { 52 | "buildOptimizer": false, 53 | "optimization": false, 54 | "vendorChunk": true, 55 | "extractLicenses": false, 56 | "sourceMap": true, 57 | "namedChunks": true 58 | } 59 | }, 60 | "defaultConfiguration": "production" 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "configurations": { 65 | "production": { 66 | "browserTarget": "json-schema-form:build:production" 67 | }, 68 | "development": { 69 | "browserTarget": "json-schema-form:build:development" 70 | } 71 | }, 72 | "defaultConfiguration": "development" 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "json-schema-form:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "polyfills": [ 84 | "zone.js", 85 | "zone.js/testing" 86 | ], 87 | "tsConfig": "tsconfig.spec.json", 88 | "assets": [ 89 | "src/favicon.ico", 90 | "src/assets" 91 | ], 92 | "styles": [ 93 | "src/styles.css" 94 | ], 95 | "scripts": [] 96 | } 97 | } 98 | } 99 | }, 100 | "@dashjoin/json-schema-form": { 101 | "projectType": "library", 102 | "root": "projects/dashjoin/json-schema-form", 103 | "sourceRoot": "projects/dashjoin/json-schema-form/src", 104 | "prefix": "lib", 105 | "architect": { 106 | "build": { 107 | "builder": "@angular-devkit/build-angular:ng-packagr", 108 | "options": { 109 | "project": "projects/dashjoin/json-schema-form/ng-package.json" 110 | }, 111 | "configurations": { 112 | "production": { 113 | "tsConfig": "projects/dashjoin/json-schema-form/tsconfig.lib.prod.json" 114 | }, 115 | "development": { 116 | "tsConfig": "projects/dashjoin/json-schema-form/tsconfig.lib.json" 117 | } 118 | }, 119 | "defaultConfiguration": "production" 120 | }, 121 | "test": { 122 | "builder": "@angular-devkit/build-angular:karma", 123 | "options": { 124 | "tsConfig": "projects/dashjoin/json-schema-form/tsconfig.spec.json", 125 | "polyfills": [ 126 | "zone.js", 127 | "zone.js/testing" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "cli": { 135 | "analytics": "199125d7-2e2e-4a3e-b53e-8a3696733c76" 136 | } 137 | } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | JSON Schema Form Playground 2 | 3 | 4 | 5 | JSON Schema and Data 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | 25 | 28 | 29 |
Value (Changes are applied to the form onBlur)Schema (Changes are applied to the form onBlur)
15 | 16 | 18 | 19 |
23 | {{errorV}} 24 | 26 | {{errorS}} 27 |
30 |
31 | 32 |
33 | 34 | 35 | 36 | Examples (Press to select sample data and schema) 37 | 38 | 39 | 40 | 41 | 44 | 52 | 53 | 54 | 57 | 74 | 75 | 76 | 79 | 104 | 105 | 106 | 109 | 116 | 117 | 118 | 121 | 135 | 136 | 137 | 140 | 157 | 158 | 159 | 162 | 169 | 170 |
42 | Types: 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
55 | Widgets: 56 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
77 | JSON schema constructs: 78 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
107 | Logic: 108 | 110 | 113 | 114 | 115 |
119 | Autocomplete: 120 | 122 |   123 | data  124 |   125 | data  126 | 127 | 128 | 129 | 134 |
138 | Layout: 139 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 156 |
160 | Complex: 161 | 163 | 164 | 168 |
171 |
172 | 173 |
174 | 175 | 176 | 177 | {{description}} 178 | 179 | {{error === undefined ? "Form is valid" : "Form is invalid: "+error}} 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * simplified version of the JSON Meta Schema. 3 | * This includes the optional definition keywords that instruct the 4 | * form editor as to which input widgets are to be used and as to 5 | * how the widget layout is to be performed 6 | */ 7 | export interface Schema { 8 | 9 | /** 10 | * schema property type 11 | */ 12 | type?: 'boolean' | 'string' | 'array' | 'number' | 'integer' | 'object'; 13 | 14 | /** 15 | * schema property reference 16 | */ 17 | '$ref'?: string; 18 | 19 | /** 20 | * referenced schemas can be embedded here, this key must be schema.$id 21 | */ 22 | referenced?: { [key: string]: any }; 23 | 24 | /** 25 | * fixed property value range. 26 | * if set, the editor uses a select element 27 | */ 28 | enum?: (string | null)[]; 29 | 30 | /** 31 | * indicates that the property is required (i.e. must be non null) 32 | * https://json-schema.org/understanding-json-schema/reference/object.html#required 33 | */ 34 | required?: string[]; 35 | 36 | /** 37 | * string pattern 38 | * https://json-schema.org/understanding-json-schema/reference/regular_expressions.html 39 | */ 40 | pattern?: string; 41 | 42 | /** 43 | * choose a pattern based on pre-defined identifiers such as email etc. 44 | */ 45 | format?: string; 46 | 47 | /** 48 | * Pattern to parse and serialize date strings. Bases on https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html 49 | * but supports only year (yyyy), month (MM), and day (dd), Only applies to "widget: date" 50 | */ 51 | dateFormat?: string; 52 | 53 | /** 54 | * input must be multiple of x 55 | */ 56 | multipleOf?: number; 57 | 58 | /** 59 | * number < > <= >= 60 | */ 61 | maximum?: number; 62 | 63 | /** 64 | * number < > <= >= 65 | */ 66 | minimum?: number; 67 | 68 | /** 69 | * number < > <= >= 70 | */ 71 | exclusiveMaximum?: number; 72 | 73 | /** 74 | * number < > <= >= 75 | */ 76 | exclusiveMinimum?: number; 77 | 78 | /** 79 | * string must be shorter / longer than x 80 | */ 81 | maxLength?: number; 82 | 83 | /** 84 | * string must be shorter / longer than x 85 | */ 86 | minLength?: number; 87 | 88 | /** 89 | * restrict array length 90 | */ 91 | maxItems?: number; 92 | 93 | /** 94 | * restrict array length 95 | */ 96 | minItems?: number; 97 | 98 | /** 99 | * makes sure array items are unique 100 | */ 101 | uniqueItems?: boolean; 102 | 103 | /** 104 | * restrict object number of properties 105 | */ 106 | maxProperties?: number; 107 | 108 | /** 109 | * restrict object number of properties 110 | */ 111 | minProperties?: number; 112 | 113 | /** 114 | * field names must match this regular expression 115 | */ 116 | propertyNames?: string; 117 | 118 | /** 119 | * dependencies allows specifying a map of field names to the fields they depend on: 120 | * "dependencies": {"credit_card": ["billing_address"]} states that a credit card 121 | * can only be present if the billing address is present 122 | */ 123 | dependencies?: { [key: string]: string[] }; 124 | 125 | /** 126 | * additional properties (with unknown name) have the following schema 127 | */ 128 | additionalProperties?: Schema; 129 | 130 | /** 131 | * field title 132 | * https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.1 133 | */ 134 | title?: string; 135 | 136 | /** 137 | * field description 138 | * https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.1 139 | */ 140 | description?: string; 141 | 142 | /** 143 | * https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5 144 | */ 145 | examples?: string[]; 146 | 147 | /** 148 | * https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.2 149 | */ 150 | default?: any; 151 | 152 | /** 153 | * https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.4 154 | */ 155 | readOnly?: boolean; 156 | 157 | /** 158 | * like readonly but allows setting a value initially 159 | */ 160 | createOnly?: boolean; 161 | 162 | /** 163 | * defines types that can be ref'ed 164 | */ 165 | definitions?: { [key: string]: Schema }; 166 | 167 | /** 168 | * defines the array element structure if type = array 169 | */ 170 | items?: Schema; 171 | 172 | /** 173 | * defines properties if type = object 174 | */ 175 | properties?: { [key: string]: Schema }; 176 | 177 | 178 | // Extension keywords, meaning those defined outside of this document and its companions, are free to define other behaviors as well 179 | // https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.4.3.1 180 | 181 | /** 182 | * defines this property to be a URI. On the UI it will be displayed as a string. 183 | * For instance, the implementation might decide to use the labelService to display nice names on the UI 184 | */ 185 | uri?: boolean; 186 | 187 | /** 188 | * defines which input widget is used for data display and entry. 189 | * The implementation uses a mix of HTML input properties 190 | * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) 191 | * and native angular widgets such as https://material.angular.io/components/datepicker/overview 192 | */ 193 | widget?: 'select' | 'upload' | 'upload64' | 'date' | 'textarea' | 'password' | 'color' | 194 | 'datetime-local' | 'email' | 'month' | 'tel' | 'time' | 'url' | 'week' | 'custom'; 195 | 196 | /** 197 | * if widget=custom, this fields indicates which entry from the widget registry is to be used 198 | */ 199 | widgetType?: string; 200 | 201 | /** 202 | * style applied to form element 203 | */ 204 | style?: any; 205 | 206 | /** 207 | * class applied to form element 208 | */ 209 | class?: string[]; 210 | 211 | /** 212 | * used in case the select / autocomplete options are gathered from a REST service URL. 213 | * defines the REST service URL. 214 | */ 215 | choicesUrl?: string; 216 | 217 | /** 218 | * used in case the select / autocomplete options are gathered from a REST service URL. 219 | * defines the HTTP verb to use for the REST service URL. The default is POST. 220 | */ 221 | choicesVerb?: string; 222 | 223 | /** 224 | * used in case the select / autocomplete options are gathered from a REST service URL. 225 | * defines the REST service parameter. The convention is to have a single parameter. 226 | * Multiple fields need to be wrapped into a single object 227 | */ 228 | choicesUrlArgs?: any; 229 | 230 | /** 231 | * determines when the request is being made (default is onFocus) 232 | */ 233 | choicesLoad?: 'onFocus' | 'onLoad'; 234 | 235 | /** 236 | * used in case the select / autocomplete options are defined statically 237 | */ 238 | choices?: (string | number)[]; 239 | 240 | /** 241 | * name of the displayWith function 242 | */ 243 | displayWith?: string; 244 | 245 | /** 246 | * alternative to displayWith in case we have static options 247 | */ 248 | displayWithChoices?: string[]; 249 | 250 | /** 251 | * used in case the select / autocomplete options are gathered from a REST service URL. 252 | * used to transform the REST result into a string array or an array of objects with name and value fields 253 | */ 254 | jsonata?: string; 255 | 256 | /** 257 | * input control layout: 258 | * 259 | * horizontal (default): input controls are arranged horizontally and flex-wrap if there is insufficient space 260 | * vertical: input controls are arranged vertically 261 | * tab: controls are shown in tabs (only applies to arrays) 262 | * table: controls are shown in a table with the property names being the column names (only applies to an array of objects) 263 | * select: array is shown as a multi-select (only applies to arrays of string) 264 | */ 265 | layout?: 'tab' | 'table' | 'vertical' | 'horizontal' | 'select' | 'chips'; 266 | 267 | /** 268 | * defines order, omission, and 2-level hierarchy (via nested lists) of the object properties. 269 | * The top level layout is defined as either horizontal or vertical, sublists use the opposite direction 270 | */ 271 | order?: (string | string[])[]; 272 | 273 | /** 274 | * simplified version of conditionals (https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.2). 275 | * works in conjunction with 'case'. switch marks a property whose value determines whether other properties 276 | * are shown or not 277 | */ 278 | switch?: string; 279 | 280 | /** 281 | * show the property if the switch property's value is one of case's values 282 | */ 283 | case?: string[]; 284 | 285 | /** 286 | * indicates whether the component should be located in an expansion panel 287 | */ 288 | expanded?: boolean; 289 | 290 | /** 291 | * hide undefined properties in object layouts 292 | */ 293 | hideUndefined?: boolean; 294 | 295 | /** 296 | * allows defining computed properties that are set when the value changes 297 | */ 298 | computed?: { [key: string]: any }; 299 | 300 | /** 301 | * allows customizing the validation error message 302 | */ 303 | errorMessage?: string; 304 | 305 | /** 306 | * if true, indicates not to show form editor elements 307 | */ 308 | static?: boolean; 309 | 310 | /** 311 | * allows specifying the json schema version 312 | */ 313 | $schema?: string; 314 | } 315 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/src/lib/base/base.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, Input, OnInit, Type, ViewChild } from '@angular/core'; 3 | import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; 4 | import { map, Observable, startWith } from 'rxjs'; 5 | import { Choice } from '../choice'; 6 | import { JsonSchemaFormService } from '../json-schema-form.service'; 7 | import { Schema } from '../schema'; 8 | import { State } from '../state'; 9 | import jsonata from 'jsonata' 10 | import { KeyValue } from '@angular/common'; 11 | 12 | /** 13 | * base component for all form elements 14 | */ 15 | @Component({ 16 | selector: 'app-base', 17 | templateUrl: './base.component.html', 18 | styleUrls: ['./base.component.css'] 19 | }) 20 | export class BaseComponent implements OnInit { 21 | 22 | /** 23 | * built-in formats 24 | */ 25 | static formats: any = { 26 | email: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 27 | ipv4: /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/, 28 | url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/, 29 | uri: /^\w+:(\/?\/?)[^\s]+$/ 30 | }; 31 | 32 | @Input() state!: State; 33 | 34 | choices: Choice[] = [] 35 | 36 | filteredOptions!: Observable; 37 | 38 | constructor(protected http: HttpClient, protected service: JsonSchemaFormService) { } 39 | 40 | ngOnInit() { 41 | 42 | // make sure control matches the schema 43 | if (this.state.schema.type === 'object') { 44 | if (!(this.state.control instanceof FormGroup)) 45 | throw new Error('For object schema, control must be a FormGroup'); 46 | } 47 | else if (this.state.schema.type === 'array' && this.state.schema.layout !== 'select' && this.state.schema.layout !== 'chips') { 48 | if (!(this.state.control instanceof FormArray)) 49 | throw new Error('For array schema, control must be a FormArray'); 50 | } else { 51 | if (!(this.state.control instanceof FormControl)) 52 | throw new Error('For primitive schema, control must be a FormControl'); 53 | } 54 | 55 | // load choices 56 | if (this.state.schema.choicesUrl) { 57 | const request = this.state.schema.choicesVerb === 'GET' ? 58 | this.http.get(this.state.schema.choicesUrl) : 59 | this.http.post(this.state.schema.choicesUrl, {}) 60 | request.subscribe((res: any) => { 61 | if (this.state.schema.jsonata) { 62 | jsonata(this.state.schema.jsonata).evaluate(res).then(res2 => { 63 | this.setChoices(res2 as any) 64 | }) 65 | } 66 | else 67 | this.setChoices(res as any) 68 | }) 69 | } 70 | 71 | if (this.state.schema.enum) { 72 | // translate enum to choices 73 | this.setChoices(this.state.schema.enum) 74 | } 75 | else if (this.state.schema.choices) { 76 | this.setChoices(this.state.schema.choices) 77 | } else if (this.state.value) 78 | // default choice is current value 79 | this.setChoices([this.state.value]) 80 | 81 | // set name to title if set 82 | if (this.state.schema.title) 83 | this.state.name = this.state.schema.title 84 | } 85 | 86 | setChoices(choices: any[]) { 87 | this.choices = [] 88 | for (const choice of choices) 89 | if (choice?.name && choice?.value) 90 | this.choices.push(choice) 91 | else 92 | this.choices.push({ 93 | value: choice, 94 | name: choice ? choice : '' 95 | }) 96 | this.filteredOptions = this.state.control.valueChanges.pipe( 97 | startWith(''), 98 | map(value => this._filter(value || '')), 99 | ); 100 | } 101 | 102 | _filter(value: string): Choice[] { 103 | const filterValue = value.toLowerCase(); 104 | return this.choices.filter(option => option.name.toLowerCase().includes(filterValue)); 105 | } 106 | 107 | /** 108 | * cast control to primitive type 109 | */ 110 | formControl(): FormControl { 111 | return this.state.control as FormControl; 112 | } 113 | 114 | /** 115 | * case control to group / object 116 | */ 117 | formGroup(): FormGroup { 118 | return this.state.control as FormGroup; 119 | } 120 | 121 | /** 122 | * case control to array 123 | */ 124 | formArray(): FormArray { 125 | return this.state.control as FormArray; 126 | } 127 | 128 | /** 129 | * create control and pass value to it 130 | */ 131 | static createControl(schema: Schema, value: any, required: boolean): AbstractControl { 132 | if (schema.type === 'object') { 133 | return new FormGroup({}); 134 | } 135 | if (schema.type === 'array' && schema.layout !== 'select' && schema.layout !== 'chips') { 136 | return new FormArray([]); 137 | } 138 | 139 | const control = new FormControl(); 140 | this.prepareControl(control, schema, value, required) 141 | return control 142 | } 143 | 144 | static prepareControl(control: AbstractControl, schema: Schema, value: any, required: boolean) { 145 | // handle default 146 | if (schema.default) 147 | if (!value) 148 | value = schema.default 149 | 150 | if (schema.type !== 'object' && schema.type !== 'array') 151 | control.setValue(value) 152 | 153 | if (required) 154 | control.addValidators(Validators.required) 155 | 156 | if (schema.pattern) 157 | control.addValidators(Validators.pattern(schema.pattern)) 158 | 159 | if (schema.format) 160 | control.addValidators(Validators.pattern(this.formats[schema.format])) 161 | 162 | if (schema.maxLength) 163 | control.addValidators(Validators.maxLength(schema.maxLength)) 164 | 165 | if (schema.minLength) 166 | control.addValidators(Validators.minLength(schema.minLength)) 167 | 168 | if (schema.multipleOf) 169 | control.addValidators(multipleOf(schema.multipleOf)) 170 | 171 | if (schema.maximum) 172 | control.addValidators(Validators.max(schema.maximum)) 173 | 174 | if (schema.minimum) 175 | control.addValidators(Validators.min(schema.minimum)) 176 | 177 | if (schema.exclusiveMaximum) 178 | control.addValidators(Validators.max(schema.exclusiveMaximum - 1)) 179 | 180 | if (schema.exclusiveMinimum) 181 | control.addValidators(Validators.min(schema.exclusiveMinimum + 1)) 182 | 183 | if (schema.maxItems) 184 | control.addValidators(maxItems(schema.maxItems)) 185 | 186 | if (schema.minItems) 187 | control.addValidators(minItems(schema.minItems)) 188 | 189 | if (schema.maxProperties) 190 | control.addValidators(maxProperties(schema.maxProperties)) 191 | 192 | if (schema.minProperties) 193 | control.addValidators(minProperties(schema.minProperties)) 194 | 195 | if (schema.uniqueItems) 196 | control.addValidators(uniqueItems()) 197 | 198 | if (schema.propertyNames) 199 | control.addValidators(propertyNames(schema.propertyNames)) 200 | 201 | if (schema.dependencies) 202 | control.addValidators(dependencies(schema.dependencies)) 203 | 204 | if (schema.readOnly) 205 | control.disable() 206 | 207 | if (schema.createOnly && value) 208 | control.disable() 209 | 210 | return control 211 | } 212 | 213 | /** 214 | * angular pipe sorting function for keyValue - keep the JSON order and do not 215 | * order alphabetically 216 | */ 217 | originalOrder = (a: KeyValue, b: KeyValue): number => { 218 | return 0; 219 | } 220 | } 221 | 222 | function multipleOf(multiple: number): ValidatorFn { 223 | return (control: AbstractControl): ValidationErrors | null => { 224 | if (control.value % multiple === 0) 225 | return null 226 | else 227 | return { multipleOf: { value: control.value } } 228 | }; 229 | } 230 | 231 | function maxItems(max: number): ValidatorFn { 232 | return (control: AbstractControl): ValidationErrors | null => { 233 | if (control.value.length <= max) 234 | return null 235 | else 236 | return { maxItems: { value: control.value } } 237 | }; 238 | } 239 | 240 | function minItems(min: number): ValidatorFn { 241 | return (control: AbstractControl): ValidationErrors | null => { 242 | if (control.value.length >= min) 243 | return null 244 | else 245 | return { minItems: { value: control.value } } 246 | }; 247 | } 248 | 249 | function maxProperties(max: number): ValidatorFn { 250 | return (control: AbstractControl): ValidationErrors | null => { 251 | if (Object.keys(control.value).length <= max) 252 | return null 253 | else 254 | return { maxProperties: { value: control.value } } 255 | }; 256 | } 257 | 258 | function minProperties(min: number): ValidatorFn { 259 | return (control: AbstractControl): ValidationErrors | null => { 260 | if (Object.keys(control.value).length >= min) 261 | return null 262 | else 263 | return { minProperties: { value: control.value } } 264 | }; 265 | } 266 | 267 | function uniqueItems(): ValidatorFn { 268 | return (control: AbstractControl): ValidationErrors | null => { 269 | const unique: any = [] 270 | for (const i of control.value) 271 | if (!unique.includes(i)) 272 | unique.push(i) 273 | if (unique.length === control.value.length) 274 | return null 275 | else 276 | return { minItems: { value: control.value } } 277 | }; 278 | } 279 | 280 | function propertyNames(pattern: string): ValidatorFn { 281 | return (control: AbstractControl): ValidationErrors | null => { 282 | for (const key of Object.keys(control.value)) { 283 | const re = new RegExp(pattern); 284 | if (!re.test(key)) 285 | return { propertyNames: { value: control.value } } 286 | }; 287 | return null 288 | } 289 | } 290 | 291 | function dependencies(map: any): ValidatorFn { 292 | return (control: AbstractControl): ValidationErrors | null => { 293 | for (const [k, v] of Object.entries(map)) 294 | if (control.value[k]) 295 | for (const i of v as any) 296 | if (!control.value[i]) 297 | return { propertyNames: { value: control.value } } 298 | return null 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Lightweight Angular JSON Schema Form Component 2 | 3 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dashjoin_json-schema-form&metric=alert_status)](https://sonarcloud.io/dashboard?id=dashjoin_json-schema-form) 4 | [![npm version](https://img.shields.io/npm/v/@dashjoin/json-schema-form.svg?style=flat-square)](https://www.npmjs.com/package/@dashjoin/json-schema-form) 5 | 6 | ![](https://raw.github.com/jdorn/json-editor/master/jsoneditor.png) 7 | 8 | ## Goal 9 | 10 | * Implement any web form with flexible styling and validation in a completely declarative way 11 | * Live demo: https://dashjoin.github.io/ 12 | * Stackblitz: https://stackblitz.com/edit/dashjoin 13 | * [Video Tutorial](https://www.youtube.com/watch?v=Xk9dxbbBFjo) 14 | 15 | ## Features 16 | 17 | * Supports JSON Schema Draft 6 18 | * Can load referenced schemas from URLs 19 | * Renders compact forms 20 | * Supports 2-way databinding 21 | * Autocomplete & typeahead based on REST services (complex responses can be processed via extended JSONata) 22 | * CSS styling 23 | * Built-in validation 24 | * Flexible layout options (tab, table, vertical, horizontal, ...) 25 | * Several input widgets (file upload, date / color picker, autocomplete, ...) 26 | * Lightweight: < 1000 lines of code 27 | 28 | ## Installation 29 | 30 | To use the library in your project, follow these steps: 31 | 32 | ```shell 33 | npm i @dashjoin/json-schema-form 34 | npm i @angular/material 35 | npm i jsonata 36 | ``` 37 | 38 | In your app module add: 39 | 40 | ```typescript 41 | import { BrowserModule } from '@angular/platform-browser'; 42 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 43 | import { JsonSchemaFormModule } from '@dashjoin/json-schema-form'; 44 | ... 45 | 46 | @NgModule({ 47 | ... 48 | imports: [ 49 | BrowserModule, 50 | BrowserAnimationsModule, 51 | JsonSchemaFormModule, 52 | ... 53 | ], 54 | ... 55 | } 56 | ``` 57 | 58 | A small sample component: 59 | 60 | ```typescript 61 | import { Component, OnInit } from '@angular/core'; 62 | import { State } from '@dashjoin/json-schema-form'; 63 | import { FormArray } from '@angular/forms'; 64 | 65 | @Component({ 66 | selector: 'app-root', 67 | template: ` 68 | 69 | ` 70 | }) 71 | export class AppComponent implements OnInit { 72 | 73 | state: State = { 74 | schema: { 75 | type: 'array', 76 | items: { 77 | type: 'object', 78 | properties: { 79 | name: { type: 'string' }, 80 | bday: { type: 'string', widget: 'date' } 81 | } 82 | } 83 | }, 84 | value: [{ 85 | name: 'Joe', 86 | bday: '2018-09-09T22:00:00.000Z' 87 | }], 88 | name: 'myform', 89 | 90 | // pick FormArray, FormGroup or FormControl for arrays, objects, or single values respectively 91 | control: new FormArray([]) 92 | }; 93 | 94 | ngOnInit(): void { 95 | // subscribe to form value change / validation or state events 96 | this.state.control.valueChanges.subscribe(res => { 97 | console.log(res); 98 | }) 99 | } 100 | } 101 | ``` 102 | 103 | Finally, add the material style and icons to styles.css: 104 | 105 | ```css 106 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 107 | @import "https://fonts.googleapis.com/icon?family=Material+Icons"; 108 | ``` 109 | 110 | ## JSON Schema Extensions 111 | 112 | We define a couple of extensions to JSON Schema in order to define the user interface and layout of the form. Please also see the [demo playground](https://dashjoin.github.io/) where examples of all configuration options are available. 113 | 114 | ### Widget 115 | 116 | This option specifies a specific input widget to be used. The default is a simple text field. The following options are available: 117 | 118 | ``` 119 | { 120 | "type": "string", 121 | "widget": "date" 122 | } 123 | ``` 124 | 125 | * select: shows a select input field with options (No free text entry is possible. Options can be loaded via rest (see below)) 126 | * upload: the JSON property is set to the contents of an uploaded file 127 | * date: uses the material date picker component 128 | * textarea: displays a multi line textarea 129 | * password: input is shown as ***** 130 | * color: shows a color picker widget 131 | * datetime-local, email, month, tel, time, url, week: uses the browser native [input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) 132 | 133 | ### Custom Widgets 134 | 135 | It is possible to create custom widgets using the following steps: 136 | 137 | * Create a component that extends [BaseComponent](https://github.com/dashjoin/json-schema-form/blob/master/projects/dashjoin/json-schema-form/src/lib/base/base.component.ts). All relevant data such as the applicable subschema and the current value are passed to the component. Make sure to emit value changes via state.control. An example can be found [here](https://github.com/dashjoin/json-schema-form/blob/master/src/app/custom/custom.component.ts) 138 | * Include the component in your @NgModule declarations 139 | * In the parent component, add this service to your constructor: private service: JsonSchemaFormService 140 | * Register your widget in ngOnInit() using this service: this.service.registerComponent('rich-text-editor', CustomComponent); 141 | * Include the widget in your schema: { "widget": "custom", "widgetType": "rich-text-editor" } 142 | 143 | ### Autocomplete choices 144 | 145 | The following fields control how select and autocomplete options are obtained from a REST backend: 146 | 147 | ``` 148 | { 149 | "type": "string", 150 | "choicesUrl": "/assets/autocomplete-simple.json", 151 | "choicesVerb": "GET" 152 | } 153 | ``` 154 | 155 | * choices: string array that allows defining the choices statically 156 | * choicesUrl: defines the REST service URL 157 | * choicesVerb: defines the HTTP verb to use for the REST service URL, default is POST 158 | * choicesUrlArgs: defines the REST service parameter. The convention is to have a single parameter. Multiple fields need to be wrapped into a single object 159 | * jsonata: used to transform the REST result into a string array or an array of objects with name and value fields if it is not already in that form. 160 | The transformation is expressed using [JSONata](https://jsonata.org/) 161 | * choicesLoad: determines whether the choices are loaded upon page load (onLoad) or upon focus (onFocus), which is the default 162 | 163 | ### Autocomplete and Select Display Names and Values 164 | 165 | If you want the option's control value (what is saved in the form) to be different than the option's display value (what is displayed in the text field), 166 | the "displayWith" option allows you to do so. The value of "displayWith" is the name under which the implementation class to perform this job was registered. 167 | The class must implement the [ChoiceHandler](https://github.com/dashjoin/json-schema-form/blob/master/projects/dashjoin/json-schema-form/src/lib/choice.ts) interface. An example can be found at the end of the [playground component](https://github.com/dashjoin/json-schema-form/blob/master/src/app/app.component.ts). 168 | The registration can be done in ngOnInit() using this service: this.service.registerDisplayWith('states', new MyDisplayer()); Consider the following example: 169 | 170 | ``` 171 | { 172 | "type": "string", 173 | "displayWith": "localName", 174 | "choices": [ 175 | "https://en.wikipedia.org/wiki/Indonesia", 176 | "https://en.wikipedia.org/wiki/Peru", 177 | "As is - no tooltip" 178 | ] 179 | } 180 | ``` 181 | 182 | The autocomplete is configured with "localName" which is a built-in displayer. 183 | It treats options like URLs and displays the local name which is the text after the last slash, hash, colon or dot. This causes the dropdown to display "Peru" with the tooltip indicating the real value "https://en.wikipedia.org/wiki/Peru" which is written to the JSON value. 184 | 185 | The custom implementation also enables you to exercise tight control over filtering, typeahead loading of options, and determining the display value. 186 | For an example of a typeahead implementation, see the class MyTypeAhead at the bottom of the [playground component](https://github.com/dashjoin/json-schema-form/blob/master/src/app/app.component.ts). 187 | 188 | ### Layout options 189 | 190 | Layout options determine how the input elements of arrays and objects are arranged. These options can be applied for each nesting layer (e.g. if you're entering an array of objects): 191 | 192 | ``` 193 | { 194 | "type": "array", 195 | "layout": "horizontal", 196 | "items": { 197 | "type": "object", 198 | "layout": "vertical", 199 | "properties": { 200 | "name": { 201 | "type": "string" 202 | }, 203 | "version": { 204 | "type": "number" 205 | } 206 | } 207 | } 208 | } 209 | ``` 210 | 211 | * horizontal (default): input controls are arranged horizontally and flex-wrap if there is insufficient space 212 | * vertical: input controls are arranged vertically 213 | * tab: controls are shown in tabs (only applies to arrays and objects with additionalProperties) 214 | * table: controls are shown in a table with the property names being the column names (only applies to an array of objects) 215 | * select: array is shown as a multi-select (only applies to arrays of string) 216 | * Any element can be placed in an expansion panel by adding "expanded": true / false. The Boolean value indicates whether the panel is expanded by default or not 217 | 218 | The order field allows to control the inputs of objects: 219 | 220 | * The order field can be a list of field names. For example "order": ["firstname", "lastname"] defines the first name input to appear before the last name, regardless of their order in the properties 221 | * If a property is omitted, the form does not display an input. So in the example above, an age field is not in the form even if it is listed in properties. 222 | * Order can also specify a 2-level hierarchy like "order": [["firstname", "lastname"], "emails"]. If a vertical layout is chosen, this displays firstname and lastname in the first row and the array of emails in the second row. The first row automatically chooses the opposite layout direction internally. 223 | 224 | The style and class fields allow passing CSS styles and classes to the input fields. For instance, you could emphasize 225 | the input with a higher z elevation and accommodate for longer 226 | input values by increasing the default input element width: 227 | 228 | ``` 229 | { 230 | "type": "string", 231 | "class": [ 232 | "mat-elevation-z2" 233 | ], 234 | "style": { 235 | "width": "400px" 236 | } 237 | } 238 | ``` 239 | 240 | Please also see the definition of the [Schema](https://github.com/dashjoin/json-schema-form/blob/master/projects/dashjoin/json-schema-form/src/lib/schema.ts) object. 241 | 242 | ### Application Logic 243 | 244 | In some situations, you would like to compute a field based on the contents of other fields. 245 | This can be achieved via the "compute" option. It can be placed within an object as follows: 246 | 247 | ``` 248 | { 249 | "type": "object", 250 | "properties": { "first": {"type": "string"}, "last": { "type": "string" }, "salutation": { "type": "string", "readOnly": true } }, 251 | "computed": { 252 | "salutation": '"Dear " & first & " " & last & "," & $context("var")' 253 | } 254 | } 255 | ``` 256 | 257 | In this example, any change to the first or last fields trigger a change in salutation which is displayed as a read only form field. 258 | The expression defining the salutation value is expressed in JSONata (). 259 | The custom function $context allows the host application to reference data which was set via this.service.setContext(key, value). 260 | 261 | ## Validation and Submitting 262 | 263 | Some JSON Schema constructs like "pattern" or "required" allow validating an object against the schema. 264 | The result of this validation is displayed on the UI but it is also propagated to the parent component 265 | via the "error" output variable. Error contains the first validation error message or null if the form is 266 | valid. The following example shows how this information can be used to deactivate form submission: 267 | 268 | ``` 269 | 270 | 271 | 272 | ``` 273 | 274 | Note that not all JSON schema validation constructs are supported. Also, arrays and 275 | additional property objects do not propagate the information and the invalid value is undefined. 276 | 277 | ## Unsupported JSON Schema properties 278 | 279 | We support JSON Schema Draft 6 with these exceptions: 280 | 281 | * patternProperties: allows defining a property type depending on the property name. You can work around this using additionalProperties. 282 | * const: allows defining a value to be constant. Work around this using default and /or enum with a single option. 283 | * Combining schemas (oneOf, anyOf, not, allOf): this allows giving multiple options (schemas) for a property. These constructs make a lot of sense for validation but are hard to apply in the context of a form and therefore, they are not supported. 284 | * contains: specifies that an array must contain one instance of a given type. As with the schema combination constructs, this makes sense for validation for not for forms. 285 | 286 | ## Referenced Schemas 287 | 288 | In order to foster reuse, schemas are often made available on the web. In this case, you can use JSON schema's $ref mechanism to have the browser load the schema as follows: 289 | 290 | ``` 291 | 292 | ``` 293 | 294 | The URL can also be relative to the form's URL: 295 | 296 | ``` 297 | 298 | ``` 299 | 300 | If you do not want the schema to be downloaded, you can also manually provide referenced schemas via the root schema: 301 | 302 | ``` 303 | { 304 | ... 305 | referenced: { 306 | 'http://example.org/': { $id: 'http://example.org/', ... }, 307 | 'urn:myschema': { $id: 'urn:myschema', ... }, 308 | } 309 | } 310 | ``` 311 | 312 | ## Structure of this repository 313 | 314 | The repository contains: 315 | 316 | * [The actual library code](https://github.com/dashjoin/json-schema-form/tree/master/projects/dashjoin/json-schema-form/src/lib) 317 | * [Sources of the online demo playground](https://github.com/dashjoin/json-schema-form/tree/master/src/app) 318 | 319 | ## Contribute 320 | 321 | We welcome contributions. If you are interested in contributing to Dashjoin, let us know! 322 | You'll get to know an open-minded and motivated team working together to build the next generation platform. 323 | 324 | * [Join our Slack](https://join.slack.com/t/dashjoin/shared_invite/zt-1274qbzq9-mwxBq4WwSTJsITjrvYV4pA) and say hello 325 | * [Follow us](https://twitter.com/dashjoin) on Twitter 326 | * [Submit](https://github.com/dashjoin/json-schema-form/issues) your ideas by opening an issue with the enhancement label 327 | * [Help out](https://github.com/dashjoin/json-schema-form/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) by fixing "a good first issue" 328 | -------------------------------------------------------------------------------- /projects/dashjoin/json-schema-form/README.md: -------------------------------------------------------------------------------- 1 | # A Lightweight Angular JSON Schema Form Component 2 | 3 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dashjoin_json-schema-form&metric=alert_status)](https://sonarcloud.io/dashboard?id=dashjoin_json-schema-form) 4 | [![npm version](https://img.shields.io/npm/v/@dashjoin/json-schema-form.svg?style=flat-square)](https://www.npmjs.com/package/@dashjoin/json-schema-form) 5 | 6 | ![](https://raw.github.com/jdorn/json-editor/master/jsoneditor.png) 7 | 8 | ## Goal 9 | 10 | * Implement any web form with flexible styling and validation in a completely declarative way 11 | * Live demo: https://dashjoin.github.io/ 12 | * Stackblitz: https://stackblitz.com/edit/dashjoin 13 | * [Video Tutorial](https://www.youtube.com/watch?v=Xk9dxbbBFjo) 14 | 15 | ## Features 16 | 17 | * Supports JSON Schema Draft 6 18 | * Can load referenced schemas from URLs 19 | * Renders compact forms 20 | * Supports 2-way databinding 21 | * Autocomplete & typeahead based on REST services (complex responses can be processed via extended JSONata) 22 | * CSS styling 23 | * Built-in validation 24 | * Flexible layout options (tab, table, vertical, horizontal, ...) 25 | * Several input widgets (file upload, date / color picker, autocomplete, ...) 26 | * Lightweight: < 1000 lines of code 27 | 28 | ## Installation 29 | 30 | To use the library in your project, follow these steps: 31 | 32 | ```shell 33 | npm i @dashjoin/json-schema-form 34 | npm i @angular/material 35 | npm i jsonata 36 | ``` 37 | 38 | In your app module add: 39 | 40 | ```typescript 41 | import { BrowserModule } from '@angular/platform-browser'; 42 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 43 | import { JsonSchemaFormModule } from '@dashjoin/json-schema-form'; 44 | ... 45 | 46 | @NgModule({ 47 | ... 48 | imports: [ 49 | BrowserModule, 50 | BrowserAnimationsModule, 51 | JsonSchemaFormModule, 52 | ... 53 | ], 54 | ... 55 | } 56 | ``` 57 | 58 | A small sample component: 59 | 60 | ```typescript 61 | import { Component, OnInit } from '@angular/core'; 62 | import { State } from '@dashjoin/json-schema-form'; 63 | import { FormArray } from '@angular/forms'; 64 | 65 | @Component({ 66 | selector: 'app-root', 67 | template: ` 68 | 69 | ` 70 | }) 71 | export class AppComponent implements OnInit { 72 | 73 | state: State = { 74 | schema: { 75 | type: 'array', 76 | items: { 77 | type: 'object', 78 | properties: { 79 | name: { type: 'string' }, 80 | bday: { type: 'string', widget: 'date' } 81 | } 82 | } 83 | }, 84 | value: [{ 85 | name: 'Joe', 86 | bday: '2018-09-09T22:00:00.000Z' 87 | }], 88 | name: 'myform', 89 | 90 | // pick FormArray, FormGroup or FormControl for arrays, objects, or single values respectively 91 | control: new FormArray([]) 92 | }; 93 | 94 | ngOnInit(): void { 95 | // subscribe to form value change / validation or state events 96 | this.state.control.valueChanges.subscribe(res => { 97 | console.log(res); 98 | }) 99 | } 100 | } 101 | ``` 102 | 103 | Finally, add the material style and icons to styles.css: 104 | 105 | ```css 106 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 107 | @import "https://fonts.googleapis.com/icon?family=Material+Icons"; 108 | ``` 109 | 110 | ## JSON Schema Extensions 111 | 112 | We define a couple of extensions to JSON Schema in order to define the user interface and layout of the form. Please also see the [demo playground](https://dashjoin.github.io/) where examples of all configuration options are available. 113 | 114 | ### Widget 115 | 116 | This option specifies a specific input widget to be used. The default is a simple text field. The following options are available: 117 | 118 | ``` 119 | { 120 | "type": "string", 121 | "widget": "date" 122 | } 123 | ``` 124 | 125 | * select: shows a select input field with options (No free text entry is possible. Options can be loaded via rest (see below)) 126 | * upload: the JSON property is set to the contents of an uploaded file 127 | * date: uses the material date picker component 128 | * textarea: displays a multi line textarea 129 | * password: input is shown as ***** 130 | * color: shows a color picker widget 131 | * datetime-local, email, month, tel, time, url, week: uses the browser native [input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) 132 | 133 | ### Custom Widgets 134 | 135 | It is possible to create custom widgets using the following steps: 136 | 137 | * Create a component that extends [BaseComponent](https://github.com/dashjoin/json-schema-form/blob/master/projects/dashjoin/json-schema-form/src/lib/base/base.component.ts). All relevant data such as the applicable subschema and the current value are passed to the component. Make sure to emit value changes via state.control. An example can be found [here](https://github.com/dashjoin/json-schema-form/blob/master/src/app/custom/custom.component.ts) 138 | * Include the component in your @NgModule declarations 139 | * In the parent component, add this service to your constructor: private service: JsonSchemaFormService 140 | * Register your widget in ngOnInit() using this service: this.service.registerComponent('rich-text-editor', CustomComponent); 141 | * Include the widget in your schema: { "widget": "custom", "widgetType": "rich-text-editor" } 142 | 143 | ### Autocomplete choices 144 | 145 | The following fields control how select and autocomplete options are obtained from a REST backend: 146 | 147 | ``` 148 | { 149 | "type": "string", 150 | "choicesUrl": "/assets/autocomplete-simple.json", 151 | "choicesVerb": "GET" 152 | } 153 | ``` 154 | 155 | * choices: string array that allows defining the choices statically 156 | * choicesUrl: defines the REST service URL 157 | * choicesVerb: defines the HTTP verb to use for the REST service URL, default is POST 158 | * choicesUrlArgs: defines the REST service parameter. The convention is to have a single parameter. Multiple fields need to be wrapped into a single object 159 | * jsonata: used to transform the REST result into a string array or an array of objects with name and value fields if it is not already in that form. 160 | The transformation is expressed using [JSONata](https://jsonata.org/) 161 | * choicesLoad: determines whether the choices are loaded upon page load (onLoad) or upon focus (onFocus), which is the default 162 | 163 | ### Autocomplete and Select Display Names and Values 164 | 165 | If you want the option's control value (what is saved in the form) to be different than the option's display value (what is displayed in the text field), 166 | the "displayWith" option allows you to do so. The value of "displayWith" is the name under which the implementation class to perform this job was registered. 167 | The class must implement the [ChoiceHandler](https://github.com/dashjoin/json-schema-form/blob/master/projects/dashjoin/json-schema-form/src/lib/choice.ts) interface. An example can be found at the end of the [playground component](https://github.com/dashjoin/json-schema-form/blob/master/src/app/app.component.ts). 168 | The registration can be done in ngOnInit() using this service: this.service.registerDisplayWith('states', new MyDisplayer()); Consider the following example: 169 | 170 | ``` 171 | { 172 | "type": "string", 173 | "displayWith": "localName", 174 | "choices": [ 175 | "https://en.wikipedia.org/wiki/Indonesia", 176 | "https://en.wikipedia.org/wiki/Peru", 177 | "As is - no tooltip" 178 | ] 179 | } 180 | ``` 181 | 182 | The autocomplete is configured with "localName" which is a built-in displayer. 183 | It treats options like URLs and displays the local name which is the text after the last slash, hash, colon or dot. This causes the dropdown to display "Peru" with the tooltip indicating the real value "https://en.wikipedia.org/wiki/Peru" which is written to the JSON value. 184 | 185 | The custom implementation also enables you to exercise tight control over filtering, typeahead loading of options, and determining the display value. 186 | For an example of a typeahead implementation, see the class MyTypeAhead at the bottom of the [playground component](https://github.com/dashjoin/json-schema-form/blob/master/src/app/app.component.ts). 187 | 188 | ### Layout options 189 | 190 | Layout options determine how the input elements of arrays and objects are arranged. These options can be applied for each nesting layer (e.g. if you're entering an array of objects): 191 | 192 | ``` 193 | { 194 | "type": "array", 195 | "layout": "horizontal", 196 | "items": { 197 | "type": "object", 198 | "layout": "vertical", 199 | "properties": { 200 | "name": { 201 | "type": "string" 202 | }, 203 | "version": { 204 | "type": "number" 205 | } 206 | } 207 | } 208 | } 209 | ``` 210 | 211 | * horizontal (default): input controls are arranged horizontally and flex-wrap if there is insufficient space 212 | * vertical: input controls are arranged vertically 213 | * tab: controls are shown in tabs (only applies to arrays and objects with additionalProperties) 214 | * table: controls are shown in a table with the property names being the column names (only applies to an array of objects) 215 | * select: array is shown as a multi-select (only applies to arrays of string) 216 | * Any element can be placed in an expansion panel by adding "expanded": true / false. The Boolean value indicates whether the panel is expanded by default or not 217 | 218 | The order field allows to control the inputs of objects: 219 | 220 | * The order field can be a list of field names. For example "order": ["firstname", "lastname"] defines the first name input to appear before the last name, regardless of their order in the properties 221 | * If a property is omitted, the form does not display an input. So in the example above, an age field is not in the form even if it is listed in properties. 222 | * Order can also specify a 2-level hierarchy like "order": [["firstname", "lastname"], "emails"]. If a vertical layout is chosen, this displays firstname and lastname in the first row and the array of emails in the second row. The first row automatically chooses the opposite layout direction internally. 223 | 224 | The style and class fields allow passing CSS styles and classes to the input fields. For instance, you could emphasize 225 | the input with a higher z elevation and accommodate for longer 226 | input values by increasing the default input element width: 227 | 228 | ``` 229 | { 230 | "type": "string", 231 | "class": [ 232 | "mat-elevation-z2" 233 | ], 234 | "style": { 235 | "width": "400px" 236 | } 237 | } 238 | ``` 239 | 240 | Please also see the definition of the [Schema](https://github.com/dashjoin/json-schema-form/blob/master/projects/dashjoin/json-schema-form/src/lib/schema.ts) object. 241 | 242 | ### Application Logic 243 | 244 | In some situations, you would like to compute a field based on the contents of other fields. 245 | This can be achieved via the "compute" option. It can be placed within an object as follows: 246 | 247 | ``` 248 | { 249 | "type": "object", 250 | "properties": { "first": {"type": "string"}, "last": { "type": "string" }, "salutation": { "type": "string", "readOnly": true } }, 251 | "computed": { 252 | "salutation": '"Dear " & first & " " & last & "," & $context("var")' 253 | } 254 | } 255 | ``` 256 | 257 | In this example, any change to the first or last fields trigger a change in salutation which is displayed as a read only form field. 258 | The expression defining the salutation value is expressed in JSONata (). 259 | The custom function $context allows the host application to reference data which was set via this.service.setContext(key, value). 260 | 261 | ## Validation and Submitting 262 | 263 | Some JSON Schema constructs like "pattern" or "required" allow validating an object against the schema. 264 | The result of this validation is displayed on the UI but it is also propagated to the parent component 265 | via the "error" output variable. Error contains the first validation error message or null if the form is 266 | valid. The following example shows how this information can be used to deactivate form submission: 267 | 268 | ``` 269 | 270 | 271 | 272 | ``` 273 | 274 | Note that not all JSON schema validation constructs are supported. Also, arrays and 275 | additional property objects do not propagate the information and the invalid value is undefined. 276 | 277 | ## Unsupported JSON Schema properties 278 | 279 | We support JSON Schema Draft 6 with these exceptions: 280 | 281 | * patternProperties: allows defining a property type depending on the property name. You can work around this using additionalProperties. 282 | * const: allows defining a value to be constant. Work around this using default and /or enum with a single option. 283 | * Combining schemas (oneOf, anyOf, not, allOf): this allows giving multiple options (schemas) for a property. These constructs make a lot of sense for validation but are hard to apply in the context of a form and therefore, they are not supported. 284 | * contains: specifies that an array must contain one instance of a given type. As with the schema combination constructs, this makes sense for validation for not for forms. 285 | 286 | ## Referenced Schemas 287 | 288 | In order to foster reuse, schemas are often made available on the web. In this case, you can use JSON schema's $ref mechanism to have the browser load the schema as follows: 289 | 290 | ``` 291 | 292 | ``` 293 | 294 | The URL can also be relative to the form's URL: 295 | 296 | ``` 297 | 298 | ``` 299 | 300 | If you do not want the schema to be downloaded, you can also manually provide referenced schemas via the root schema: 301 | 302 | ``` 303 | { 304 | ... 305 | referenced: { 306 | 'http://example.org/': { $id: 'http://example.org/', ... }, 307 | 'urn:myschema': { $id: 'urn:myschema', ... }, 308 | } 309 | } 310 | ``` 311 | 312 | ## Structure of this repository 313 | 314 | The repository contains: 315 | 316 | * [The actual library code](https://github.com/dashjoin/json-schema-form/tree/master/projects/dashjoin/json-schema-form/src/lib) 317 | * [Sources of the online demo playground](https://github.com/dashjoin/json-schema-form/tree/master/src/app) 318 | 319 | ## Contribute 320 | 321 | We welcome contributions. If you are interested in contributing to Dashjoin, let us know! 322 | You'll get to know an open-minded and motivated team working together to build the next generation platform. 323 | 324 | * [Join our Slack](https://join.slack.com/t/dashjoin/shared_invite/zt-1274qbzq9-mwxBq4WwSTJsITjrvYV4pA) and say hello 325 | * [Follow us](https://twitter.com/dashjoin) on Twitter 326 | * [Submit](https://github.com/dashjoin/json-schema-form/issues) your ideas by opening an issue with the enhancement label 327 | * [Help out](https://github.com/dashjoin/json-schema-form/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) by fixing "a good first issue" 328 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { FormArray, FormControl, FormGroup } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { JsonSchemaFormComponent, JsonSchemaFormService, Schema, State } from '@dashjoin/json-schema-form'; 5 | import { CustomComponent } from './custom/custom.component'; 6 | import { FormDirective } from './form.directive'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | template: '' 11 | }) 12 | export class AppComponent { 13 | title = 'json-schema-form'; 14 | } 15 | 16 | /** 17 | * JSON schema form demo 18 | */ 19 | @Component({ 20 | templateUrl: './app.component.html', 21 | styles: ['textarea {font-family: monospace; height: 300px}', 'td {border-bottom: 1px solid #ddd;}'] 22 | }) 23 | export class MainComponent implements OnInit { 24 | 25 | /** 26 | * need to access custom component registry 27 | * @param service service for registering custom widgets etc. 28 | * @param route allows selecting an example via URL 29 | */ 30 | constructor(public service: JsonSchemaFormService, private route: ActivatedRoute) { } 31 | 32 | @ViewChild(FormDirective, { static: true }) formHost!: FormDirective; 33 | 34 | /** 35 | * example schema for meta schema case - also used in schema editor component 36 | */ 37 | static schemaExample = { 38 | type: 'object', 39 | properties: { 40 | name: { 41 | type: 'string', 42 | title: 'Enter your name' 43 | }, 44 | age: { 45 | type: 'number', 46 | title: 'Your age', 47 | }, 48 | emails: { 49 | type: 'array', 50 | items: { 51 | type: 'string', format: 'email' 52 | } 53 | } 54 | } 55 | }; 56 | 57 | /** 58 | * meta schema for meta schema case - also used in schema editor component 59 | */ 60 | static metaschema: Schema = { 61 | $schema: 'https://json-schema.org/draft-06/schema#', 62 | $ref: '#/definitions/prop', 63 | definitions: { 64 | prop: { 65 | type: 'object', 66 | hideUndefined: true, 67 | switch: 'type', 68 | class: [ 69 | 'mat-elevation-z4' 70 | ], 71 | style: { 72 | 'font-size': 'small' 73 | }, 74 | properties: { 75 | type: { type: 'string', enum: ['string', 'number', 'array', 'object'] }, 76 | enum: { type: 'array', items: { type: 'string' }, case: ['string'] }, 77 | multipleOf: { type: 'number', case: ['number'] }, 78 | maximum: { type: 'number', case: ['number'] }, 79 | exclusiveMaximum: { type: 'number', case: ['number'] }, 80 | minimum: { type: 'number', case: ['number'] }, 81 | exclusiveMinimum: { type: 'number', case: ['number'] }, 82 | maxLength: { type: 'number', case: ['string'] }, 83 | minLength: { type: 'number', case: ['string'] }, 84 | pattern: { type: 'string', case: ['string'] }, 85 | maxItems: { type: 'number', case: ['array'] }, 86 | minItems: { type: 'number', case: ['array'] }, 87 | uniqueItems: { type: 'boolean', case: ['array'] }, 88 | maxProperties: { type: 'number', case: ['object'] }, 89 | minProperties: { type: 'number', case: ['object'] }, 90 | additionalProperties: { $ref: '#/definitions/prop' }, 91 | required: { type: 'array', case: ['object'], items: { type: 'string' } }, 92 | propertyNames: { type: 'string', case: ['object'] }, 93 | title: { type: 'string' }, 94 | description: { type: 'string' }, 95 | default: { type: 'string' }, 96 | examples: { type: 'array', items: { type: 'string' }, case: ['string'] }, 97 | readOnly: { type: 'boolean', case: ['string', 'number', 'array'] }, 98 | format: { type: 'string', case: ['string'], enum: [null, 'email', 'ipv4', 'url', 'uri'] }, 99 | items: { 100 | $ref: '#/definitions/propNoRec' 101 | }, 102 | properties: { 103 | case: ['object'], 104 | type: 'object', 105 | layout: 'vertical', 106 | additionalProperties: { $ref: '#/definitions/prop' } 107 | } 108 | } 109 | }, 110 | propNoRec: { 111 | case: ['array'], 112 | 113 | type: 'object', 114 | hideUndefined: true, 115 | switch: 'type', 116 | class: [ 117 | 'mat-elevation-z4' 118 | ], 119 | style: { 120 | 'font-size': 'small' 121 | }, 122 | properties: { 123 | type: { type: 'string', enum: ['string', 'number', 'array', 'object'] }, 124 | enum: { type: 'array', items: { type: 'string' }, case: ['string'] }, 125 | multipleOf: { type: 'number', case: ['number'] }, 126 | maximum: { type: 'number', case: ['number'] }, 127 | exclusiveMaximum: { type: 'number', case: ['number'] }, 128 | minimum: { type: 'number', case: ['number'] }, 129 | exclusiveMinimum: { type: 'number', case: ['number'] }, 130 | maxLength: { type: 'number', case: ['string'] }, 131 | minLength: { type: 'number', case: ['string'] }, 132 | pattern: { type: 'string', case: ['string'] }, 133 | maxItems: { type: 'number', case: ['array'] }, 134 | minItems: { type: 'number', case: ['array'] }, 135 | uniqueItems: { type: 'boolean', case: ['array'] }, 136 | maxProperties: { type: 'number', case: ['object'] }, 137 | minProperties: { type: 'number', case: ['object'] }, 138 | additionalProperties: { $ref: '#/definitions/prop' }, 139 | required: { type: 'array', case: ['object'], items: { type: 'string' } }, 140 | propertyNames: { type: 'string', case: ['object'] }, 141 | title: { type: 'string' }, 142 | description: { type: 'string' }, 143 | default: { type: 'string' }, 144 | examples: { type: 'array', items: { type: 'string' }, case: ['string'] }, 145 | readOnly: { type: 'boolean', case: ['string', 'number', 'array'] }, 146 | format: { type: 'string', case: ['string'], enum: [null, 'email', 'ipv4', 'url', 'uri'] }, 147 | properties: { 148 | case: ['object'], 149 | type: 'object', 150 | layout: 'vertical', 151 | additionalProperties: { $ref: '#/definitions/prop' } 152 | } 153 | } 154 | }, 155 | } 156 | }; 157 | 158 | state!: State; 159 | 160 | /** 161 | * show validation result 162 | */ 163 | error: string | undefined; 164 | 165 | /** 166 | * desc of the example 167 | */ 168 | description = 'The simplest JSON schema form. Try clicking on the buttons to get to more complex examples.'; 169 | 170 | /** 171 | * examples 172 | */ 173 | examples: { [key: string]: { description: string, value: any, schema: Schema } } = 174 | { 175 | string: { 176 | description: 'The simplest JSON schema form. Try clicking on the buttons to get to more complex examples.', 177 | value: 'test', 178 | schema: { type: 'string' } 179 | }, 180 | boolean: { 181 | description: 'Boolean defaults to a checkbox', 182 | value: true, 183 | schema: { type: 'boolean' } 184 | }, 185 | integer: { 186 | description: 'Integer uses a textfield and rounds floating point numbers entered.', 187 | value: 5, 188 | schema: { type: 'integer' } 189 | }, 190 | number: { 191 | description: 'Number allows entering any type of integer or floating point', 192 | value: 3.14, 193 | schema: { type: 'number' } 194 | }, 195 | array: { 196 | description: 'Arrays display + and - controls', 197 | value: ['a', 'b'], 198 | schema: { type: 'array', items: { type: 'string' } } 199 | }, 200 | object: { 201 | description: 'Object displays a key / value form', 202 | value: { name: 'Angular', version: 9 }, 203 | schema: { type: 'object', properties: { name: { type: 'string' }, version: { type: 'number' } } } 204 | }, 205 | select: { 206 | description: 'The select widget exchanges the input field for a select combo box', 207 | value: 'France', 208 | schema: { 209 | type: 'string', 210 | widget: 'select', 211 | choicesUrl: '/assets/autocomplete-simple.json', 212 | choicesVerb: 'GET' 213 | } 214 | }, 215 | upload: { 216 | description: 'The file contents is written into the string (if type is array it is parsed, if type is object it is parsed if possible - the result is an object with data and file metadata)', 217 | value: '', 218 | schema: { 219 | type: 'string', 220 | widget: 'upload' 221 | } 222 | }, 223 | upload64: { 224 | description: 'The file contents is base64 encoded and written into the string', 225 | value: '', 226 | schema: { 227 | type: 'string', 228 | widget: 'upload64' 229 | } 230 | }, 231 | date: { 232 | description: 'Entering dates using the material date picker', 233 | value: { 234 | ISO8601: '2020-10-14T00:00:00.000Z', 235 | formatted: '12/31/2020', 236 | millisecs: 0 237 | }, 238 | schema: { 239 | type: 'object', 240 | properties: { 241 | ISO8601: { 242 | type: 'string', 243 | widget: 'date' 244 | }, 245 | formatted: { 246 | type: 'string', 247 | widget: 'date', 248 | dateFormat: 'MM/dd/yyyy' 249 | }, 250 | millisecs: { 251 | type: 'integer', 252 | widget: 'date' 253 | } 254 | } 255 | } 256 | }, 257 | textarea: { 258 | description: 'Textarea allows multi-line inputs. Note that the style key controls the input size', 259 | value: 'multi\nline\ntext', 260 | schema: { 261 | type: 'string', 262 | widget: 'textarea', 263 | style: { width: '600px', height: '300px', 'font-family': 'courier' } 264 | } 265 | }, 266 | password: { 267 | description: 'Password entry - hidden on the UI', 268 | value: 'secret', 269 | schema: { 270 | type: 'string', 271 | widget: 'password' 272 | } 273 | }, 274 | color: { 275 | description: 'Color picker generates hex color codes', 276 | value: '', 277 | schema: { 278 | type: 'string', 279 | widget: 'color' 280 | } 281 | }, 282 | 'datetime-local': { 283 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 284 | value: '', 285 | schema: { 286 | type: 'string', 287 | widget: 'datetime-local' 288 | } 289 | }, 290 | email: { 291 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 292 | value: '', 293 | schema: { 294 | type: 'string', 295 | widget: 'email' 296 | } 297 | }, 298 | month: { 299 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 300 | value: '', 301 | schema: { 302 | type: 'string', 303 | widget: 'month' 304 | } 305 | }, 306 | tel: { 307 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 308 | value: '', 309 | schema: { 310 | type: 'string', 311 | widget: 'tel' 312 | } 313 | }, 314 | time: { 315 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 316 | value: '', 317 | schema: { 318 | type: 'string', 319 | widget: 'time' 320 | } 321 | }, 322 | url: { 323 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 324 | value: '', 325 | schema: { 326 | type: 'string', 327 | widget: 'url' 328 | } 329 | }, 330 | week: { 331 | description: 'Browser input type (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)', 332 | value: '', 333 | schema: { 334 | type: 'string', 335 | widget: 'week' 336 | } 337 | }, 338 | custom: { 339 | description: 'JSON schema form can be extended by providing custom widgets. This example provides a rich text editor based on the ngx-editor component.', 340 | value: 'test', 341 | schema: { 342 | type: 'string', 343 | widget: 'custom', 344 | widgetType: 'rich-text-editor' 345 | } 346 | }, 347 | enum: { 348 | description: 'JSON schema enums defaults to a select combo box', 349 | value: null, 350 | schema: { type: 'string', enum: [null, 'true', 'false'] } 351 | }, 352 | required: { 353 | description: 'Required fields warn the user if they are left blank', 354 | value: {}, 355 | schema: { 356 | type: 'object', 357 | required: ['id'], 358 | properties: { 359 | id: { type: 'string' }, 360 | name: { type: 'string' } 361 | } 362 | } 363 | }, 364 | title: { 365 | description: 'JSON schema titles show up in the input field', 366 | value: null, 367 | schema: { type: 'string', title: 'My title' } 368 | }, 369 | description: { 370 | description: 'JSON schema description translates to tool tips', 371 | value: null, 372 | schema: { type: 'string', description: 'You find me in the tooltip' } 373 | }, 374 | default: { 375 | description: 'JSON schema default values make sure null and undefined values are set', 376 | value: undefined, 377 | schema: { type: 'string', default: 'default value' } 378 | }, 379 | examples: { 380 | description: 'JSON schema examples show up as a placeholder', 381 | value: null, 382 | schema: { type: 'string', examples: ['A possible entry'] } 383 | }, 384 | readOnly: { 385 | description: 'JSON schema read only causes the input element to be disabled', 386 | value: 'readOnly value', 387 | schema: { type: 'string', readOnly: true } 388 | }, 389 | additionalProperties: { 390 | description: 'JSON schema additional properties allow arbitrary key / value objects to be edited', 391 | value: { url: 'https://example.org', args: { limit: 10 } }, 392 | schema: { 393 | type: 'object', 394 | properties: { 395 | url: { type: 'string' }, 396 | args: { 397 | type: 'object', 398 | layout: 'vertical', 399 | additionalProperties: { type: 'string' } 400 | } 401 | } 402 | } 403 | }, 404 | ref: { 405 | description: 'JSON schema ref allows for definitions to be reused. Here we reuse the address to enter both work and home address.', 406 | value: null, 407 | schema: { 408 | definitions: { 409 | address: { 410 | type: 'object', 411 | properties: { 412 | city: { type: 'string' }, 413 | zip: { type: 'number' }, 414 | } 415 | } 416 | }, 417 | type: 'object', 418 | properties: { 419 | home: { $ref: '#/definitions/address' }, 420 | work: { $ref: '#/definitions/address' } 421 | } 422 | } 423 | }, 424 | refUrl: { 425 | description: `JSON schema $ref is loaded from a URL`, 426 | value: null, 427 | schema: { $ref: 'https://raw.githubusercontent.com/riskine/ontology/master/schemas/core/profession.json' } 428 | }, 429 | refLocal: { 430 | description: `JSON schema $ref is loaded via the referenced field in the root schema. 431 | It is up to the application to load the referenced schemas and place them in the map. 432 | Note that the map key is the schema $id`, 433 | value: null, 434 | schema: { 435 | $ref: 'https://www.dashjoin.org/#/concept', 436 | referenced: { 437 | 'https://www.dashjoin.org/': { 438 | $id: 'https://www.dashjoin.org/', 439 | concept: { 440 | type: 'object', 441 | properties: { 442 | a: { $ref: 'string.json' }, 443 | b: { $ref: 'string.json' } 444 | } 445 | } 446 | }, 447 | 'https://www.dashjoin.org/string.json': { 448 | $id: 'https://www.dashjoin.org/string.json', 449 | type: 'string' 450 | } 451 | } 452 | } 453 | }, 454 | pattern: { 455 | description: 'JSON schema pattern allows specifying a pattern that must match the input (^ matches the beginning, $ the end)', 456 | value: 'abcd3456', 457 | schema: { 458 | type: 'string', 459 | pattern: '^[a-z]+$' 460 | } 461 | }, 462 | maxLength: { 463 | description: 'JSON schema maxLength (minLength) allows specifying that a string must be at most (least) x characters long', 464 | value: 'CA', 465 | schema: { 466 | type: 'string', 467 | maxLength: 2 468 | } 469 | }, 470 | format: { 471 | description: 'JSON schema format work like built-in patterns (email, ipv4, uri, url)', 472 | value: 'john@example.org', 473 | schema: { 474 | type: 'string', 475 | format: 'email' 476 | } 477 | }, 478 | multipleOf: { 479 | description: 'JSON schema multipleOf requires the input to be a multiple of x', 480 | value: 88, 481 | schema: { 482 | type: 'number', 483 | multipleOf: 11 484 | } 485 | }, 486 | maximum: { 487 | description: `JSON schema maximum requires the input to be less 488 | than x (exclusiveMaximum, minimum and exclusiveMinimum work accordingly)`, 489 | value: 4, 490 | schema: { 491 | type: 'number', 492 | maximum: 10 493 | } 494 | }, 495 | maxItems: { 496 | description: `JSON schema maxItems (minItems) restricts array length`, 497 | value: [1], 498 | schema: { 499 | type: 'array', 500 | items: { type: 'string' }, 501 | minItems: 2, 502 | maxItems: 3, 503 | } 504 | }, 505 | uniqueItems: { 506 | description: `JSON schema uniqueItems states that every array element must be unique`, 507 | value: [1, 2, 2, 3], 508 | schema: { 509 | type: 'array', 510 | items: { type: 'number' }, 511 | uniqueItems: true, 512 | } 513 | }, 514 | maxProperties: { 515 | description: `JSON schema maxProperties (minProperties) restricts the number of fields`, 516 | value: { key: 'value' }, 517 | schema: { 518 | type: 'object', 519 | additionalProperties: { type: 'string' }, 520 | minProperties: 2, 521 | maxProperties: 3, 522 | } 523 | }, 524 | propertyNames: { 525 | description: `JSON schema propertyNames requires field names to match this regular expression`, 526 | value: { key: 'value' }, 527 | schema: { 528 | type: 'object', 529 | additionalProperties: { type: 'string' }, 530 | propertyNames: '^[a-z]+$' 531 | } 532 | }, 533 | dependencies: { 534 | description: `JSON schema dependencies defines which properties depend on other properties being present`, 535 | value: { credit_card: 5555555555555555 }, 536 | schema: { 537 | type: 'object', 538 | properties: { 539 | credit_card: { type: 'number' }, 540 | billing_address: { type: 'string' } 541 | }, 542 | dependencies: { credit_card: ['billing_address'] } 543 | } 544 | }, 545 | compute: { 546 | description: 'Allows to compute fields based on JSONata expressions (https://jsonata.org/)', 547 | value: null, 548 | schema: { 549 | type: 'object', 550 | properties: { first: { type: 'string' }, last: { type: 'string' }, salutation: { type: 'string', readOnly: true } }, 551 | computed: { 552 | salutation: '"Dear " & first & " " & last & ", " & $context("var")' 553 | } 554 | } 555 | }, 556 | errorMessage: { 557 | description: 'Allows customizing the validation error message', 558 | value: '1234', 559 | schema: { 560 | type: 'string', widget: 'password', errorMessage: 'Your password must have at least 6 characters', minLength: 6 561 | } 562 | }, 563 | createOnly: { 564 | description: 'Like readOnly, but allows changing values if the original value is null / undefined', 565 | value: { createOnly: null, exists: 'Existing data cannot be changed' }, 566 | schema: { 567 | type: 'object', properties: { 568 | createOnly: { type: 'string', createOnly: true }, 569 | exists: { type: 'string', createOnly: true, style: { width: '300px' } }, 570 | } 571 | } 572 | }, 573 | simpleGet: { 574 | description: 'Getting autocomplete options from a REST service', 575 | value: null, 576 | schema: { 577 | type: 'string', 578 | choicesUrl: '/assets/autocomplete-simple.json', 579 | choicesVerb: 'GET' 580 | } 581 | }, 582 | jsonPointer: { 583 | description: 'Getting autocomplete options from a REST service and processing the result via JSONata', 584 | value: null, 585 | schema: { 586 | type: 'string', 587 | choicesUrl: '/assets/autocomplete-complex.json', 588 | jsonata: 'result.name', 589 | choicesVerb: 'GET' 590 | } 591 | }, 592 | jsonata: { 593 | description: 'Getting autocomplete options (name and value) from a REST service and processing the result via JSONata', 594 | value: null, 595 | schema: { 596 | type: 'string', 597 | choicesUrl: '/assets/autocomplete-complex.json', 598 | jsonata: 'result.{"name": name, "value": url}', 599 | choicesVerb: 'GET' 600 | } 601 | }, 602 | 'static-choices': { 603 | description: 'Static options for autocomplete', 604 | value: null, 605 | schema: { 606 | type: 'string', 607 | choices: ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] 608 | } 609 | }, 610 | displayWith: { 611 | description: 'Select and autocomplete allow associating display names to value options', 612 | value: { 613 | select: 'WA', 614 | autocomplete: 'https://en.wikipedia.org/wiki/Indonesia' 615 | }, 616 | schema: { 617 | type: 'object', 618 | properties: { 619 | autocomplete: { 620 | type: 'string', 621 | displayWith: 'localName', 622 | choices: ['https://en.wikipedia.org/wiki/Indonesia', 'https://en.wikipedia.org/wiki/Peru', 'As is - no tooltip'] 623 | }, 624 | select: { 625 | type: 'string', 626 | widget: 'select', 627 | displayWith: 'states', 628 | choices: ['CA', 'OR', 'WA'] 629 | } 630 | } 631 | } 632 | }, 633 | displayWithChoices: { 634 | description: 'Select with fixed value and display options', 635 | value: 'WA', 636 | schema: { 637 | type: 'string', 638 | widget: 'select', 639 | displayWithChoices: ['California', 'Orgeon', 'Washington'], 640 | choices: ['CA', 'OR', 'WA'] 641 | } 642 | }, 643 | typeAhead: { 644 | description: 'Custom ChoiceHandler allows implementing typeahead functionality', 645 | value: null, 646 | schema: { 647 | type: 'string', 648 | displayWith: 'typeAhead' 649 | } 650 | }, 651 | order: { 652 | description: 'Define order, omission, and hierarchy of object fields', 653 | value: { 654 | name: 'Joe', 655 | hidden: 'not in form', 656 | age: 22, 657 | emails: ['joe@example.org', 'joe@gmail.com'], 658 | address: { city: 'LA' } 659 | }, 660 | schema: { 661 | type: 'object', 662 | layout: 'vertical', 663 | order: [ 664 | [ 665 | 'name', 666 | 'age' 667 | ], 668 | 'emails', 669 | 'address' 670 | ], 671 | properties: { 672 | emails: { 673 | class: [ 674 | 'mat-elevation-z0' 675 | ], 676 | type: 'array', 677 | items: { 678 | type: 'string' 679 | } 680 | }, 681 | name: { 682 | type: 'string' 683 | }, 684 | hidden: { 685 | type: 'string' 686 | }, 687 | age: { 688 | type: 'number' 689 | }, 690 | address: { 691 | type: 'object', 692 | properties: { 693 | city: { 694 | type: 'string' 695 | }, 696 | zip: { 697 | type: 'integer' 698 | } 699 | } 700 | } 701 | } 702 | } 703 | }, 704 | tab: { 705 | description: 'Tab layout for arrays and objects with arbitrary key / value pairs', 706 | value: [{ name: 'Angular', version: 9 }, { name: 'Vue' }], 707 | schema: { 708 | type: 'array', layout: 'tab', 709 | items: { type: 'object', properties: { name: { type: 'string' }, version: { type: 'number' } } } 710 | } 711 | }, 712 | table: { 713 | description: 'Table layout for arrays of objects', 714 | value: [{ name: 'Angular', version: 9 }, { name: 'Vue' }], 715 | schema: { 716 | type: 'array', layout: 'table', 717 | items: { type: 'object', properties: { name: { type: 'string' }, version: { type: 'number' } } } 718 | } 719 | }, 720 | vertical: { 721 | description: 'Vertical flex layout of input elements', 722 | value: [{ name: 'Angular', version: 9 }, { name: 'Vue' }], 723 | schema: { 724 | type: 'array', layout: 'vertical', 725 | items: { type: 'object', properties: { name: { type: 'string' }, version: { type: 'number' } } } 726 | } 727 | }, 728 | horizontal: { 729 | description: 'Horizontal flex layout of input elements', 730 | value: [{ name: 'Angular', version: 9 }, { name: 'Vue' }], 731 | schema: { 732 | type: 'array', layout: 'horizontal', 733 | items: { type: 'object', properties: { name: { type: 'string' }, version: { type: 'number' } } } 734 | } 735 | }, 736 | nested: { 737 | description: 'Example showing that layouts can also be nested on different levels', 738 | value: [{ name: 'Angular', version: 9 }, { name: 'Vue' }], 739 | schema: { 740 | type: 'array', layout: 'horizontal', 741 | items: { type: 'object', layout: 'vertical', properties: { name: { type: 'string' }, version: { type: 'number' } } } 742 | } 743 | }, 744 | 'array-select': { 745 | description: 'Allows arrays to be assembled from options', 746 | value: ['India', 'China'], 747 | schema: { 748 | type: 'array', layout: 'select', 749 | choicesUrl: '/assets/autocomplete-simple.json', 750 | choicesVerb: 'GET', 751 | items: { type: 'string' } 752 | } 753 | }, 754 | chips: { 755 | description: 'Chips style editing of string arrays', 756 | value: ['red', 'green', 'yellow'], 757 | schema: { 758 | title: 'You favorite colors', 759 | type: 'array', layout: 'chips', 760 | items: { type: 'string' } 761 | } 762 | }, 763 | conditional: { 764 | description: 'Allows a switch field to determine which other fields are visible. For instance, cicles show radius but not height', 765 | value: { type: 'circle' }, 766 | schema: { 767 | type: 'object', 768 | switch: 'type', 769 | properties: { 770 | type: { type: 'string', enum: ['circle', 'rect', 'square'] }, 771 | color: { type: 'string', widget: 'color' }, 772 | radius: { type: 'number', case: ['circle'] }, 773 | width: { type: 'number', case: ['rect', 'square'] }, 774 | height: { type: 'number', case: ['rect'] }, 775 | } 776 | } 777 | }, 778 | style: { 779 | description: 'Shows how css styles can be passed to form elements', 780 | value: 'style me!', 781 | schema: { 782 | type: 'string', 783 | style: { 784 | 'font-size': '44px', 785 | 'font-family': 'courier', 786 | width: '80%', 787 | 'background-color': 'lightgrey', 788 | color: 'blue', 789 | padding: '50px' 790 | } 791 | } 792 | }, 793 | class: { 794 | description: 'Shows how css classes can be passed to the form elements', 795 | value: ['a', 'b'], 796 | schema: { 797 | type: 'array', 798 | class: [ 799 | 'mat-elevation-z16' 800 | ], 801 | items: { 802 | type: 'string' 803 | } 804 | } 805 | }, 806 | expanded: { 807 | description: 'If expanded is present, the component is put in an expansion panel. The value indicates if the panel is open or not', 808 | value: [{ name: 'Angular' }, {}], 809 | schema: { 810 | type: 'array', 811 | layout: 'vertical', 812 | title: 'Software', 813 | items: { 814 | expanded: false, 815 | type: 'object', 816 | properties: { 817 | name: { 818 | type: 'string' 819 | }, 820 | version: { 821 | type: 'number' 822 | } 823 | } 824 | } 825 | } 826 | }, 827 | hideUndefined: { 828 | description: 'For object layouts, hides the input elements for undefined properties', 829 | value: { p1: 'x' }, 830 | schema: { 831 | type: 'object', 832 | hideUndefined: true, 833 | properties: { 834 | p1: { type: 'string' }, 835 | p2: { type: 'string' }, 836 | p3: { type: 'array', items: { type: 'string' } }, 837 | p4: { type: 'object', properties: { x: { type: 'string' } } } 838 | } 839 | } 840 | }, 841 | complex: { 842 | description: 'A complex example combining multiple features', 843 | value: [ 844 | { 845 | name: 'joe', 846 | country: 'United States', 847 | email: ['joe@example.org', 'alt@example.org'], 848 | birthday: '2000-03-22T23:00:00.000Z', 849 | consent: 'yes' 850 | }, 851 | { 852 | name: 'mike' 853 | } 854 | ], 855 | schema: { 856 | type: 'array', 857 | title: 'Person', 858 | items: { 859 | type: 'object', 860 | layout: 'vertical', 861 | required: ['consent'], 862 | properties: { 863 | name: { type: 'string' }, 864 | country: { 865 | description: 'Options loaded via REST', 866 | type: 'string', 867 | widget: 'select', 868 | choicesUrl: '/assets/autocomplete-simple.json', 869 | choicesVerb: 'GET' 870 | }, 871 | password: { 872 | type: 'string', 873 | widget: 'password' 874 | }, 875 | birthday: { 876 | type: 'string', 877 | widget: 'date' 878 | }, 879 | email: { 880 | type: 'array', 881 | layout: 'vertical', 882 | items: { type: 'string', format: 'email', errorMessage: 'Please enter a valid email' } 883 | }, 884 | consent: { 885 | title: 'I consent', 886 | description: 'This is a required field', 887 | type: 'string', 888 | enum: [null, 'yes', 'no'] 889 | }, 890 | } 891 | } 892 | } 893 | }, 894 | metaschema: { 895 | description: 'This schema allows editing JSON schema itself. Note that not all JSON schema features are supported.', 896 | value: MainComponent.schemaExample, 897 | schema: MainComponent.metaschema 898 | } 899 | }; 900 | 901 | /** 902 | * error string in case the user enters invalid JSON in the schema text area 903 | */ 904 | errorS: any; 905 | 906 | /** 907 | * error string in case the user enters invalid JSON in the value text area 908 | */ 909 | errorV: any; 910 | 911 | /** 912 | * register custom demo comp 913 | */ 914 | ngOnInit() { 915 | this.service.registerComponent('rich-text-editor', CustomComponent); 916 | this.select('string') 917 | 918 | this.route.params.subscribe(res => { 919 | if ((res as any).id) { 920 | this.select((res as any).id); 921 | } 922 | }) 923 | } 924 | 925 | /** 926 | * select one of the examples 927 | */ 928 | select(key: string) { 929 | this.description = this.examples[key].description; 930 | this.error = ''; 931 | this.set(this.examples[key].schema, this.examples[key].value) 932 | } 933 | 934 | /** 935 | * select one of the examples 936 | */ 937 | set(schema: any, value: any) { 938 | this.formHost.viewContainerRef.clear() 939 | const add = this.formHost.viewContainerRef.createComponent(JsonSchemaFormComponent) 940 | let control 941 | if (schema.type === 'array' && schema.layout !== 'select' && schema.layout !== 'chips') 942 | control = new FormArray([]) 943 | else if (schema.type === 'object') 944 | control = new FormGroup({}) 945 | else 946 | control = new FormControl() 947 | 948 | this.state = { 949 | schema, 950 | value, 951 | name: 'test', 952 | control 953 | } 954 | add.instance.state = this.state 955 | 956 | this.state.control.valueChanges.subscribe(res => { 957 | setTimeout(() => { 958 | // this triggers a re-render, protect against ExpressionChangedAfterItHasBeenCheckedError 959 | this.state.value = res 960 | }) 961 | console.log(res); 962 | }) 963 | this.state.control.statusChanges.subscribe(res => { 964 | setTimeout(() => { 965 | // this triggers a re-render, protect against ExpressionChangedAfterItHasBeenCheckedError 966 | if (res === 'VALID' || res === 'DISABLED') 967 | this.error = undefined 968 | else 969 | this.error = res 970 | }) 971 | console.log(res); 972 | }) 973 | } 974 | 975 | /** 976 | * stringify JSON for display in the textarea 977 | */ 978 | stringify(o: any): string { 979 | return JSON.stringify(o, null, 2); 980 | } 981 | 982 | /** 983 | * user made change in schema textarea: 984 | * set schema and handle parse error 985 | */ 986 | changeS(event: any): void { 987 | try { 988 | this.set(JSON.parse(event.target.value), this.state.value) 989 | this.errorS = null; 990 | } catch (e) { 991 | this.errorS = e; 992 | } 993 | } 994 | 995 | /** 996 | * user made change in value textarea: 997 | * set schema and handle parse error 998 | */ 999 | changeV(event: any): void { 1000 | try { 1001 | this.set(this.state.schema, JSON.parse(event.target.value)); 1002 | this.errorV = null; 1003 | } catch (e) { 1004 | this.errorV = e; 1005 | } 1006 | } 1007 | } 1008 | --------------------------------------------------------------------------------