UNPKG

angular2-json-schema-form

Version:
467 lines (454 loc) 17 kB
import { AbstractControl, FormArray, FormControl, FormGroup, ValidatorFn } from '@angular/forms'; import * as _ from 'lodash'; import { forEach, getControlValidators, hasOwn, hasValue, inArray, isArray, isEmpty, isObject, isDefined, isPrimitive, JsonPointer, JsonValidators, Pointer, toJavaScriptType, toSchemaType, resolveRecursiveReferences, SchemaPrimitiveType } from './index'; /** * FormGroup function library: * * buildFormGroupTemplate: Builds a FormGroupTemplate from schema * * buildFormGroup: Builds an Angular 2 FormGroup from a FormGroupTemplate * * setRequiredFields: * * formatFormData: * * getControl: * * fixJsonFormOptions: * * ---- Under construction: ---- * buildFormGroupTemplateFromLayout: Builds a FormGroupTemplate from a form layout */ /** * 'buildFormGroupTemplate' function * * Builds a template for an Angular 2 FormGroup from a JSON Schema. * * TODO: add support for pattern properties * https://spacetelescope.github.io/understanding-json-schema/reference/object.html * * @param {any} jsf - * @param {any = null} setValues - * @param {boolean = true} mapArrays - * @param {string = ''} schemaPointer - * @param {string = ''} dataPointer - * @param {any = ''} templatePointer - * @return {any} - */ export function buildFormGroupTemplate( jsf: any, setValues: any = null, mapArrays: boolean = true, schemaPointer: string = '', dataPointer: string = '', templatePointer: any = '' ): any { const schema: any = JsonPointer.get(jsf.schema, schemaPointer); let useValues: any = jsf.globalOptions.setSchemaDefaults ? mergeValues(JsonPointer.get(schema, '/default'), setValues) : setValues; const schemaType: string | string[] = JsonPointer.get(schema, '/type'); let controlType: 'FormGroup' | 'FormArray' | 'FormControl' | '$ref'; if (schemaType === 'object' && hasOwn(schema, 'properties')) { controlType = 'FormGroup'; } else if (schemaType === 'array' && hasOwn(schema, 'items')) { controlType = 'FormArray'; } else if (!schemaType && hasOwn(schema, '$ref')) { controlType = '$ref'; } else { controlType = 'FormControl'; } if (dataPointer !== '' && !jsf.dataMap.has(dataPointer)) { jsf.dataMap.set(dataPointer, new Map); jsf.dataMap.get(dataPointer).set('schemaPointer', schemaPointer); jsf.dataMap.get(dataPointer).set('schemaType', schema.type); if (controlType) { jsf.dataMap.get(dataPointer).set('templatePointer', templatePointer); jsf.dataMap.get(dataPointer).set('templateType', controlType); } const genericDataPointer = JsonPointer.toGenericPointer(dataPointer, jsf.arrayMap); if (!jsf.dataMap.has(genericDataPointer)) { jsf.dataMap.set(genericDataPointer, new Map); jsf.dataMap.get(genericDataPointer).set('schemaPointer', schemaPointer); jsf.dataMap.get(genericDataPointer).set('schemaType', schema.type); } } let controls: any; let validators: any = getControlValidators(schema); switch (controlType) { case 'FormGroup': controls = {}; if (jsf.globalOptions.setSchemaDefaults) { useValues = mergeValues( JsonPointer.get(schema, '/properties/default'), useValues); } forEach(schema.properties, (item, key) => { if (key !== 'ui:order') { controls[key] = buildFormGroupTemplate( jsf, JsonPointer.get(useValues, [<string>key]), mapArrays, schemaPointer + '/properties/' + key, dataPointer + '/' + key, templatePointer + '/controls/' + key ); } }); jsf.globalOptions.fieldsRequired = setRequiredFields(schema, controls); return { controlType, controls, validators }; case 'FormArray': const minItems = schema.minItems || 0; const maxItems = schema.maxItems || 1000000; if (isArray(schema.items)) { // 'items' is an array = tuple items if (mapArrays && !jsf.arrayMap.get(dataPointer)) { jsf.arrayMap.set(dataPointer, schema.items.length); } controls = []; for (let i = 0, l = schema.items.length; i < l; i++) { if (i >= minItems && !JsonPointer.has(jsf.templateRefLibrary, [dataPointer + '/' + i]) ) { jsf.templateRefLibrary[dataPointer + '/' + i] = buildFormGroupTemplate( jsf, null, mapArrays, schemaPointer + '/items/' + i, dataPointer + '/' + i, templatePointer + '/controls/' + i ); } if (i < maxItems) { const useValue = isArray(useValues) ? useValues[i] : useValues; controls.push(buildFormGroupTemplate( jsf, useValue, false, schemaPointer + '/items/' + i, dataPointer + '/' + i, templatePointer + '/controls/' + i )); } } if (schema.items.length < maxItems && hasOwn(schema, 'additionalItems') && isObject(schema.additionalItems) ) { // 'additionalItems' is an object = additional list items const l = Math.max( schema.items.length + 1, isArray(useValues) ? useValues.length : 0 ); for (let i = schema.items.length; i < l; i++) { const useValue = isArray(useValues) ? useValues[i] : useValues; controls.push(buildFormGroupTemplate( jsf, useValue, false, schemaPointer + '/additionalItems', dataPointer + '/' + i, templatePointer + '/controls/' + i )); if (isArray(useValues)) { useValues = null; } } if ( !JsonPointer.has(jsf, ['templateRefLibrary', dataPointer + '/-']) ) { jsf.templateRefLibrary[dataPointer + '/-'] = buildFormGroupTemplate( jsf, null, mapArrays, schemaPointer + '/additionalItems', dataPointer + '/-', templatePointer + '/controls/-' ); } } } else { // 'items' is an object = list items only (no tuple items) if (mapArrays && !jsf.arrayMap.get(dataPointer)) { jsf.arrayMap.set(dataPointer, 0); } if ( !JsonPointer.has(jsf.templateRefLibrary, [dataPointer + '/-']) ) { jsf.templateRefLibrary[dataPointer + '/-'] = buildFormGroupTemplate( jsf, null, mapArrays, schemaPointer + '/items', dataPointer + '/-', templatePointer + '/controls/-' ); } controls = []; if (jsf.globalOptions.setSchemaDefaults) { useValues = mergeValues( JsonPointer.get(schema, '/items/default'), useValues); } if (isArray(useValues) && useValues.length) { for (let i of Object.keys(useValues)) { controls.push(buildFormGroupTemplate( jsf, useValues[i], false, schemaPointer + '/items', dataPointer + '/' + i, templatePointer + '/controls/' + i )); } useValues = null; } } let initialItemCount = Math.max(minItems, JsonPointer.has(schema, '/items/$ref') ? 0 : 1); if (controls.length < initialItemCount) { for (let i = controls.length, l = initialItemCount; i < l; i++) { controls.push(buildFormGroupTemplate( jsf, useValues, false, schemaPointer + '/items', dataPointer + '/' + i, templatePointer + '/controls/' + i )); } } return { controlType, controls, validators }; case 'FormControl': let value: { value: any, disabled: boolean } = { value: isPrimitive(useValues) ? useValues : null, disabled: schema['disabled'] || JsonPointer.get(schema, '/x-schema-form/disabled') || false }; return { controlType, value, validators }; case '$ref': const schemaRef: string = JsonPointer.compile(schema.$ref); if (!hasOwn(jsf.templateRefLibrary, schemaRef)) { // Set to null first to prevent recursive reference from causing endless loop jsf.templateRefLibrary[schemaRef] = null; const newTemplate: any = buildFormGroupTemplate(jsf, null, false, schemaRef); if (newTemplate) { jsf.templateRefLibrary[schemaRef] = newTemplate; } else { delete jsf.templateRefLibrary[schemaRef]; } } return null; default: return null; } } /** * 'buildFormGroup' function * * @param {any} template - * @return {AbstractControl} */ export function buildFormGroup(template: any): AbstractControl { let validatorFns: ValidatorFn[] = []; let validatorFn: ValidatorFn = null; if (hasOwn(template, 'validators')) { forEach(template.validators, (parameters, validator) => { if (typeof JsonValidators[validator] === 'function') { validatorFns.push(JsonValidators[validator].apply(null, parameters)); } }); if (validatorFns.length && inArray(['FormGroup', 'FormArray'], template.controlType) ) { validatorFn = validatorFns.length > 1 ? JsonValidators.compose(validatorFns) : validatorFns[0]; } } if (hasOwn(template, 'controlType')) { switch (template.controlType) { case 'FormGroup': let groupControls: { [key: string]: AbstractControl } = {}; forEach(template.controls, (controls, key) => { let newControl: AbstractControl = buildFormGroup(controls); if (newControl) { groupControls[key] = newControl; } }); return new FormGroup(groupControls, validatorFn); case 'FormArray': return new FormArray(_.filter(_.map(template.controls, controls => buildFormGroup(controls) )), validatorFn); case 'FormControl': return new FormControl(template.value, validatorFns); } } return null; } /** * 'mergeValues' function * * @param {any[]} ...valuesToMerge - Multiple values to merge * @return {any} - Merged values */ export function mergeValues(...valuesToMerge) { let mergedValues: any = null; for (let index = 0, length = arguments.length; index < length; index++) { const currentValue = arguments[index]; if (!isEmpty(currentValue)) { if (typeof currentValue === 'object' && (isEmpty(mergedValues) || typeof mergedValues !== 'object') ) { if (isArray(currentValue)) { mergedValues = [].concat(currentValue); } else if (isObject(currentValue)) { mergedValues = Object.assign({}, currentValue); } } else if (typeof currentValue !== 'object') { mergedValues = currentValue; } else if (isObject(mergedValues) && isObject(currentValue)) { Object.assign(mergedValues, currentValue); } else if (isObject(mergedValues) && isArray(currentValue)) { let newValues = []; for (let value of currentValue) { newValues.push(mergeValues(mergedValues, value)); } mergedValues = newValues; } else if (isArray(mergedValues) && isObject(currentValue)) { let newValues = []; for (let value of mergedValues) { newValues.push(mergeValues(value, currentValue)); } mergedValues = newValues; } else if (isArray(mergedValues) && isArray(currentValue)) { let newValues = []; const l = Math.max(mergedValues.length, currentValue.length); for (let i = 0; i < l; i++) { if (i < mergedValues.length && i < currentValue.length) { newValues.push(mergeValues(mergedValues[i], currentValue[i])); } else if (i < mergedValues.length) { newValues.push(mergedValues[i]); } else if (i < currentValue.length) { newValues.push(currentValue[i]); } } mergedValues = newValues; } } } return mergedValues; } /** * 'setRequiredFields' function * * @param {schema} schema - JSON Schema * @param {object} formControlTemplate - Form Control Template object * @return {boolean} - true if any fields have been set to required, false if not */ export function setRequiredFields(schema: any, formControlTemplate: any): boolean { let fieldsRequired = false; if (hasOwn(schema, 'required') && !isEmpty(schema.required)) { fieldsRequired = true; let requiredArray = isArray(schema.required) ? schema.required : [schema.required]; requiredArray = forEach(requiredArray, key => JsonPointer.set(formControlTemplate, '/' + key + '/validators/required', []) ); } return fieldsRequired; // TODO: Add support for patternProperties // https://spacetelescope.github.io/understanding-json-schema/reference/object.html // #pattern-properties } /** * 'formatFormData' function * * @param {any} formData - Angular 2 FormGroup data object * @param {Map<string, any>} dataMap - * @param {Map<string, string>} recursiveRefMap - * @param {Map<string, number>} arrayMap - * @param {boolean = false} fixErrors - if TRUE, tries to fix data * @return {any} - formatted data object */ export function formatFormData( formData: any, dataMap: Map<string, any>, recursiveRefMap: Map<string, string>, arrayMap: Map<string, number>, fixErrors: boolean = false ): any { // return formData; let formattedData = {}; JsonPointer.forEachDeep(formData, (value, dataPointer) => { if (typeof value !== 'object') { let genericPointer: string = JsonPointer.has(dataMap, [dataPointer, 'schemaType']) ? dataPointer : resolveRecursiveReferences(dataPointer, recursiveRefMap, arrayMap); if (JsonPointer.has(dataMap, [genericPointer, 'schemaType'])) { const schemaType: SchemaPrimitiveType | SchemaPrimitiveType[] = dataMap.get(genericPointer).get('schemaType'); if (schemaType === 'null') { JsonPointer.set(formattedData, dataPointer, null); } else if ( hasValue(value) && inArray(schemaType, ['string', 'integer', 'number', 'boolean']) ) { const newValue = fixErrors ? toSchemaType(value, schemaType) : toJavaScriptType(value, schemaType); if (isDefined(newValue)) { JsonPointer.set(formattedData, dataPointer, newValue); } } } else { console.error('formatFormData error: Schema type not found ' + 'for form value at "' + genericPointer + '".'); console.error(formData); console.error(dataMap); console.error(recursiveRefMap); console.error(arrayMap); } } }); return formattedData; } /** * 'getControl' function * * Uses a JSON Pointer for a data object to retrieve a control from * an Angular 2 formGroup or formGroup template. (Note: though a formGroup * template is much simpler, its basic structure is idential to a formGroup). * * If the optional third parameter 'returnGroup' is set to TRUE, the group * containing the control is returned, rather than the control itself. * * @param {FormGroup} formGroup - Angular 2 FormGroup to get value from * @param {Pointer} dataPointer - JSON Pointer (string or array) * @param {boolean = false} returnGroup - If true, return group containing control * @return {group} - Located value (or true or false, if returnError = true) */ export function getControl( formGroup: any, dataPointer: Pointer, returnGroup: boolean = false ): any { const dataPointerArray: string[] = JsonPointer.parse(dataPointer); let subGroup = formGroup; if (dataPointerArray !== null) { let l = dataPointerArray.length - (returnGroup ? 1 : 0); for (let i = 0; i < l; ++i) { let key = dataPointerArray[i]; if (subGroup.hasOwnProperty('controls')) { subGroup = subGroup.controls; } if (isArray(subGroup) && (key === '-')) { subGroup = subGroup[subGroup.length - 1]; } else if (subGroup.hasOwnProperty(key)) { subGroup = subGroup[key]; } else { console.error('getControl error: Unable to find "' + key + '" item in FormGroup.'); console.error(dataPointer); console.error(formGroup); return; } } return subGroup; } console.error('getControl error: Invalid JSON Pointer: ' + dataPointer); } /** * 'fixJsonFormOptions' function * * Rename JSON Form-style 'options' lists to * Angular Schema Form-style 'titleMap' lists. * * @param {any} formObject * @return {any} */ export function fixJsonFormOptions(layout: any): any { if (isObject(layout) || isArray(layout)) { forEach(layout, (value, key) => { if (isObject(value) && hasOwn(value, 'options') && isObject(value.options)) { value.titleMap = value.options; delete value.options; } }, 'top-down'); } return layout; }