├── 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 |
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 | | {{x.value.title ? x.value.title :
4 | x.key}}
5 | |
6 | |
7 |
8 |
9 | |
10 |
11 | |
12 |
13 |
16 | |
17 |
18 |
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 |
8 |
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 | | Value (Changes are applied to the form onBlur) |
11 | Schema (Changes are applied to the form onBlur) |
12 |
13 |
14 | |
15 |
16 | |
17 |
18 |
19 | |
20 |
21 |
22 | |
23 | {{errorV}}
24 | |
25 |
26 | {{errorS}}
27 | |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Examples (Press to select sample data and schema)
37 |
38 |
39 |
40 |
41 | |
42 | Types:
43 | |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | |
52 |
53 |
54 | |
55 | Widgets:
56 | |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | |
74 |
75 |
76 | |
77 | JSON schema constructs:
78 | |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | |
104 |
105 |
106 | |
107 | Logic:
108 | |
109 |
110 |
113 |
114 |
115 | |
116 |
117 |
118 | |
119 | Autocomplete:
120 | |
121 |
122 |
123 | data
124 |
125 | data
126 |
127 |
128 |
129 |
134 | |
135 |
136 |
137 | |
138 | Layout:
139 | |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
156 | |
157 |
158 |
159 | |
160 | Complex:
161 | |
162 |
163 |
164 |
168 | |
169 |
170 |
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 | [](https://sonarcloud.io/dashboard?id=dashjoin_json-schema-form)
4 | [](https://www.npmjs.com/package/@dashjoin/json-schema-form)
5 |
6 | 
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 | [](https://sonarcloud.io/dashboard?id=dashjoin_json-schema-form)
4 | [](https://www.npmjs.com/package/@dashjoin/json-schema-form)
5 |
6 | 
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 |
--------------------------------------------------------------------------------