Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] oneOf/anyOf support #505

Open
wants to merge 7 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/directives/decorators/bootstrap/bootstrap-decorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

array: {template: base + 'array.html', replace: false},
tabarray: {template: base + 'tabarray.html', replace: false},
tabs: {template: base + 'tabs.html', replace: false},
Expand Down
10 changes: 10 additions & 0 deletions src/directives/decorators/bootstrap/formselect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div sf-form-select="form" class="schema-form-form-select {{form.htmlClass}}">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably create a simple one for core.

<legend ng-class="{'sr-only': !showTitle() }">{{ form.title }}</legend>
<div class="help-block" ng-show="form.description" ng-bind-html="form.description"></div>
<select ng-model="selectedForm"
ng-disabled="form.readonly"
class="form-control {{form.fieldHtmlClass}}"
ng-options="form.items.indexOf(item) as item.title for item in form.items">
</select>
<sf-decorator ng-repeat="item in form.items" form="item" ng-show="selectedForm == $index"></sf-decorator>
</div>
52 changes: 52 additions & 0 deletions src/directives/formselect.js
Original file line number Diff line number Diff line change
@@ -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
});
});
}
};
}
]);
141 changes: 133 additions & 8 deletions src/services/schema-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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);
Expand All @@ -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 || []);
Expand Down