angular2-json-schema-form
Version:
Angular 2 JSON Schema Form builder
429 lines (387 loc) • 17.9 kB
text/typescript
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
*/
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
schema: any; // The JSON Schema
layout: any[]; // The form layout
data: any; // The data model
options: any; // The global form options
framework: string; // The framework to load
widgets: string; // Any custom widgets to load
// Alternate combined single input
form: any; // For testing, and JSON Schema Form API compatibility
// Angular Schema Form API compatibility inputs
model: any; // Alternate input for data model
// React JSON Schema Form API compatibility inputs
JSONSchema: any; // Alternate input for JSON Schema
UISchema: any; // UI schema - alternate form layout format
formData: any; // Alternate input for data model
// Development inputs, for testing and debugging
loadExternalAssets: boolean; // Load external framework assets?
debug: boolean; // Show debug information?
// Outputs
onChanges = new EventEmitter<any>(); // Live unvalidated internal form data
onSubmit = new EventEmitter<any>(); // Complete validated form data
isValid = new EventEmitter<boolean>(); // Is current data valid?
validationErrors = new EventEmitter<any>(); // Validation errors (if any)
formSchema = new EventEmitter<any>(); // Final schema used to create form
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);
}
}