UNPKG

@talend/json-schema-form-core

Version:

JSON-Schema and JSON-UI-Schema utilities for form generation.

292 lines (262 loc) 8.15 kB
import canonicalTitleMap from './canonical-title-map'; import { stringify } from './sf-path'; /* Utils */ const stripNullType = type => { if (Array.isArray(type) && type.length === 2) { if (type[0] === 'null') { return type[1]; } if (type[1] === 'null') { return type[0]; } } return type; }; // Creates an default titleMap list from an enum, i.e. a list of strings. const enumToTitleMap = enm => { const titleMap: any = []; // canonical titleMap format is a list. enm.forEach(name => { titleMap.push({ name, value: name }); }); return titleMap; }; /** * Creates a default form definition from a schema. */ export function defaultFormDefinition(schemaTypes, name, schema, options) { const rules = schemaTypes[stripNullType(schema.type)]; if (rules) { let def; // We give each rule a possibility to recurse it's children. const innerDefaultFormDefinition = (childName, childSchema, childOptions) => defaultFormDefinition(schemaTypes, childName, childSchema, childOptions); for (let i = 0; i < rules.length; i++) { def = rules[i](name, schema, options, innerDefaultFormDefinition); // first handler in list that actually returns something is our handler! if (def) { // Do we have form defaults in the schema under the x-schema-form-attribute? if (def.schema['x-schema-form']) { Object.assign(def, def.schema['x-schema-form']); } return def; } } } } /** * Creates a form object with all common properties */ export function stdFormObj(name, schema, options) { options = options || {}; // The Object.assign used to be a angular.copy. Should work though. const f = options.global && options.global.formDefaults ? Object.assign({}, options.global.formDefaults) : {}; if (options.global && options.global.supressPropertyTitles === true) { f.title = schema.title; } else { f.title = schema.title || name; } if (schema.description) { f.description = schema.description; } if (options.required === true || schema.required === true) { f.required = true; } if (schema.maxLength) { f.maxlength = schema.maxLength; } if (schema.minLength) { f.minlength = schema.minLength; } if (schema.readOnly || schema.readonly) { f.readonly = true; } if (schema.minimum) { f.minimum = schema.minimum + (schema.exclusiveMinimum ? 1 : 0); } if (schema.maximum) { f.maximum = schema.maximum - (schema.exclusiveMaximum ? 1 : 0); } // Non standard attributes (DONT USE DEPRECATED) // If you must set stuff like this in the schema use the x-schema-form attribute if (schema.validationMessage) { f.validationMessage = schema.validationMessage; } if (schema.enumNames) { f.titleMap = canonicalTitleMap(schema.enumNames, schema.enum); } f.schema = schema; // Ng model options doesn't play nice with undefined, might be defined // globally though f.ngModelOptions = f.ngModelOptions || {}; return f; } /*** Schema types to form type mappings, with defaults ***/ export function text(name, schema, options) { if (stripNullType(schema.type) === 'string' && !schema.enum) { const f = stdFormObj(name, schema, options); f.key = options.path; f.type = 'text'; options.lookup[stringify(options.path)] = f; return f; } } // default in json form for number and integer is a text field // input type="number" would be more suitable don't ya think? export function number(name, schema, options) { if (stripNullType(schema.type) === 'number') { const f = stdFormObj(name, schema, options); f.key = options.path; f.type = 'number'; options.lookup[stringify(options.path)] = f; return f; } } export function integer(name, schema, options) { if (stripNullType(schema.type) === 'integer') { const f = stdFormObj(name, schema, options); f.key = options.path; f.type = 'number'; options.lookup[stringify(options.path)] = f; return f; } } export function checkbox(name, schema, options) { if (stripNullType(schema.type) === 'boolean') { const f = stdFormObj(name, schema, options); f.key = options.path; f.type = 'checkbox'; options.lookup[stringify(options.path)] = f; return f; } } export function select(name, schema, options) { if (stripNullType(schema.type) === 'string' && schema.enum) { const f = stdFormObj(name, schema, options); f.key = options.path; f.type = 'select'; if (!f.titleMap) { f.titleMap = enumToTitleMap(schema.enum); } options.lookup[stringify(options.path)] = f; return f; } } export function checkboxes(name, schema, options) { if (stripNullType(schema.type) === 'array' && schema.items && schema.items.enum) { const f = stdFormObj(name, schema, options); f.key = options.path; f.type = 'checkboxes'; if (!f.titleMap) { f.titleMap = enumToTitleMap(schema.items.enum); } options.lookup[stringify(options.path)] = f; return f; } } export function fieldset(name, schema, options, defaultFormDef) { if (stripNullType(schema.type) === 'object') { const f = stdFormObj(name, schema, options); f.type = 'fieldset'; f.key = options.path; f.items = []; options.lookup[stringify(options.path)] = f; // recurse down into properties if (schema.properties) { Object.keys(schema.properties).forEach(key => { const value = schema.properties[key]; const path = options.path.slice(); path.push(key); if (options.ignore[stringify(path)] !== true) { const required = schema.required && schema.required.indexOf(key) !== -1; const def = defaultFormDef(key, value, { path, required: required || false, lookup: options.lookup, ignore: options.ignore, global: options.global, }); if (def) { f.items.push(def); } } }); } return f; } } export function array(name, schema, options, defaultFormDef) { if (stripNullType(schema.type) === 'array') { const f = stdFormObj(name, schema, options); f.type = 'array'; f.key = options.path; options.lookup[stringify(options.path)] = f; const required = schema.required && schema.required.indexOf(options.path[options.path.length - 1]) !== -1; // The default is to always just create one child. This works since if the // schemas items declaration is of type: "object" then we get a fieldset. // We also follow json form notatation, adding empty brackets "[]" to // signify arrays. const arrPath = options.path.slice(); arrPath.push(''); f.items = [ defaultFormDef(name, schema.items, { path: arrPath, required: required || false, lookup: options.lookup, ignore: options.ignore, global: options.global, }), ]; return f; } } export function createDefaults() { // First sorted by schema type then a list. // Order has importance. First handler returning an form snippet will be used. return { string: [select, text], object: [fieldset], number: [number], integer: [integer], boolean: [checkbox], array: [checkboxes, array], }; } /** * Create form defaults from schema */ export function defaultForm( schema: any, defaultSchemaTypes: any, ignore?: any, globalOptions?: any, ) { const form: any[] = []; const lookup = {}; // Map path => form obj for fast lookup in merging ignore = ignore || {}; globalOptions = globalOptions || {}; defaultSchemaTypes = defaultSchemaTypes || createDefaults(); if (schema.properties) { Object.keys(schema.properties).forEach(key => { if (ignore[key] !== true) { const required = schema.required && schema.required.indexOf(key) !== -1; const def: any = defaultFormDefinition(defaultSchemaTypes, key, schema.properties[key], { path: [key], // Path to this property in bracket notation. lookup: lookup, // Extra map to register with. Optimization for merger. ignore: ignore, // The ignore list of paths (sans root level name) required: required, // Is it required? (v4 json schema style) global: globalOptions, // Global options, including form defaults }); if (def) { form.push(def); } } }); } else { throw new Error('Not implemented. Only type "object" allowed at root level of schema.'); } return { form: form, lookup: lookup }; }