UNPKG

angular2-json-schema-form

Version:
429 lines (387 loc) 17.9 kB
import { ChangeDetectionStrategy, Component, DoCheck, EventEmitter, Input, Output, OnChanges, OnInit, } from '@angular/core'; import { FormGroup } from '@angular/forms'; import * as _ from 'lodash'; import { FrameworkLibraryService } from '../frameworks/framework-library.service'; import { WidgetLibraryService } from '../widgets/widget-library.service'; import { JsonSchemaFormService } from './json-schema-form.service'; import { hasOwn, hasValue, isArray, isEmpty, isObject, JsonPointer } from './utilities/index'; /** * @module 'JsonSchemaFormComponent' - Angular 2 JSON Schema Form * * Root module of the Angular 2 JSON Schema Form client-side library, * an Angular 2 library which generates an HTML form from a JSON schema * structured data model and/or a JSON Schema Form layout description. * * This library also validates input data by the user, both using individual * validators which provide real-time feedback while the user is filling out * the form, and then using the entire schema when the form is submitted, * to make sure the returned JSON data object is valid. * * This library is similar to, and mostly API compatible with: * * - JSON Schema Form's Angular Schema Form library for AngularJs * http://schemaform.io * http://schemaform.io/examples/bootstrap-example.html (examples) * * - Joshfire's JSON Form library for jQuery * https://github.com/joshfire/jsonform * http://ulion.github.io/jsonform/playground (examples) * * - Mozilla's react-jsonschema-form library for React * https://github.com/mozilla-services/react-jsonschema-form * https://mozilla-services.github.io/react-jsonschema-form (examples) * * This library depends on: * - Angular 2 (obviously) https://angular.io * - lodash, JavaScript utility library https://github.com/lodash/lodash * - ajv, Another JSON Schema validator https://github.com/epoberezkin/ajv * In addition, the testing playground also depends on: * - brace, Browserified Ace editor http://thlorenz.github.io/brace */ @Component({ selector: 'json-schema-form', template: ` <form (ngSubmit)="submitForm()"> <root-widget [formID]="formID" [layout]="jsf.layout"></root-widget> </form> <div *ngIf="debug || jsf.globalOptions.debug"> Debug output: <pre>{{debugOutput}}</pre> </div>`, providers: [ JsonSchemaFormService ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class JsonSchemaFormComponent implements DoCheck, OnChanges, OnInit { private formID: number; // Unique ID for displayed form private debugOutput: any; // Debug information, if requested private formValueSubscription: any = null; // Recommended inputs @Input() schema: any; // The JSON Schema @Input() layout: any[]; // The form layout @Input() data: any; // The data model @Input() options: any; // The global form options @Input() framework: string; // The framework to load @Input() widgets: string; // Any custom widgets to load // Alternate combined single input @Input() form: any; // For testing, and JSON Schema Form API compatibility // Angular Schema Form API compatibility inputs @Input() model: any; // Alternate input for data model // React JSON Schema Form API compatibility inputs @Input() JSONSchema: any; // Alternate input for JSON Schema @Input() UISchema: any; // UI schema - alternate form layout format @Input() formData: any; // Alternate input for data model // Development inputs, for testing and debugging @Input() loadExternalAssets: boolean; // Load external framework assets? @Input() debug: boolean; // Show debug information? // Outputs @Output() onChanges = new EventEmitter<any>(); // Live unvalidated internal form data @Output() onSubmit = new EventEmitter<any>(); // Complete validated form data @Output() isValid = new EventEmitter<boolean>(); // Is current data valid? @Output() validationErrors = new EventEmitter<any>(); // Validation errors (if any) @Output() formSchema = new EventEmitter<any>(); // Final schema used to create form @Output() formLayout = new EventEmitter<any>(); // Final layout used to create form constructor( private frameworkLibrary: FrameworkLibraryService, private widgetLibrary: WidgetLibraryService, private jsf: JsonSchemaFormService ) { } ngOnInit() { this.initializeForm(); } ngOnChanges() { this.initializeForm(); } /** * 'initializeForm' function * * - Update 'schema', 'layout', and 'initialValues', from inputs. * * - Create 'dataMap' to map the data to the schema and template. * * - Create 'schemaRefLibrary' to resolve schema $ref links. * * - Create 'layoutRefLibrary' to use when dynamically adding * form components to arrays and recursive $ref points. * * - Create 'formGroupTemplate', then from it 'formGroup', * the Angular 2 formGroup used to control the reactive form. * * @return {void} */ public initializeForm(): void { if ( this.schema || this.layout || this.data || this.form || this.JSONSchema || this.UISchema ) { // Reset all form values to defaults this.jsf.resetAllValues(); // Initialize 'options' (global form options) and set framework // Combine available inputs: // 1. options - recommended // 2. form.options - Single input style this.jsf.setOptions({ debug: !!this.debug }); let loadExternalAssets: boolean = this.loadExternalAssets || false; let framework: any = this.framework || 'default'; if (isObject(this.options)) { this.jsf.setOptions(this.options); loadExternalAssets = this.options.loadExternalAssets || loadExternalAssets; framework = this.options.framework || framework; } if (isObject(this.form) && isObject(this.form.options)) { this.jsf.setOptions(this.form.options); loadExternalAssets = this.form.options.loadExternalAssets || loadExternalAssets; framework = this.form.options.framework || framework; } if (isObject(this.widgets)) { this.jsf.setOptions({ widgets: this.widgets }); } this.frameworkLibrary.setLoadExternalAssets(loadExternalAssets); this.frameworkLibrary.setFramework(framework); this.jsf.framework = this.frameworkLibrary.getFramework(); if (isObject(this.jsf.globalOptions.widgets)) { for (let widget of Object.keys(this.jsf.globalOptions.widgets)) { this.widgetLibrary.registerWidget(widget, this.jsf.globalOptions.widgets[widget]); } } if (isObject(this.form) && isObject(this.form.tpldata)) { this.jsf.setTpldata(this.form.tpldata); } // Initialize 'schema' // Use first available input: // 1. schema - recommended / Angular Schema Form style // 2. form.schema - Single input / JSON Form style // 3. JSONSchema - React JSON Schema Form style // 4. form.JSONSchema - For testing single input React JSON Schema Forms // 5. form - For testing single schema-only inputs // TODO: 6. (none) no schema - construct form entirely from layout instead if (isObject(this.schema)) { this.jsf.AngularSchemaFormCompatibility = true; this.jsf.schema = _.cloneDeep(this.schema); } else if (hasOwn(this.form, 'schema') && isObject(this.form.schema)) { this.jsf.schema = _.cloneDeep(this.form.schema); } else if (isObject(this.JSONSchema)) { this.jsf.ReactJsonSchemaFormCompatibility = true; this.jsf.schema = _.cloneDeep(this.JSONSchema); } else if (hasOwn(this.form, 'JSONSchema') && isObject(this.form.JSONSchema)) { this.jsf.ReactJsonSchemaFormCompatibility = true; this.jsf.schema = _.cloneDeep(this.form.JSONSchema); } else if (hasOwn(this.form, 'properties') && isObject(this.form.properties)) { this.jsf.schema = _.cloneDeep(this.form); } if (!isEmpty(this.jsf.schema)) { // Allow for JSON schema shorthand (JSON Form style) if (!hasOwn(this.jsf.schema, 'type') && hasOwn(this.jsf.schema, 'properties') && isObject(this.jsf.schema.properties) ) { this.jsf.schema.type = 'object'; } else if (!hasOwn(this.jsf.schema, 'type') || this.jsf.schema.type !== 'object' || !hasOwn(this.jsf.schema, 'properties') ) { this.jsf.JsonFormCompatibility = true; this.jsf.schema = { 'type': 'object', 'properties': this.jsf.schema }; } // If JSON Schema is version 3 (JSON Form style), convert it to version 4 this.jsf.convertJsonSchema3to4(); // Initialize ajv and compile schema this.jsf.compileAjvSchema(); // Resolve all schema $ref links this.jsf.resolveSchemaRefLinks(); } // Initialize 'layout' // Use first available array input: // 1. layout - recommended // 2. form - Angular Schema Form style // 3. form.form - JSON Form style // 4. form.layout - Single input style // 5. (none) no input - use default layout instead if (isArray(this.layout)) { this.jsf.layout = _.cloneDeep(this.layout); } else if (isArray(this.form)) { this.jsf.AngularSchemaFormCompatibility = true; this.jsf.layout = _.cloneDeep(this.form); } else if (this.form && isArray(this.form.form)) { this.jsf.JsonFormCompatibility = true; this.jsf.layout = this.jsf.fixJsonFormOptions(_.cloneDeep(this.form.form)); } else if (this.form && isArray(this.form.layout)) { this.jsf.layout = _.cloneDeep(this.form.layout); } else { this.jsf.layout = this.jsf.globalOptions.addSubmit === false ? [ '*' ] : [ '*', { type: 'submit', title: 'Submit' } ]; } // Import alternate layout formats 'UISchema' or 'customFormItems' // used for React JSON Schema Form and JSON Form API compatibility // Use first available input: // 1. UISchema - React JSON Schema Form style // 2. form.UISchema - For testing single input React JSON Schema Forms // 2. form.customFormItems - JSON Form style // 3. (none) no input - don't import let alternateLayout: any = null; if (isObject(this.UISchema)) { this.jsf.ReactJsonSchemaFormCompatibility = true; alternateLayout = _.cloneDeep(this.UISchema); } else if (hasOwn(this.form, 'UISchema')) { this.jsf.ReactJsonSchemaFormCompatibility = true; alternateLayout = _.cloneDeep(this.form.UISchema); } else if (hasOwn(this.form, 'customFormItems')) { this.jsf.JsonFormCompatibility = true; alternateLayout = this.jsf.fixJsonFormOptions(_.cloneDeep(this.form.customFormItems)); } // if alternate layout found, copy options into schema if (alternateLayout) { JsonPointer.forEachDeep(alternateLayout, (value, pointer) => { const schemaPointer: string = pointer.replace(/\//g, '/properties/') .replace(/\/properties\/items\/properties\//g, '/items/properties/') .replace(/\/properties\/titleMap\/properties\//g, '/titleMap/properties/'); if (hasValue(value) && hasValue(pointer)) { const groupPointer: string[] = JsonPointer.parse(schemaPointer).slice(0, -2); let key = JsonPointer.toKey(schemaPointer); let itemPointer: string | string[]; // If 'ui:order' object found, copy into schema root if (key === 'ui:order') { itemPointer = schemaPointer; // Copy other alternate layout options to schema 'x-schema-form', // (like Angular Schema Form options) and remove any 'ui:' prefixes } else { itemPointer = groupPointer.concat(['x-schema-form', key.slice(0, 3) === 'ui:' ? key.slice(3) : key ]); } if (JsonPointer.has(this.jsf.schema, groupPointer) && !JsonPointer.has(this.jsf.schema, itemPointer) ) { JsonPointer.set(this.jsf.schema, itemPointer, value); } } }); } // Initialize 'initialValues' // Use first available input: // 1. data - recommended // 2. model - Angular Schema Form style // 3. form.value - JSON Form style // 4. form.data - Single input style // 5. formData - React JSON Schema Form style // 6. form.formData - For easier testing of React JSON Schema Forms // 7. (none) no data - initialize data from schema and layout defaults only if (isObject(this.data)) { this.jsf.initialValues = _.cloneDeep(this.data); } else if (isObject(this.model)) { this.jsf.AngularSchemaFormCompatibility = true; this.jsf.initialValues = _.cloneDeep(this.model); } else if (isObject(this.form) && isObject(this.form.value)) { this.jsf.JsonFormCompatibility = true; this.jsf.initialValues = _.cloneDeep(this.form.value); } else if (isObject(this.form) && isObject(this.form.data)) { this.jsf.initialValues = _.cloneDeep(this.form.data); } else if (isObject(this.formData)) { this.jsf.ReactJsonSchemaFormCompatibility = true; this.jsf.initialValues = _.cloneDeep(this.formData); } else if (hasOwn(this.form, 'formData') && isObject(this.form.formData)) { this.jsf.ReactJsonSchemaFormCompatibility = true; this.jsf.initialValues = _.cloneDeep(this.form.formData); } if (isEmpty(this.jsf.schema)) { // TODO: If layout, but no schema, build schema from layout if (this.jsf.layout.indexOf('*') === -1) { this.jsf.buildSchemaFromLayout(); // If no schema and no layout, build schema from data } else if (!isEmpty(this.jsf.initialValues)) { this.jsf.buildSchemaFromData(); } } if (!isEmpty(this.jsf.schema)) { // If not already initialized, initialize ajv and compile schema this.jsf.compileAjvSchema(); // Build the Angular 2 FormGroup template from the schema this.jsf.buildFormGroupTemplate(); // Update all layout elements, add values, widgets, and validators, // replace any '*' with a layout built from all schema elements, // and update the FormGroup template with any new validators this.jsf.buildLayout(this.widgetLibrary); // Build the real Angular 2 FormGroup from the FormGroup template this.jsf.buildFormGroup(); } if (this.jsf.formGroup) { // // Calculate references to other fields // if (!isEmpty(this.jsf.formGroup.value)) { // forEach(this.jsf.formGroup.value, (value, key, object, rootObject) => { // if (typeof value === 'string') { // object[key] = this.jsf.parseText(value, value, rootObject, key); // } // }, 'top-down'); // } // // TODO: Figure out how to display calculated values without changing object data // // See http://ulion.github.io/jsonform/playground/?example=templating-values // TODO: (re-)render the form // Subscribe to form changes to output live data, validation, and errors this.jsf.dataChanges.subscribe(data => this.onChanges.emit(data)); this.jsf.isValidChanges.subscribe(isValid => this.isValid.emit(isValid)); this.jsf.validationErrorChanges.subscribe(errors => this.validationErrors.emit(errors)); // Output final schema, final layout, and initial data this.formSchema.emit(this.jsf.schema); this.formLayout.emit(this.jsf.layout); this.onChanges.emit(this.jsf.data); // If 'validateOnRender' = true, output initial validation and any errors if (JsonPointer.get(this.jsf, '/globalOptions/validateOnRender')) { this.isValid.emit(this.jsf.isValid); this.validationErrors.emit(this.jsf.validationErrors); } // Uncomment individual lines to output debugging information to console: // (These always work.) // console.log('loading form...'); // console.log(this.jsf.schema); // console.log(this.jsf.layout); // console.log(this.jsf.initialValues); // console.log(this.jsf.formGroup.value); // console.log(this.jsf.formGroupTemplate); // console.log(this.jsf.formGroup); // console.log(this.jsf.schemaRefLibrary); // console.log(this.jsf.layoutRefLibrary); // console.log(this.jsf.templateRefLibrary); // console.log(this.jsf.dataMap); // console.log(this.jsf.arrayMap); // console.log(this.jsf.schemaRecursiveRefMap); // console.log(this.jsf.dataRecursiveRefMap); } else { // TODO: Display error message } } } // Uncomment individual lines to output debugging information to browser: // (These only work if the 'debug' option has also been set to 'true'.) ngDoCheck() { if (this.debug || this.jsf.globalOptions.debug) { const vars: any[] = []; // vars.push(this.jsf.schema); // vars.push(this.jsf.layout); // vars.push(this.jsf.initialValues); // vars.push(this.jsf.formGroup.value); // vars.push(this.jsf.formGroupTemplate); // vars.push(this.jsf.formGroup); // vars.push(this.jsf.schemaRefLibrary); // vars.push(this.jsf.layoutRefLibrary); // vars.push(this.jsf.templateRefLibrary); // vars.push(this.jsf.dataMap); // vars.push(this.jsf.arrayMap); // vars.push(this.jsf.schemaRecursiveRefMap); // vars.push(this.jsf.dataRecursiveRefMap); this.debugOutput = vars.map(v => JSON.stringify(v, null, 2)).join('\n'); } } private submitForm() { this.onSubmit.emit(this.jsf.validData); } }