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 @@
+
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 || []);