diff --git a/src/directives/decorators/bootstrap/bootstrap-decorator.js b/src/directives/decorators/bootstrap/bootstrap-decorator.js index 4f0d5bb7e..658cb7fa4 100644 --- a/src/directives/decorators/bootstrap/bootstrap-decorator.js +++ b/src/directives/decorators/bootstrap/bootstrap-decorator.js @@ -9,6 +9,7 @@ angular.module('schemaForm').config(['schemaFormDecoratorsProvider', function(de console.log('fieldset children frag', children.childNodes) args.fieldFrag.childNode.appendChild(children); }},*/ + formselect: {template: base + 'formselect.html', replace: false}, array: {template: base + 'array.html', replace: false}, tabarray: {template: base + 'tabarray.html', replace: false}, tabs: {template: base + 'tabs.html', replace: false}, diff --git a/src/directives/decorators/bootstrap/formselect.html b/src/directives/decorators/bootstrap/formselect.html new file mode 100644 index 000000000..8719d8929 --- /dev/null +++ b/src/directives/decorators/bootstrap/formselect.html @@ -0,0 +1,10 @@ +
+ {{ form.title }} +
+ + +
diff --git a/src/directives/formselect.js b/src/directives/formselect.js new file mode 100644 index 000000000..9643d2321 --- /dev/null +++ b/src/directives/formselect.js @@ -0,0 +1,52 @@ +/** + * Directive that handles the model arrays + */ +angular.module('schemaForm').directive('sfFormSelect', ['sfSelect', 'schemaForm', 'sfValidator', 'sfPath', + function(sfSelect, schemaForm, sfValidator, sfPath) { + + return { + restrict: 'A', + scope: true, + require: '?ngModel', + link: function(scope, element, attrs, ngModel) { + scope.selectedForm = 0; + // Keeps the model data for each form + var formData = []; + // TODO: Watch the model value and pick a form to match it + scope.$parent.$watch(attrs.sfFormSelect, function(form) { + if (!form) { + return; + } + + var model = sfSelect(form.key, scope.model); + + // Watch the model value and change to a form that matches it + var key = sfPath.normalize(form.key); + scope.$parent.$watch('model' + key, function (value) { + model = scope.modelData = value; + // If the selected form still validates, make sure we don't change it + if (angular.isNumber(scope.selectedForm)) { + var r = sfValidator.validate(form.items[scope.selectedForm], model); + if (r.valid) return; + } + // Search for a form that is valid for the given model data + for (var i = 0; i < form.items.length; i++) { + var result = sfValidator.validate(form.items[i], model); + if (result.valid) { + scope.selectedForm = i; + break; + } + } + }); + + // Restore data associated with selected form + scope.$watch('selectedForm', function(selectedForm, oldForm) { + formData[oldForm] = model; + if (formData[selectedForm]) sfSelect(form.key, scope.model, formData[selectedForm]); + // TODO: Fill defaults if we don't have data for this form + }); + }); + } + }; + } +]); diff --git a/src/services/schema-form.js b/src/services/schema-form.js index feaec7293..0acf631bb 100644 --- a/src/services/schema-form.js +++ b/src/services/schema-form.js @@ -6,14 +6,16 @@ angular.module('schemaForm').provider('schemaForm', ['sfPathProvider', function(sfPathProvider) { var stripNullType = function(type) { - if (Array.isArray(type) && type.length == 2) { - if (type[0] === 'null') - return type[1]; - if (type[1] === 'null') - return type[0]; + if (Array.isArray(type) && type.length > 1) { + var filtered = type.filter(function (type) { + return type !== 'null' + }); + if (filtered.length == 1) + return filtered[0]; + return filtered; } return type; - } + }; //Creates an default titleMap list from an enum, i.e. a list of strings. var enumToTitleMap = function(enm) { @@ -43,8 +45,71 @@ angular.module('schemaForm').provider('schemaForm', return titleMap; }; + var extendSchemas = function(obj1, obj2) { + obj1 = angular.extend({}, obj1); + obj2 = angular.extend({}, obj2); + + var self = this; + var extended = {}; + angular.forEach(obj1, function(val,prop) { + // If this key is also defined in obj2, merge them + if(typeof obj2[prop] !== 'undefined') { + // Required arrays should be unioned together + if(prop === 'required' && typeof val === 'object' && Array.isArray(val)) { + // Union arrays and unique + extended.required = val.concat(obj2[prop]).reduce(function(p, c) { + if (p.indexOf(c) < 0) p.push(c); + return p; + }, []); + } + // Type should be intersected and is either an array or string + else if(prop === 'type' && (typeof val === 'string' || Array.isArray(val))) { + // Make sure we're dealing with arrays + if(typeof val === 'string') val = [val]; + if(typeof obj2.type === 'string') obj2.type = [obj2.type]; + + + extended.type = val.filter(function(n) { + return obj2.type.indexOf(n) !== -1; + }); + + // If there's only 1 type, use a string instead of array + if(extended.type.length === 1) { + extended.type = extended.type[0]; + } + } + // All other arrays should be intersected (enum, etc.) + else if(typeof val === 'object' && Array.isArray(val)){ + extended[prop] = val.filter(function(n) { + return obj2[prop].indexOf(n) !== -1; + }); + } + // Objects should be recursively merged + else if(typeof val === 'object' && val !== null) { + extended[prop] = extendSchemas(val, obj2[prop]); + } + // Otherwise, use the first value + else { + extended[prop] = val; + } + } + // Otherwise, just use the one in obj1 + else { + extended[prop] = val; + } + }); + // Properties in obj2 that aren't in obj1 + angular.forEach(obj2, function(val, prop) { + if(typeof obj1[prop] === 'undefined') { + extended[prop] = val; + } + }); + + return extended; + }; + var defaultFormDefinition = function(name, schema, options) { - var rules = defaults[stripNullType(schema.type)]; + var rules = defaults['any'].concat(defaults[stripNullType(schema.type)]); if (rules) { var def; for (var i = 0; i < rules.length; i++) { @@ -228,9 +293,52 @@ angular.module('schemaForm').provider('schemaForm', }; + var formselect = function(name, schema, options) { + var types = stripNullType(schema.type); + if (!(schema.oneOf || schema.anyOf || angular.isArray(types))) return; + var f = stdFormObj(name, schema, options); + f.type = 'formselect'; + f.key = options.path; + var schemas = []; + // TODO: What if there are more than one of these keys in the same schema? + if (angular.isArray(types)) { + angular.forEach(types, function(type) { + schemas.push(extendSchemas({type: type}, schema)); + }) + } else { + angular.forEach(schema.anyOf || schema.oneOf, function(value) { + var extended = extendSchemas(value, schema); + delete extended.oneOf; + delete extended.anyOf; + schemas.push(extended) + }) + } + f.items = []; + angular.forEach(schemas, function(s) { + var subForm = defaultFormDefinition(name, s, options); + subForm.notitle = true; + f.items.push(subForm); + }); + + return f; + }; + + var allof = function(name, schema, options) { + if (schema.allOf) { + var extended = schema; + var allOf = schema.allOf; + delete schema.allOf; + angular.forEach(allOf, function(s) { + extended = extendSchemas(s, extended); + }); + return defaultFormDefinition(name, extended, options); + } + }; + //First sorted by schema type then a list. //Order has importance. First handler returning an form snippet will be used. var defaults = { + any: [allof, formselect], string: [select, text], object: [fieldset], number: [number], @@ -434,7 +542,6 @@ angular.module('schemaForm').provider('schemaForm', path = path || []; var traverse = function(schema, fn, path) { - fn(schema, path); angular.forEach(schema.properties, function(prop, name) { var currentPath = path.slice(); currentPath.push(name); @@ -446,6 +553,24 @@ angular.module('schemaForm').provider('schemaForm', var arrPath = path.slice(); arrPath.push(''); traverse(schema.items, fn, arrPath); } + if (schema.dependencies) { + angular.forEach(schema.dependencies, function(value, key) { + if(typeof value === "object" && !(Array.isArray(value))) { + traverse(value, fn, path); + } + }); + } + if (schema.not) { + traverse(schema.not, fn, path) + } + angular.forEach(['allOf', 'oneOf', 'anyOf'], function(prop) { + if (schema[prop]) { + angular.forEach(schema[prop], function(value) { + traverse(value, fn, path); + }) + } + }); + fn(schema, path); }; traverse(schema, fn, path || []);