diff --git a/.jscs.json b/.jscs.json
new file mode 100644
index 000000000..5d5a43647
--- /dev/null
+++ b/.jscs.json
@@ -0,0 +1,4 @@
+{
+ "preset": "google",
+ "maximumLineLength": 100
+}
diff --git a/.travis.yml b/.travis.yml
index 20fd86b6a..70e0bf43c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,7 @@
language: node_js
node_js:
- 0.10
+
+before_script:
+ - npm install -g bower
+ - bower install
diff --git a/CHANGELOG b/CHANGELOG
index a3ead0886..258ceef66 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,10 @@
+v0.7.2
+------
+ * Add-ons now have their own repos, and there is a colorpicker!
+ * 'arrayIndex' is exposed as a locals in conditionals
+ * New tab in tabarrays gets focus.
+ * We now follow google javscript code guidelines, policed by jscs.
+
v0.7.1
------
Thanks to @torstenrudolf, this release is basically his PR:s.
diff --git a/README.md b/README.md
index 12b11bb53..7aa24109e 100644
--- a/README.md
+++ b/README.md
@@ -94,9 +94,7 @@ You can also just download the contents of the `dist/` folder and add dependenci
### Dependencies
-Schema form has a lot of dependencies, most of which are optional. Therefor
-
-Schema Form depends on:
+Schema form has a lot of dependencies, most of which are optional. Schema Form depends on:
1. [AngularJS](https://angularjs.org/) version 1.3.x is recomended. Version 1.2.x
has some limitation. See [known limitations](docs/knownlimitations.md).
@@ -109,19 +107,40 @@ If you install via bower you get all of the above except bootstrap since we
don't want to push a certain version or flavor on you. Also make
sure you got the angular version you actually want.
+
#### Additional dependecies
-1. If you want to use the date picker, you'll also need [jQuery](https://github.com/jquery/jquery) and [pickadate.js](http://amsul.ca/pickadate.js/)
-2. If you'd like to use drag-and-drop reordering of arrays, you'll also need [ui-sortable](https://github.com/angular-ui/ui-sortable) and its [jQueryUI](http://jqueryui.com/) dependencies. See the *ui-sortable* documentation for details about which parts of jQueryUI are needed. You can safely ignore these if you don't need reordering.
-3. Schema Form provides tabbed arrays through the form type `tabarray`. Tab arrays default to tabs on the left side. For these to work, you'll need to include the CSS from [bootstrap-vertical-tabs](https://github.com/dbtek/bootstrap-vertical-tabs). However, you won't need Bootstrap Vertical Tabs for horizontal tabs (the `tabType: "top"` option).
+1. If you'd like to use drag-and-drop reordering of arrays, you'll also need [ui-sortable](https://github.com/angular-ui/ui-sortable) and its [jQueryUI](http://jqueryui.com/) dependencies. See the *ui-sortable* documentation for details about which parts of jQueryUI are needed. You can safely ignore these if you don't need reordering.
+2. Schema Form provides tabbed arrays through the form type `tabarray`. Tab arrays default to tabs on the left side. For these to work, you'll need to include the CSS from [bootstrap-vertical-tabs](https://github.com/dbtek/bootstrap-vertical-tabs). However, you won't need Bootstrap Vertical Tabs for horizontal tabs (the `tabType: "top"` option).
The minified files include templates - no need to load additional HTML files.
+
+### Script Loading
+
+Schema form is split into two main files, `dist/schema-form.min.js` and
+`dist/boostrap-decorator.min.js` and they need be loaded in that order. AngularJ,
+[tv4](https://github.com/geraintluff/tv4) and [objectpath](https://github.com/mike-marcacci/objectpath)
+also needs to be loaded *before* Schema Form.
+
+
+```html
+
+
+
+
+
+
+```
+
+
Add-ons
------
-There is currently only one add-on, a date picker using the excellent [pickadate.js](http://amsul.ca/pickadate.js/).
+There is currently two add-ons, a date picker and a colorpicker. They have their own repos and you
+can find them here with usage instructions:
-See the [add-on docs](docs/datepicker.md) for usage.
+ * [https://github.com/Textalk/angular-schema-form-datepicker](https://github.com/Textalk/angular-schema-form-datepicker)
+ * [https://github.com/Textalk/angular-schema-form-colorpicker](https://github.com/Textalk/angular-schema-form-colorpicker)
Building
--------
@@ -147,11 +166,13 @@ Unit tests are run with [karma](http://karma-runner.github.io) and written using
To run the tests:
1. Install all dependencies via NPM
-2. Install the Karma CLI
-3. Run the tests
+2. Install dev dependencies with bower.
+3. Install the Karma CLI
+4. Run the tests
```bash
$ npm install
+$ bower install
$ sudo npm install -g karma-cli
$ karma start karma.conf.js
```
@@ -159,7 +180,10 @@ $ karma start karma.conf.js
Contributing
------------
-**Heads up!** Sometime soon we will go over and change the code style to follow
-whatever [jscs](https://github.com/mdevils/node-jscs) says with preset set to 'google'.
+All contributions are welcome! We're trying to use
+[git flow](http://danielkummer.github.io/git-flow-cheatsheet/), so please base any merge request
+on the **development** branch instead of **master**.
-All contributions are welcome! We're trying to use [git flow](http://danielkummer.github.io/git-flow-cheatsheet/), so please base any merge request on the **development** branch instead of **master**.
+Also run any code through the code style checker [jscs](https://github.com/mdevils/node-jscs)
+(or even better use it in your editor) with preset set to `google`. You can also us `gulp jscs` to
+check your code.
diff --git a/bower.json b/bower.json
index 69e45163a..0c09f223a 100644
--- a/bower.json
+++ b/bower.json
@@ -5,7 +5,7 @@
"dist/bootstrap-decorator.min.js",
"dist/bootstrap-datepicker.min.js"
],
- "version": "0.7.1",
+ "version": "0.7.2",
"authors": [
"Textalk",
"David Jensen "
@@ -36,6 +36,10 @@
"objectpath": "~1.0.4"
},
"devDependencies": {
- "angular-ui-ace": "bower"
+ "angular-ui-ace": "bower",
+ "angular-schema-form-datepicker": ">= 0.1.0",
+ "jquery": "~2.1.1",
+ "angular-mocks": ">= 1.2",
+ "angular-schema-form-colorpicker": "~0.1.0"
}
}
diff --git a/dist/bootstrap-datepicker.min.js b/dist/bootstrap-datepicker.min.js
index 9baa01ca1..95eae69cd 100644
--- a/dist/bootstrap-datepicker.min.js
+++ b/dist/bootstrap-datepicker.min.js
@@ -1 +1 @@
-angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/datepicker/datepicker.html",'
')}]),angular.module("schemaForm").directive("pickADate",function(){var e=function(e){return angular.isString(e)||angular.isNumber(e)?new Date(e):e};return{restrict:"A",require:"ngModel",scope:{ngModel:"=",minDate:"=",maxDate:"="},link:function(r,a,t,o){if(a.pickadate){a.pickadate({onClose:function(){a.blur()},formatSubmit:null});var i="yyyy-mm-dd",n=$.fn.pickadate.defaults.format,s=a.pickadate("picker");if(o.$formatters.push(function(e){return angular.isUndefined(e)||null===e?e:(s.set("view",e,{format:t.format||i}),s.set("highlight",e,{format:t.format||i}),s.get("highlight",n))}),o.$parsers.push(function(){return s.get("select",t.format||i)}),angular.isDefined(t.minDate))var c=r.$watch("minDate",function(r){r&&(s.set("min",e(r)),c())},!0);if(angular.isDefined(t.maxDate))var m=r.$watch("maxDate",function(r){r&&(s.set("max",e(r)),m())},!0)}}}}),angular.module("schemaForm").config(["schemaFormProvider","schemaFormDecoratorsProvider","sfPathProvider",function(e,r,a){var t=function(r,t,o){if("string"===t.type&&"date"==t.format){var i=e.stdFormObj(r,t,o);return i.key=o.path,i.type="datepicker",o.lookup[a.stringify(o.path)]=i,i}};e.defaults.string.unshift(t),r.addMapping("bootstrapDecorator","datepicker","directives/decorators/bootstrap/datepicker/datepicker.html"),r.createDirective("datepicker","directives/decorators/bootstrap/datepicker/datepicker.html")}]);
\ No newline at end of file
+angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/datepicker/datepicker.html",'
')}]),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){var t="directives/decorators/bootstrap/";e.createDecorator("bootstrapDecorator",{textarea:t+"textarea.html",fieldset:t+"fieldset.html",array:t+"array.html",tabarray:t+"tabarray.html",tabs:t+"tabs.html",section:t+"section.html",conditional:t+"section.html",actions:t+"actions.html",select:t+"select.html",checkbox:t+"checkbox.html",checkboxes:t+"checkboxes.html",number:t+"default.html",password:t+"default.html",submit:t+"submit.html",button:t+"submit.html",radios:t+"radios.html","radios-inline":t+"radios-inline.html",radiobuttons:t+"radio-buttons.html",help:t+"help.html","default":t+"default.html"},[function(e){return e.readonly&&e.key&&"fieldset"!==e.type?t+"readonly.html":void 0}]),e.createDirectives({textarea:t+"textarea.html",select:t+"select.html",checkbox:t+"checkbox.html",checkboxes:t+"checkboxes.html",number:t+"default.html",submit:t+"submit.html",button:t+"submit.html",text:t+"default.html",date:t+"default.html",password:t+"default.html",datepicker:t+"datepicker.html",input:t+"default.html",radios:t+"radios.html","radios-inline":t+"radios-inline.html",radiobuttons:t+"radio-buttons.html"})}]).directive("sfFieldset",function(){return{transclude:!0,scope:!0,templateUrl:"directives/decorators/bootstrap/fieldset-trcl.html",link:function(e,t,r){e.title=e.$eval(r.title)}}});
\ No newline at end of file
+angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/actions-trcl.html",''),e.put("directives/decorators/bootstrap/actions.html",''),e.put("directives/decorators/bootstrap/array.html",'
')}]),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){var t="directives/decorators/bootstrap/";e.createDecorator("bootstrapDecorator",{textarea:t+"textarea.html",fieldset:t+"fieldset.html",array:t+"array.html",tabarray:t+"tabarray.html",tabs:t+"tabs.html",section:t+"section.html",conditional:t+"section.html",actions:t+"actions.html",select:t+"select.html",checkbox:t+"checkbox.html",checkboxes:t+"checkboxes.html",number:t+"default.html",password:t+"default.html",submit:t+"submit.html",button:t+"submit.html",radios:t+"radios.html","radios-inline":t+"radios-inline.html",radiobuttons:t+"radio-buttons.html",help:t+"help.html","default":t+"default.html"},[function(e){return e.readonly&&e.key&&"fieldset"!==e.type?t+"readonly.html":void 0}]),e.createDirectives({textarea:t+"textarea.html",select:t+"select.html",checkbox:t+"checkbox.html",checkboxes:t+"checkboxes.html",number:t+"default.html",submit:t+"submit.html",button:t+"submit.html",text:t+"default.html",date:t+"default.html",password:t+"default.html",datepicker:t+"datepicker.html",input:t+"default.html",radios:t+"radios.html","radios-inline":t+"radios-inline.html",radiobuttons:t+"radio-buttons.html"})}]).directive("sfFieldset",function(){return{transclude:!0,scope:!0,templateUrl:"directives/decorators/bootstrap/fieldset-trcl.html",link:function(e,t,r){e.title=e.$eval(r.title)}}});
\ No newline at end of file
diff --git a/dist/schema-form.js b/dist/schema-form.js
index 43f168448..9affa22d8 100644
--- a/dist/schema-form.js
+++ b/dist/schema-form.js
@@ -1,3 +1,6 @@
+// Deps is sort of a problem for us, maybe in the future we will ask the user to depend
+// on modules for add-ons
+
var deps = ['ObjectPath'];
try {
//This throws an expection if module does not exist.
@@ -11,53 +14,66 @@ try {
deps.push('ui.sortable');
} catch (e) {}
-angular.module('schemaForm',deps);
-
-angular.module('schemaForm').provider('sfPath',['ObjectPathProvider',function(ObjectPathProvider){
- var ObjectPath = { parse: ObjectPathProvider.parse };
-
- // if we're on Angular 1.2.x, we need to continue using dot notation
- if(angular.version.major === 1 && angular.version.minor < 3) {
- ObjectPath.stringify = function(arr) {
- return Array.isArray(arr) ? arr.join('.') : arr.toString();
- };
- } else {
- ObjectPath.stringify = ObjectPathProvider.stringify;
- }
-
- // we want this to use whichever stringify method is defined above, so we have to copy the code here
- ObjectPath.normalize = function(data, quote){ return ObjectPath.stringify(Array.isArray(data) ? data : ObjectPath.parse(data), quote); }
-
- this.parse = ObjectPath.parse;
- this.stringify = ObjectPath.stringify;
- this.normalize = ObjectPath.normalize;
- this.$get = function(){
- return ObjectPath;
- };
+try {
+ //This throws an expection if module does not exist.
+ angular.module('angularSpectrumColorpicker');
+ deps.push('angularSpectrumColorpicker');
+} catch (e) {}
+
+angular.module('schemaForm', deps);
+
+angular.module('schemaForm').provider('sfPath',
+['ObjectPathProvider', function(ObjectPathProvider) {
+ var ObjectPath = {parse: ObjectPathProvider.parse};
+
+ // if we're on Angular 1.2.x, we need to continue using dot notation
+ if (angular.version.major === 1 && angular.version.minor < 3) {
+ ObjectPath.stringify = function(arr) {
+ return Array.isArray(arr) ? arr.join('.') : arr.toString();
+ };
+ } else {
+ ObjectPath.stringify = ObjectPathProvider.stringify;
+ }
+
+ // We want this to use whichever stringify method is defined above,
+ // so we have to copy the code here.
+ ObjectPath.normalize = function(data, quote) {
+ return ObjectPath.stringify(Array.isArray(data) ? data : ObjectPath.parse(data), quote);
+ };
+
+ this.parse = ObjectPath.parse;
+ this.stringify = ObjectPath.stringify;
+ this.normalize = ObjectPath.normalize;
+ this.$get = function () {
+ return ObjectPath;
+ };
}]);
+
/**
* @ngdoc service
* @name sfSelect
* @kind function
*
- * @description
- * Utility method to access deep properties without
- * throwing errors when things are not defined.
- * Can also set a value in a deep structure, creating objects when missing
- * ex.
- * var foo = Select('address.contact.name',obj)
- * Select('address.contact.name',obj,'Leeroy')
- *
- * @param {string} projection A dot path to the property you want to get/set
- * @param {object} obj (optional) The object to project on, defaults to 'this'
- * @param {Any} value (opional) The value to set, if parts of the path of
- * the projection is missing empty objects will be created.
- * @returns {Any|undefined} returns the value at the end of the projection path
- * or undefined if there is none.
*/
angular.module('schemaForm').factory('sfSelect', ['sfPath', function (sfPath) {
var numRe = /^\d+$/;
+ /**
+ * @description
+ * Utility method to access deep properties without
+ * throwing errors when things are not defined.
+ * Can also set a value in a deep structure, creating objects when missing
+ * ex.
+ * var foo = Select('address.contact.name',obj)
+ * Select('address.contact.name',obj,'Leeroy')
+ *
+ * @param {string} projection A dot path to the property you want to get/set
+ * @param {object} obj (optional) The object to project on, defaults to 'this'
+ * @param {Any} valueToSet (opional) The value to set, if parts of the path of
+ * the projection is missing empty objects will be created.
+ * @returns {Any|undefined} returns the value at the end of the projection path
+ * or undefined if there is none.
+ */
return function(projection, obj, valueToSet) {
if (!obj) {
obj = this;
@@ -108,11 +124,12 @@ angular.module('schemaForm').factory('sfSelect', ['sfPath', function (sfPath) {
};
}]);
-angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider','sfPathProvider',function($compileProvider, sfPathProvider){
+angular.module('schemaForm').provider('schemaFormDecorators',
+['$compileProvider', 'sfPathProvider', function($compileProvider, sfPathProvider) {
var defaultDecorator = '';
var directives = {};
- var templateUrl = function(name,form) {
+ var templateUrl = function(name, form) {
//schemaDecorator is alias for whatever is set as default
if (name === 'sfDecorator') {
name = defaultDecorator;
@@ -122,7 +139,7 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
//rules first
var rules = directive.rules;
- for (var i = 0; i< rules.length; i++) {
+ for (var i = 0; i < rules.length; i++) {
var res = rules[i](form);
if (res) {
return res;
@@ -138,10 +155,9 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
return directive.mappings['default'];
};
-
- var createDirective = function(name){
- $compileProvider.directive(name,['$parse','$compile','$http','$templateCache',
- function($parse, $compile, $http, $templateCache){
+ var createDirective = function(name) {
+ $compileProvider.directive(name, ['$parse', '$compile', '$http', '$templateCache',
+ function($parse, $compile, $http, $templateCache) {
return {
restrict: 'AE',
@@ -149,9 +165,9 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
transclude: false,
scope: true,
require: '?^sfSchema',
- link: function(scope,element,attrs,sfSchema) {
+ link: function(scope, element, attrs, sfSchema) {
//rebind our part of the form to the scope.
- var once = scope.$watch(attrs.form,function(form){
+ var once = scope.$watch(attrs.form, function(form) {
if (form) {
scope.form = form;
@@ -159,10 +175,14 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
//ok let's replace that template!
//We do this manually since we need to bind ng-model properly and also
//for fieldsets to recurse properly.
- var url = templateUrl(name,form);
- $http.get(url,{ cache: $templateCache }).then(function(res){
- var key = form.key ? sfPathProvider.stringify(form.key).replace(/"/g, '"') : '';
- var template = res.data.replace(/\$\$value\$\$/g,'model'+(key[0] !== '['?'.':'')+key);
+ var url = templateUrl(name, form);
+ $http.get(url, {cache: $templateCache}).then(function(res) {
+ var key = form.key ?
+ sfPathProvider.stringify(form.key).replace(/"/g, '"') : '';
+ var template = res.data.replace(
+ /\$\$value\$\$/g,
+ 'model' + (key[0] !== '[' ? '.' : '') + key
+ );
element.html(template);
$compile(element.contents())(scope);
});
@@ -175,17 +195,17 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
return scope.form && scope.form.notitle !== true && scope.form.title;
};
- scope.listToCheckboxValues = function(list){
+ scope.listToCheckboxValues = function(list) {
var values = {};
- angular.forEach(list,function(v){
+ angular.forEach(list, function(v) {
values[v] = true;
});
return values;
};
- scope.checkboxValuesToList = function(values){
+ scope.checkboxValuesToList = function(values) {
var lst = [];
- angular.forEach(values,function(v,k){
+ angular.forEach(values, function(v, k) {
if (v) {
lst.push(k);
}
@@ -193,15 +213,15 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
return lst;
};
- scope.buttonClick = function($event,form) {
+ scope.buttonClick = function($event, form) {
if (angular.isFunction(form.onClick)) {
- form.onClick($event,form);
+ form.onClick($event, form);
} else if (angular.isString(form.onClick)) {
if (sfSchema) {
//evaluating in scope outside of sfSchemas isolated scope
- sfSchema.evalInParentScope(form.onClick,{'$event':$event,form:form});
+ sfSchema.evalInParentScope(form.onClick, {'$event': $event, form: form});
} else {
- scope.$eval(form.onClick,{'$event':$event,form:form});
+ scope.$eval(form.onClick, {'$event': $event, form: form});
}
}
};
@@ -213,13 +233,13 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
* @param {Object} locals (optional)
* @return {Any} the result of the expression
*/
- scope.evalExpr = function(expression,locals) {
+ scope.evalExpr = function(expression, locals) {
if (sfSchema) {
//evaluating in scope outside of sfSchemas isolated scope
- return sfSchema.evalInParentScope(expression,locals);
+ return sfSchema.evalInParentScope(expression, locals);
}
- return scope.$eval(expression,locals);
+ return scope.$eval(expression, locals);
};
/**
@@ -229,10 +249,10 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
* @param {Object} locals (optional)
* @return {Any} the result of the expression
*/
- scope.evalInScope = function(expression,locals) {
- if (expression) {
- return scope.$eval(expression,locals);
- }
+ scope.evalInScope = function(expression, locals) {
+ if (expression) {
+ return scope.$eval(expression, locals);
+ }
};
/**
@@ -248,9 +268,12 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
return scope.form.validationMessage;
}
- return scope.form.validationMessage[schemaError.code] || scope.form.validationMessage['default'];
+ return scope.form.validationMessage[schemaError.code] ||
+ scope.form.validationMessage['default'];
} else {
- return scope.form.validationMessage.required || scope.form.validationMessage['default'] || scope.form.validationMessage;
+ return scope.form.validationMessage.required ||
+ scope.form.validationMessage['default'] ||
+ scope.form.validationMessage;
}
}
@@ -260,35 +283,36 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
}
//Otherwise we only use required so it must be it.
- return "Required";
+ return 'Required';
};
}
};
- }]);
+ }
+ ]);
};
- var createManualDirective = function(type,templateUrl,transclude) {
- transclude = angular.isDefined(transclude)? transclude : false;
- $compileProvider.directive('sf'+angular.uppercase(type[0])+type.substr(1), function(){
+ var createManualDirective = function(type, templateUrl, transclude) {
+ transclude = angular.isDefined(transclude) ? transclude : false;
+ $compileProvider.directive('sf' + angular.uppercase(type[0]) + type.substr(1), function() {
return {
- restrict: "EAC",
+ restrict: 'EAC',
scope: true,
replace: true,
transclude: transclude,
template: '',
- link: function(scope,element,attrs) {
+ link: function(scope, element, attrs) {
var watchThis = {
'items': 'c',
'titleMap': 'c',
'schema': 'c'
};
- var form = { type: type };
+ var form = {type: type};
var once = true;
- angular.forEach(attrs,function(value,name){
+ angular.forEach(attrs, function(value, name) {
if (name[0] !== '$' && name.indexOf('ng') !== 0 && name !== 'sfField') {
- var updateForm = function(val){
+ var updateForm = function(val) {
if (angular.isDefined(val) && val !== form[name]) {
form[name] = val;
@@ -303,17 +327,17 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
if (name === 'model') {
//"model" is bound to scope under the name "model" since this is what the decorators
//know and love.
- scope.$watch(value,function(val){
+ scope.$watch(value, function(val) {
if (val && scope.model !== val) {
scope.model = val;
}
});
} else if (watchThis[name] === 'c') {
//watch collection
- scope.$watchCollection(value,updateForm);
+ scope.$watchCollection(value, updateForm);
} else {
//$observe
- attrs.$observe(name,updateForm);
+ attrs.$observe(name, updateForm);
}
}
});
@@ -322,8 +346,6 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
});
};
-
-
/**
* Create a decorator directive and its sibling "manual" use directives.
* The directive can be used to create form fields or other form entities.
@@ -335,11 +357,12 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
**
* @param {string} name directive name (CamelCased)
* @param {Object} mappings, an object that maps "type" => "templateUrl"
- * @param {Array} rules (optional) a list of functions, function(form){}, that are each tried in turn,
+ * @param {Array} rules (optional) a list of functions, function(form) {}, that are each tried in
+ * turn,
* if they return a string then that is used as the templateUrl. Rules come before
* mappings.
*/
- this.createDecorator = function(name,mappings,rules){
+ this.createDecorator = function(name, mappings, rules) {
directives[name] = {
mappings: mappings || {},
rules: rules || []
@@ -371,8 +394,8 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
* @param {Object} mappings
*/
this.createDirectives = function(mappings) {
- angular.forEach(mappings,function(url,type){
- createManualDirective(type,url);
+ angular.forEach(mappings, function(url, type) {
+ createManualDirective(type, url);
});
};
@@ -393,15 +416,14 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
* @param {String} type Form type for the mapping
* @param {String} url The template url
*/
- this.addMapping = function(name,type,url) {
+ this.addMapping = function(name, type, url) {
if (directives[name]) {
directives[name].mappings[type] = url;
}
};
-
//Service is just a getter for directive mappings and rules
- this.$get = function(){
+ this.$get = function() {
return {
directive: function(name) {
return directives[name];
@@ -410,7 +432,6 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
};
};
-
//Create a default directive
createDirective('sfDecorator');
@@ -421,13 +442,14 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider'
* This service is not that useful outside of schema form directive
* but makes the code more testable.
*/
-angular.module('schemaForm').provider('schemaForm',['sfPathProvider', function(sfPathProvider){
+angular.module('schemaForm').provider('schemaForm',
+['sfPathProvider', function(sfPathProvider) {
//Creates an default titleMap list from an enum, i.e. a list of strings.
var enumToTitleMap = function(enm) {
var titleMap = []; //canonical titleMap format is a list.
- enm.forEach(function(name){
- titleMap.push({ name: name, value: name});
+ enm.forEach(function(name) {
+ titleMap.push({name: name, value: name});
});
return titleMap;
};
@@ -437,20 +459,20 @@ angular.module('schemaForm').provider('schemaForm',['sfPathProvider', function(s
var canonicalTitleMap = function(titleMap) {
if (!angular.isArray(titleMap)) {
var canonical = [];
- angular.forEach(titleMap, function(name,value) {
- canonical.push({ name: name, value: value });
+ angular.forEach(titleMap, function(name, value) {
+ canonical.push({name: name, value: value});
});
return canonical;
}
return titleMap;
};
- var defaultFormDefinition = function(name,schema,options){
+ var defaultFormDefinition = function(name, schema, options) {
var rules = defaults[schema.type];
if (rules) {
var def;
- for (var i=0;i 1) {
- subForm = { type: 'section', items: form.items };
+ return {
+ restrict: 'A',
+ scope: true,
+ require: '?ngModel',
+ link: function(scope, element, attrs, ngModel) {
+ var formDefCache = {};
+
+ // Watch for the form definition and then rewrite it.
+ // It's the (first) array part of the key, '[]' that needs a number
+ // corresponding to an index of the form.
+ var once = scope.$watch(attrs.sfArray, function(form) {
+
+ // An array model always needs a key so we know what part of the model
+ // to look at. This makes us a bit incompatible with JSON Form, on the
+ // other hand it enables two way binding.
+ var list = sfSelect(form.key, scope.model);
+
+ // Since ng-model happily creates objects in a deep path when setting a
+ // a value but not arrays we need to create the array.
+ if (angular.isUndefined(list)) {
+ list = [];
+ sfSelect(form.key, scope.model, list);
}
- }
-
- // We ceate copies of the form on demand, caching them for
- // later requests
- scope.copyWithIndex = function(index) {
- if (!formDefCache[index]) {
- if (subForm) {
- var copy = angular.copy(subForm);
- copy.arrayIndex= index;
- schemaForm.traverseForm(copy, setIndex(index));
- formDefCache[index] = copy;
+ scope.modelArray = list;
+
+ // Arrays with titleMaps, i.e. checkboxes doesn't have items.
+ if (form.items) {
+
+ // To be more compatible with JSON Form we support an array of items
+ // in the form definition of "array" (the schema just a value).
+ // for the subforms code to work this means we wrap everything in a
+ // section. Unless there is just one.
+ var subForm = form.items[0];
+ if (form.items.length > 1) {
+ subForm = {type: 'section', items: form.items};
}
}
- return formDefCache[index];
- };
+ // We ceate copies of the form on demand, caching them for
+ // later requests
+ scope.copyWithIndex = function(index) {
+ if (!formDefCache[index]) {
+ if (subForm) {
+ var copy = angular.copy(subForm);
+ copy.arrayIndex = index;
+ schemaForm.traverseForm(copy, setIndex(index));
+ formDefCache[index] = copy;
+ }
+ }
+ return formDefCache[index];
+ };
- scope.appendToArray = function() {
- var len = list.length;
- var copy = scope.copyWithIndex(len);
- schemaForm.traverseForm(copy, function(part){
- if (part.key && angular.isDefined(part.default)) {
- sfSelect(part.key, scope.model, part.default);
+ scope.appendToArray = function() {
+ var len = list.length;
+ var copy = scope.copyWithIndex(len);
+ schemaForm.traverseForm(copy, function(part) {
+ if (part.key && angular.isDefined(part.default)) {
+ sfSelect(part.key, scope.model, part.default);
+ }
+ });
+
+ // If there are no defaults nothing is added so we need to initialize
+ // the array. undefined for basic values, {} or [] for the others.
+ if (len === list.length) {
+ var type = sfSelect('schema.items.type', form);
+ var dflt;
+ if (type === 'object') {
+ dflt = {};
+ } else if (type === 'array') {
+ dflt = [];
+ }
+ list.push(dflt);
}
- });
- // If there are no defaults nothing is added so we need to initialize
- // the array. undefined for basic values, {} or [] for the others.
- if (len === list.length) {
- var type = sfSelect('schema.items.type',form);
- var dflt;
- if (type === 'object') {
- dflt = {};
- } else if (type === 'array') {
- dflt = [];
+ // Trigger validation.
+ if (scope.validateArray) {
+ scope.validateArray();
}
- list.push(dflt);
- }
+ return list;
+ };
- // Trigger validation.
- if (scope.validateArray) {
- scope.validateArray();
- }
- };
+ scope.deleteFromArray = function(index) {
+ list.splice(index, 1);
- scope.deleteFromArray = function(index) {
- list.splice(index,1);
+ // Trigger validation.
+ if (scope.validateArray) {
+ scope.validateArray();
+ }
+ };
- // Trigger validation.
- if (scope.validateArray) {
- scope.validateArray();
+ // Always start with one empty form unless configured otherwise.
+ // Special case: don't do it if form has a titleMap
+ if (!form.titleMap && form.startEmpty !== true && list.length === 0) {
+ scope.appendToArray();
}
- };
-
- // Always start with one empty form unless configured otherwise.
- // Special case: don't do it if form has a titleMap
- if (!form.titleMap && form.startEmpty !== true && list.length === 0) {
- scope.appendToArray();
- }
- // Title Map handling
- // If form has a titleMap configured we'd like to enable looping over
- // titleMap instead of modelArray, this is used for intance in
- // checkboxes. So instead of variable number of things we like to create
- // a array value from a subset of values in the titleMap.
- // The problem here is that ng-model on a checkbox doesn't really map to
- // a list of values. This is here to fix that.
- if (form.titleMap && form.titleMap.length > 0) {
- scope.titleMapValues = [];
-
- // We watch the model for changes and the titleMapValues to reflect
- // the modelArray
- var updateTitleMapValues = function(arr) {
+ // Title Map handling
+ // If form has a titleMap configured we'd like to enable looping over
+ // titleMap instead of modelArray, this is used for intance in
+ // checkboxes. So instead of variable number of things we like to create
+ // a array value from a subset of values in the titleMap.
+ // The problem here is that ng-model on a checkbox doesn't really map to
+ // a list of values. This is here to fix that.
+ if (form.titleMap && form.titleMap.length > 0) {
scope.titleMapValues = [];
- arr = arr || [];
- form.titleMap.forEach(function(item) {
- scope.titleMapValues.push( arr.indexOf(item.value) !== -1 );
- });
+ // We watch the model for changes and the titleMapValues to reflect
+ // the modelArray
+ var updateTitleMapValues = function(arr) {
+ scope.titleMapValues = [];
+ arr = arr || [];
- };
- //Catch default values
- updateTitleMapValues(scope.modelArray);
- scope.$watchCollection('modelArray',updateTitleMapValues);
-
- //To get two way binding we also watch our titleMapValues
- scope.$watchCollection('titleMapValues', function(vals) {
- if (vals) {
- var arr = scope.modelArray;
-
- // Apparently the fastest way to clear an array, readable too.
- // http://jsperf.com/array-destroy/32
- while (arr.length > 0) {
- arr.shift();
- }
+ form.titleMap.forEach(function(item) {
+ scope.titleMapValues.push(arr.indexOf(item.value) !== -1);
+ });
- form.titleMap.forEach(function(item,index) {
- if (vals[index]) {
- arr.push(item.value);
+ };
+ //Catch default values
+ updateTitleMapValues(scope.modelArray);
+ scope.$watchCollection('modelArray', updateTitleMapValues);
+
+ //To get two way binding we also watch our titleMapValues
+ scope.$watchCollection('titleMapValues', function(vals) {
+ if (vals) {
+ var arr = scope.modelArray;
+
+ // Apparently the fastest way to clear an array, readable too.
+ // http://jsperf.com/array-destroy/32
+ while (arr.length > 0) {
+ arr.shift();
}
- });
- }
- });
- }
+ form.titleMap.forEach(function(item, index) {
+ if (vals[index]) {
+ arr.push(item.value);
+ }
+ });
+ }
+ });
+ }
- // If there is a ngModel present we need to validate when asked.
- if (ngModel) {
- var error;
-
- scope.validateArray = function() {
- // The actual content of the array is validated by each field
- // so we settle for checking validations specific to arrays
-
- // Since we prefill with empty arrays we can get the funny situation
- // where the array is required but empty in the gui but still validates.
- // Thats why we check the length.
- var result = sfValidator.validate(
- form,
- scope.modelArray.length > 0 ? scope.modelArray : undefined
- );
- if (result.valid === false &&
- result.error &&
- (result.error.dataPath === '' ||
- result.error.dataPath === '/'+form.key[form.key.length - 1])) {
-
- // Set viewValue to trigger $dirty on field. If someone knows a
- // a better way to do it please tell.
- ngModel.$setViewValue(scope.modelArray);
- error = result.error;
- ngModel.$setValidity('schema', false);
-
- } else {
- ngModel.$setValidity('schema', true);
- }
- };
+ // If there is a ngModel present we need to validate when asked.
+ if (ngModel) {
+ var error;
+
+ scope.validateArray = function() {
+ // The actual content of the array is validated by each field
+ // so we settle for checking validations specific to arrays
+
+ // Since we prefill with empty arrays we can get the funny situation
+ // where the array is required but empty in the gui but still validates.
+ // Thats why we check the length.
+ var result = sfValidator.validate(
+ form,
+ scope.modelArray.length > 0 ? scope.modelArray : undefined
+ );
+ if (result.valid === false &&
+ result.error &&
+ (result.error.dataPath === '' ||
+ result.error.dataPath === '/' + form.key[form.key.length - 1])) {
+
+ // Set viewValue to trigger $dirty on field. If someone knows a
+ // a better way to do it please tell.
+ ngModel.$setViewValue(scope.modelArray);
+ error = result.error;
+ ngModel.$setValidity('schema', false);
- scope.$on('schemaFormValidate',scope.validateArray);
+ } else {
+ ngModel.$setValidity('schema', true);
+ }
+ };
+ scope.$on('schemaFormValidate', scope.validateArray);
- scope.hasSuccess = function(){
- return ngModel.$valid && !ngModel.$pristine;
- };
+ scope.hasSuccess = function() {
+ return ngModel.$valid && !ngModel.$pristine;
+ };
- scope.hasError = function(){
- return ngModel.$invalid;
- };
+ scope.hasError = function() {
+ return ngModel.$invalid;
+ };
- scope.schemaError = function() {
- return error;
- };
+ scope.schemaError = function() {
+ return error;
+ };
- }
+ }
- once();
- });
- }
- };
-}]);
+ once();
+ });
+ }
+ };
+ }
+]);
/**
* A version of ng-changed that only listens if
@@ -1123,22 +1134,22 @@ function(sfSelect, schemaForm, sfValidator) {
* Takes the form definition as argument.
* If the form definition has a "onChange" defined as either a function or
*/
-angular.module('schemaForm').directive('sfChanged',function(){
+angular.module('schemaForm').directive('sfChanged', function() {
return {
require: 'ngModel',
restrict: 'AC',
scope: false,
- link: function(scope,element,attrs,ctrl) {
+ link: function(scope, element, attrs, ctrl) {
var form = scope.$eval(attrs.sfChanged);
//"form" is really guaranteed to be here since the decorator directive
//waits for it. But best be sure.
if (form && form.onChange) {
ctrl.$viewChangeListeners.push(function() {
- if (angular.isFunction(form.onChange)) {
- form.onChange(ctrl.$modelValue,form);
- } else {
- scope.evalExpr(form.onChange,{ 'modelValue': ctrl.$modelValue, form: form });
- }
+ if (angular.isFunction(form.onChange)) {
+ form.onChange(ctrl.$modelValue, form);
+ } else {
+ scope.evalExpr(form.onChange, {'modelValue': ctrl.$modelValue, form: form});
+ }
});
}
}
@@ -1147,125 +1158,126 @@ angular.module('schemaForm').directive('sfChanged',function(){
/*
FIXME: real documentation
-
+
*/
angular.module('schemaForm')
.directive('sfSchema',
- ['$compile','schemaForm','schemaFormDecorators','sfSelect',
-function($compile, schemaForm, schemaFormDecorators, sfSelect){
-
- var SNAKE_CASE_REGEXP = /[A-Z]/g;
- function snake_case(name, separator){
- separator = separator || '_';
- return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
- return (pos ? separator : '') + letter.toLowerCase();
- });
- }
+['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect',
+ function($compile, schemaForm, schemaFormDecorators, sfSelect) {
+
+ var SNAKE_CASE_REGEXP = /[A-Z]/g;
+ var snakeCase = function(name, separator) {
+ separator = separator || '_';
+ return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+ return (pos ? separator : '') + letter.toLowerCase();
+ });
+ };
- return {
- scope: {
- schema: '=sfSchema',
- initialForm: '=sfForm',
- model: '=sfModel'
- },
- controller: ['$scope',function($scope){
- this.evalInParentScope = function(expr,locals){
- return $scope.$parent.$eval(expr,locals);
- };
- }],
- replace: false,
- restrict: "A",
- transclude: true,
- require: '?form',
- link: function(scope,element,attrs,formCtrl,transclude) {
-
- //expose form controller on scope so that we don't force authors to use name on form
- scope.formCtrl = formCtrl;
-
- //We'd like to handle existing markup,
- //besides using it in our template we also
- //check for ng-model and add that to an ignore list
- //i.e. even if form has a definition for it or form is ["*"]
- //we don't generate it.
- var ignore = {};
- transclude(scope,function(clone){
- clone.addClass('schema-form-ignore');
- element.prepend(clone);
-
- if (element[0].querySelectorAll) {
- var models = element[0].querySelectorAll('[ng-model]');
- if (models){
- for (var i=0; i < models.length; i++){
- var key = models[i].getAttribute('ng-model');
- //skip first part before .
- ignore[key.substring(key.indexOf('.')+1)] = true;
+ return {
+ scope: {
+ schema: '=sfSchema',
+ initialForm: '=sfForm',
+ model: '=sfModel'
+ },
+ controller: ['$scope', function($scope) {
+ this.evalInParentScope = function(expr, locals) {
+ return $scope.$parent.$eval(expr, locals);
+ };
+ }],
+ replace: false,
+ restrict: 'A',
+ transclude: true,
+ require: '?form',
+ link: function(scope, element, attrs, formCtrl, transclude) {
+
+ //expose form controller on scope so that we don't force authors to use name on form
+ scope.formCtrl = formCtrl;
+
+ //We'd like to handle existing markup,
+ //besides using it in our template we also
+ //check for ng-model and add that to an ignore list
+ //i.e. even if form has a definition for it or form is ["*"]
+ //we don't generate it.
+ var ignore = {};
+ transclude(scope, function(clone) {
+ clone.addClass('schema-form-ignore');
+ element.prepend(clone);
+
+ if (element[0].querySelectorAll) {
+ var models = element[0].querySelectorAll('[ng-model]');
+ if (models) {
+ for (var i = 0; i < models.length; i++) {
+ var key = models[i].getAttribute('ng-model');
+ //skip first part before .
+ ignore[key.substring(key.indexOf('.') + 1)] = true;
+ }
}
}
- }
- });
- //Since we are dependant on up to three
- //attributes we'll do a common watch
- var lastDigest = {};
-
- scope.$watch(function(){
+ });
+ //Since we are dependant on up to three
+ //attributes we'll do a common watch
+ var lastDigest = {};
- var schema = scope.schema;
- var form = scope.initialForm || ['*'];
+ scope.$watch(function() {
- //The check for schema.type is to ensure that schema is not {}
- if (form && schema && schema.type && (lastDigest.form !== form || lastDigest.schema !== schema) && Object.keys(schema.properties).length > 0) {
- lastDigest.schema = schema;
- lastDigest.form = form;
+ var schema = scope.schema;
+ var form = scope.initialForm || ['*'];
- // Check for options
- var options = scope.$eval(attrs.sfOptions);
+ //The check for schema.type is to ensure that schema is not {}
+ if (form && schema && schema.type &&
+ (lastDigest.form !== form || lastDigest.schema !== schema) &&
+ Object.keys(schema.properties).length > 0) {
+ lastDigest.schema = schema;
+ lastDigest.form = form;
- var merged = schemaForm.merge(schema,form,ignore,options);
- var frag = document.createDocumentFragment();
+ // Check for options
+ var options = scope.$eval(attrs.sfOptions);
- //make the form available to decorators
- scope.schemaForm = { form: merged, schema: schema };
+ var merged = schemaForm.merge(schema, form, ignore, options);
+ var frag = document.createDocumentFragment();
- //Create directives from the form definition
- angular.forEach(merged,function(obj,i){
- var n = document.createElement(attrs.sfDecoratorName || snake_case(schemaFormDecorators.defaultDecorator,'-'));
- n.setAttribute('form','schemaForm.form['+i+']');
- frag.appendChild(n);
- });
+ //make the form available to decorators
+ scope.schemaForm = {form: merged, schema: schema};
- //clean all but pre existing html.
- element.children(':not(.schema-form-ignore)').remove();
+ //Create directives from the form definition
+ angular.forEach(merged, function(obj, i) {
+ var n = document.createElement(attrs.sfDecoratorName ||
+ snakeCase(schemaFormDecorators.defaultDecorator, '-'));
+ n.setAttribute('form', 'schemaForm.form[' + i + ']');
+ frag.appendChild(n);
+ });
- element[0].appendChild(frag);
+ //clean all but pre existing html.
+ element.children(':not(.schema-form-ignore)').remove();
- //compile only children
- $compile(element.children())(scope);
+ element[0].appendChild(frag);
- //ok, now that that is done let's set any defaults
- schemaForm.traverseSchema(schema,function(prop,path){
+ //compile only children
+ $compile(element.children())(scope);
- if (angular.isDefined(prop['default'])) {
- var val = sfSelect(path, scope.model);
- if (angular.isUndefined(val)) {
- sfSelect(path, scope.model, prop['default']);
+ //ok, now that that is done let's set any defaults
+ schemaForm.traverseSchema(schema, function(prop, path) {
+ if (angular.isDefined(prop['default'])) {
+ var val = sfSelect(path, scope.model);
+ if (angular.isUndefined(val)) {
+ sfSelect(path, scope.model, prop['default']);
+ }
}
- }
- });
-
- }
- });
- }
- };
-}]);
+ });
+ }
+ });
+ }
+ };
+ }
+]);
-/* global tv4 */
-angular.module('schemaForm').directive('schemaValidate',['sfValidator',function(sfValidator){
+angular.module('schemaForm').directive('schemaValidate', ['sfValidator', function(sfValidator) {
return {
restrict: 'A',
scope: false,
require: 'ngModel',
- link: function(scope,element,attrs,ngModel) {
+ link: function(scope, element, attrs, ngModel) {
//Since we have scope false this is the same scope
//as the decorator
scope.ngModel = ngModel;
@@ -1291,7 +1303,7 @@ angular.module('schemaForm').directive('schemaValidate',['sfValidator',function(
// An empty field gives us the an empty string, which JSON schema
// happily accepts as a proper defined string, but an empty field
// for the user should trigger "required". So we set it to undefined.
- if (viewValue === "") {
+ if (viewValue === '') {
viewValue = undefined;
}
@@ -1313,7 +1325,7 @@ angular.module('schemaForm').directive('schemaValidate',['sfValidator',function(
ngModel.$parsers.unshift(validate);
// Listen to an event so we can validate the input on request
- scope.$on('schemaFormValidate',function() {
+ scope.$on('schemaFormValidate', function() {
if (ngModel.$commitViewValue) {
ngModel.$commitViewValue(true);
@@ -1324,11 +1336,11 @@ angular.module('schemaForm').directive('schemaValidate',['sfValidator',function(
//This works since we now we're inside a decorator and that this is the decorators scope.
//If $pristine and empty don't show success (even if it's valid)
- scope.hasSuccess = function(){
+ scope.hasSuccess = function() {
return ngModel.$valid && (!ngModel.$pristine || !ngModel.$isEmpty(ngModel.$modelValue));
};
- scope.hasError = function(){
+ scope.hasError = function() {
return ngModel.$invalid && !ngModel.$pristine;
};
diff --git a/dist/schema-form.min.js b/dist/schema-form.min.js
index 8b9b02ad1..eee16a634 100644
--- a/dist/schema-form.min.js
+++ b/dist/schema-form.min.js
@@ -1 +1 @@
-var deps=["ObjectPath"];try{angular.module("ngSanitize"),deps.push("ngSanitize")}catch(e){}try{angular.module("ui.sortable"),deps.push("ui.sortable")}catch(e){}angular.module("schemaForm",deps),angular.module("schemaForm").provider("sfPath",["ObjectPathProvider",function(e){var r={parse:e.parse};r.stringify=1===angular.version.major&&angular.version.minor<3?function(e){return Array.isArray(e)?e.join("."):e.toString()}:e.stringify,r.normalize=function(e,t){return r.stringify(Array.isArray(e)?e:r.parse(e),t)},this.parse=r.parse,this.stringify=r.stringify,this.normalize=r.normalize,this.$get=function(){return r}}]),angular.module("schemaForm").factory("sfSelect",["sfPath",function(e){var r=/^\d+$/;return function(t,a,n){a||(a=this);var i="string"==typeof t?e.parse(t):t;if("undefined"!=typeof n&&1===i.length)return a[i[0]]=n,a;"undefined"!=typeof n&&"undefined"==typeof a[i[0]]&&(a[i[0]]=i.length>2&&r.test(i[1])?[]:{});for(var o=a[i[0]],u=1;u',link:function(e,t,a){var n={items:"c",titleMap:"c",schema:"c"},i={type:r},o=!0;angular.forEach(a,function(r,t){if("$"!==t[0]&&0!==t.indexOf("ng")&&"sfField"!==t){var u=function(r){angular.isDefined(r)&&r!==i[t]&&(i[t]=r,o&&i.type&&(i.key||angular.isUndefined(a.key))&&(e.form=i,o=!1))};"model"===t?e.$watch(r,function(r){r&&e.model!==r&&(e.model=r)}):"c"===n[t]?e.$watchCollection(r,u):a.$observe(t,u)}})}}})};this.createDecorator=function(e,r,n){a[e]={mappings:r||{},rules:n||[]},a[t]||(t=e),i(e)},this.createDirective=o,this.createDirectives=function(e){angular.forEach(e,function(e,r){o(r,e)})},this.directive=function(e){return e=e||t,a[e]},this.addMapping=function(e,r,t){a[e]&&(a[e].mappings[r]=t)},this.$get=function(){return{directive:function(e){return a[e]},defaultDecorator:t}},i("sfDecorator")}]),angular.module("schemaForm").provider("schemaForm",["sfPathProvider",function(e){var r=function(e){var r=[];return e.forEach(function(e){r.push({name:e,value:e})}),r},t=function(e){if(!angular.isArray(e)){var r=[];return angular.forEach(e,function(e,t){r.push({name:e,value:t})}),r}return e},a=function(e,r,t){var a=d[r.type];if(a)for(var n,i=0;i1&&(c={type:"section",items:i.items})}if(n.copyWithIndex=function(e){if(!l[e]&&c){var t=angular.copy(c);t.arrayIndex=e,r.traverseForm(t,a(e)),l[e]=t}return l[e]},n.appendToArray=function(){var t=o.length,a=n.copyWithIndex(t);if(r.traverseForm(a,function(r){r.key&&angular.isDefined(r.default)&&e(r.key,n.model,r.default)}),t===o.length){var u,l=e("schema.items.type",i);"object"===l?u={}:"array"===l&&(u=[]),o.push(u)}n.validateArray&&n.validateArray()},n.deleteFromArray=function(e){o.splice(e,1),n.validateArray&&n.validateArray()},i.titleMap||i.startEmpty===!0||0!==o.length||n.appendToArray(),i.titleMap&&i.titleMap.length>0){n.titleMapValues=[];var f=function(e){n.titleMapValues=[],e=e||[],i.titleMap.forEach(function(r){n.titleMapValues.push(-1!==e.indexOf(r.value))})};f(n.modelArray),n.$watchCollection("modelArray",f),n.$watchCollection("titleMapValues",function(e){if(e){for(var r=n.modelArray;r.length>0;)r.shift();i.titleMap.forEach(function(t,a){e[a]&&r.push(t.value)})}})}if(u){var m;n.validateArray=function(){var e=t.validate(i,n.modelArray.length>0?n.modelArray:void 0);e.valid!==!1||!e.error||""!==e.error.dataPath&&e.error.dataPath!=="/"+i.key[i.key.length-1]?u.$setValidity("schema",!0):(u.$setViewValue(n.modelArray),m=e.error,u.$setValidity("schema",!1))},n.$on("schemaFormValidate",n.validateArray),n.hasSuccess=function(){return u.$valid&&!u.$pristine},n.hasError=function(){return u.$invalid},n.schemaError=function(){return m}}s()})}}}]),angular.module("schemaForm").directive("sfChanged",function(){return{require:"ngModel",restrict:"AC",scope:!1,link:function(e,r,t,a){var n=e.$eval(t.sfChanged);n&&n.onChange&&a.$viewChangeListeners.push(function(){angular.isFunction(n.onChange)?n.onChange(a.$modelValue,n):e.evalExpr(n.onChange,{modelValue:a.$modelValue,form:n})})}}}),angular.module("schemaForm").directive("sfSchema",["$compile","schemaForm","schemaFormDecorators","sfSelect",function(e,r,t,a){function n(e,r){return r=r||"_",e.replace(i,function(e,t){return(t?r:"")+e.toLowerCase()})}var i=/[A-Z]/g;return{scope:{schema:"=sfSchema",initialForm:"=sfForm",model:"=sfModel"},controller:["$scope",function(e){this.evalInParentScope=function(r,t){return e.$parent.$eval(r,t)}}],replace:!1,restrict:"A",transclude:!0,require:"?form",link:function(i,o,u,l,s){i.formCtrl=l;var c={};s(i,function(e){if(e.addClass("schema-form-ignore"),o.prepend(e),o[0].querySelectorAll){var r=o[0].querySelectorAll("[ng-model]");if(r)for(var t=0;t0){f.schema=l,f.form=s;var m=i.$eval(u.sfOptions),d=r.merge(l,s,c,m),p=document.createDocumentFragment();i.schemaForm={form:d,schema:l},angular.forEach(d,function(e,r){var a=document.createElement(u.sfDecoratorName||n(t.defaultDecorator,"-"));a.setAttribute("form","schemaForm.form["+r+"]"),p.appendChild(a)}),o.children(":not(.schema-form-ignore)").remove(),o[0].appendChild(p),e(o.children())(i),r.traverseSchema(l,function(e,r){if(angular.isDefined(e["default"])){var t=a(r,i.model);angular.isUndefined(t)&&a(r,i.model,e["default"])}})}})}}}]),angular.module("schemaForm").directive("schemaValidate",["sfValidator",function(e){return{restrict:"A",scope:!1,require:"ngModel",link:function(r,t,a,n){r.ngModel=n;var i=null,o=r.$eval(a.schemaValidate),u=function(t){if(o||(o=r.$eval(a.schemaValidate)),!o)return t;if(angular.isDefined(a.ngRequired)&&angular.isUndefined(t))return void 0;""===t&&(t=void 0);var u=e.validate(o,t);return u.valid?(n.$setValidity("schema",!0),t):(n.$setValidity("schema",!1),void(i=u.error))};n.$parsers.unshift(u),r.$on("schemaFormValidate",function(){n.$commitViewValue?n.$commitViewValue(!0):n.$setViewValue(n.$viewValue)}),r.hasSuccess=function(){return n.$valid&&(!n.$pristine||!n.$isEmpty(n.$modelValue))},r.hasError=function(){return n.$invalid&&!n.$pristine},r.schemaError=function(){return i}}}}]);
\ No newline at end of file
+var deps=["ObjectPath"];try{angular.module("ngSanitize"),deps.push("ngSanitize")}catch(e){}try{angular.module("ui.sortable"),deps.push("ui.sortable")}catch(e){}try{angular.module("angularSpectrumColorpicker"),deps.push("angularSpectrumColorpicker")}catch(e){}angular.module("schemaForm",deps),angular.module("schemaForm").provider("sfPath",["ObjectPathProvider",function(e){var r={parse:e.parse};r.stringify=1===angular.version.major&&angular.version.minor<3?function(e){return Array.isArray(e)?e.join("."):e.toString()}:e.stringify,r.normalize=function(e,t){return r.stringify(Array.isArray(e)?e:r.parse(e),t)},this.parse=r.parse,this.stringify=r.stringify,this.normalize=r.normalize,this.$get=function(){return r}}]),angular.module("schemaForm").factory("sfSelect",["sfPath",function(e){var r=/^\d+$/;return function(t,a,n){a||(a=this);var i="string"==typeof t?e.parse(t):t;if("undefined"!=typeof n&&1===i.length)return a[i[0]]=n,a;"undefined"!=typeof n&&"undefined"==typeof a[i[0]]&&(a[i[0]]=i.length>2&&r.test(i[1])?[]:{});for(var o=a[i[0]],u=1;u',link:function(e,t,a){var n={items:"c",titleMap:"c",schema:"c"},i={type:r},o=!0;angular.forEach(a,function(r,t){if("$"!==t[0]&&0!==t.indexOf("ng")&&"sfField"!==t){var u=function(r){angular.isDefined(r)&&r!==i[t]&&(i[t]=r,o&&i.type&&(i.key||angular.isUndefined(a.key))&&(e.form=i,o=!1))};"model"===t?e.$watch(r,function(r){r&&e.model!==r&&(e.model=r)}):"c"===n[t]?e.$watchCollection(r,u):a.$observe(t,u)}})}}})};this.createDecorator=function(e,r,n){a[e]={mappings:r||{},rules:n||[]},a[t]||(t=e),i(e)},this.createDirective=o,this.createDirectives=function(e){angular.forEach(e,function(e,r){o(r,e)})},this.directive=function(e){return e=e||t,a[e]},this.addMapping=function(e,r,t){a[e]&&(a[e].mappings[r]=t)},this.$get=function(){return{directive:function(e){return a[e]},defaultDecorator:t}},i("sfDecorator")}]),angular.module("schemaForm").provider("schemaForm",["sfPathProvider",function(e){var r=function(e){var r=[];return e.forEach(function(e){r.push({name:e,value:e})}),r},t=function(e){if(!angular.isArray(e)){var r=[];return angular.forEach(e,function(e,t){r.push({name:e,value:t})}),r}return e},a=function(e,r,t){var a=p[r.type];if(a)for(var n,i=0;i1&&(c={type:"section",items:i.items})}if(n.copyWithIndex=function(e){if(!l[e]&&c){var t=angular.copy(c);t.arrayIndex=e,r.traverseForm(t,a(e)),l[e]=t}return l[e]},n.appendToArray=function(){var t=o.length,a=n.copyWithIndex(t);if(r.traverseForm(a,function(r){r.key&&angular.isDefined(r.default)&&e(r.key,n.model,r.default)}),t===o.length){var u,l=e("schema.items.type",i);"object"===l?u={}:"array"===l&&(u=[]),o.push(u)}return n.validateArray&&n.validateArray(),o},n.deleteFromArray=function(e){o.splice(e,1),n.validateArray&&n.validateArray()},i.titleMap||i.startEmpty===!0||0!==o.length||n.appendToArray(),i.titleMap&&i.titleMap.length>0){n.titleMapValues=[];var f=function(e){n.titleMapValues=[],e=e||[],i.titleMap.forEach(function(r){n.titleMapValues.push(-1!==e.indexOf(r.value))})};f(n.modelArray),n.$watchCollection("modelArray",f),n.$watchCollection("titleMapValues",function(e){if(e){for(var r=n.modelArray;r.length>0;)r.shift();i.titleMap.forEach(function(t,a){e[a]&&r.push(t.value)})}})}if(u){var m;n.validateArray=function(){var e=t.validate(i,n.modelArray.length>0?n.modelArray:void 0);e.valid!==!1||!e.error||""!==e.error.dataPath&&e.error.dataPath!=="/"+i.key[i.key.length-1]?u.$setValidity("schema",!0):(u.$setViewValue(n.modelArray),m=e.error,u.$setValidity("schema",!1))},n.$on("schemaFormValidate",n.validateArray),n.hasSuccess=function(){return u.$valid&&!u.$pristine},n.hasError=function(){return u.$invalid},n.schemaError=function(){return m}}s()})}}}]),angular.module("schemaForm").directive("sfChanged",function(){return{require:"ngModel",restrict:"AC",scope:!1,link:function(e,r,t,a){var n=e.$eval(t.sfChanged);n&&n.onChange&&a.$viewChangeListeners.push(function(){angular.isFunction(n.onChange)?n.onChange(a.$modelValue,n):e.evalExpr(n.onChange,{modelValue:a.$modelValue,form:n})})}}}),angular.module("schemaForm").directive("sfSchema",["$compile","schemaForm","schemaFormDecorators","sfSelect",function(e,r,t,a){var n=/[A-Z]/g,i=function(e,r){return r=r||"_",e.replace(n,function(e,t){return(t?r:"")+e.toLowerCase()})};return{scope:{schema:"=sfSchema",initialForm:"=sfForm",model:"=sfModel"},controller:["$scope",function(e){this.evalInParentScope=function(r,t){return e.$parent.$eval(r,t)}}],replace:!1,restrict:"A",transclude:!0,require:"?form",link:function(n,o,u,l,s){n.formCtrl=l;var c={};s(n,function(e){if(e.addClass("schema-form-ignore"),o.prepend(e),o[0].querySelectorAll){var r=o[0].querySelectorAll("[ng-model]");if(r)for(var t=0;t0){f.schema=l,f.form=s;var m=n.$eval(u.sfOptions),p=r.merge(l,s,c,m),d=document.createDocumentFragment();n.schemaForm={form:p,schema:l},angular.forEach(p,function(e,r){var a=document.createElement(u.sfDecoratorName||i(t.defaultDecorator,"-"));a.setAttribute("form","schemaForm.form["+r+"]"),d.appendChild(a)}),o.children(":not(.schema-form-ignore)").remove(),o[0].appendChild(d),e(o.children())(n),r.traverseSchema(l,function(e,r){if(angular.isDefined(e["default"])){var t=a(r,n.model);angular.isUndefined(t)&&a(r,n.model,e["default"])}})}})}}}]),angular.module("schemaForm").directive("schemaValidate",["sfValidator",function(e){return{restrict:"A",scope:!1,require:"ngModel",link:function(r,t,a,n){r.ngModel=n;var i=null,o=r.$eval(a.schemaValidate),u=function(t){if(o||(o=r.$eval(a.schemaValidate)),!o)return t;if(angular.isDefined(a.ngRequired)&&angular.isUndefined(t))return void 0;""===t&&(t=void 0);var u=e.validate(o,t);return u.valid?(n.$setValidity("schema",!0),t):(n.$setValidity("schema",!1),void(i=u.error))};n.$parsers.unshift(u),r.$on("schemaFormValidate",function(){n.$commitViewValue?n.$commitViewValue(!0):n.$setViewValue(n.$viewValue)}),r.hasSuccess=function(){return n.$valid&&(!n.$pristine||!n.$isEmpty(n.$modelValue))},r.hasError=function(){return n.$invalid&&!n.$pristine},r.schemaError=function(){return i}}}}]);
\ No newline at end of file
diff --git a/docs/datepicker.md b/docs/datepicker.md
deleted file mode 100644
index 5f7a66c90..000000000
--- a/docs/datepicker.md
+++ /dev/null
@@ -1,54 +0,0 @@
-Date Picker Addon
-=================
-
-Everyone loves a nice date picker - now you can have your very own date picker in Schema Form! The date picker add-on uses the excellent jQuery-based date picker, [pickadate.js](http://amsul.ca/pickadate.js/).
-
-Dates in JSON Schema are of type *"string"* and follow the *RFC 3339* date fomat, which, in turn, follows *ISO 8601*. What does that mean for you? Basically, just stick with the format `yyyy-mm-dd` and you'll be fine.
-
-Within Schema Form, pickadate only supports dates - not times.
-
-Installation
-------------
-The date picker is an add-on to the Bootstrap decorator. To use it, just include `dist/bootstrap-datepicker.min.js` *after* `dist/bootstrap-decorator.min.js`.
-
-You'll need to load a few additional files to use pickadate:
-
-1. jQuery (pickadate depends on it)
-2. The pickadate source files (see the pickadate.js [GitHub page](https://github.com/amsul/pickadate.js) for documentation)
-3. The pickadate CSS
-4. Translation files for whatever language you want to use
-
-Usage
------
-The datepicker add-on adds a new form type, `datepicker`, and a new default
-mapping.
-
-| Form Type | Becomes |
-|:---------------|:------------:|
-| datepicker | a pickadate widget |
-
-
-| Schema | Default Form type |
-|:-------------------|:------------:|
-| "type": "string" and "format": "date" | datepicker |
-
-
-Form Type Options
--------
-The `datepicker` form type takes two options: `minDate` and `maxDate`. `minDate` and `maxDate` both accept one of the following as values:
-
-1. A string in the format `yyyy-mm-dd`,
-2. A unix timestamp (as a Number), or
-3. An instance of `Date`
-
-Here's an example:
-
-```javascript
-{
- key: "birthDate",
- minDate: "1900-01-01",
- maxDate: new Date()
-}
-```
-
-
diff --git a/docs/index.md b/docs/index.md
index 6cbd8b47b..b1f5c4417 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -427,12 +427,12 @@ They do need a list of ```items``` to have as children.
### conditional
-A *conditional* is exactly the same as a *section*, i.e. a ```
``` with other form elements in
-it, hence they need an ```items``` property. They also need a ```condition``` which is
+A *conditional* is exactly the same as a *section*, i.e. a `
` with other form elements in
+it, hence they need an `items` property. They also need a `condition` which is
a string with an angular expression. If that expression evaluates as thruthy the *conditional*
will be rendered into the DOM otherwise not. The expression is evaluated in the parent scope of
-the ```sf-schema``` directive (the same as onClick on buttons) but with access to the current model
-under the name ```model```. This is useful for hiding/showing
+the `sf-schema` directive (the same as onClick on buttons) but with access to the current model
+and current array index under the name `model` and `arrayIndex`. This is useful for hiding/showing
parts of a form depending on another form control.
ex. A checkbox that shows an input field for a code when checked
@@ -476,6 +476,58 @@ Note that angulars two-way binding automatically will update the conditional blo
event handlers and such. The condition need not reference a model value it could be anything in
scope.
+The same example, but inside an array:
+
+```javascript
+function FormCtrl($scope) {
+ $scope.persons = []
+
+ $scope.schema = {
+ "type": "object",
+ "properties": {
+ "persons": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "eligible": {
+ "type": "boolean",
+ "title": "Eligible for awesome things"
+ },
+ "code": {
+ "type":"string"
+ "title": "The Code"
+ }
+ }
+ }
+ }
+ }
+ }
+
+ $scope.form = [
+ {
+ "key": "persons",
+ "items": [
+ "persons[].name",
+ "persons[].eligible",
+ {
+ type: "conditional",
+ condition: "persons[arrayIndex].eligible", //or "model.eligable"
+ items: [
+ "persons[].code"
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+Note that arrays inside arrays won't work with conditional.
### select and checkboxes
diff --git a/examples/bootstrap-example.html b/examples/bootstrap-example.html
index 7bdcc451c..612402d9f 100644
--- a/examples/bootstrap-example.html
+++ b/examples/bootstrap-example.html
@@ -8,6 +8,8 @@
+
+