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",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
')}]),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",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
')}]),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 diff --git a/dist/bootstrap-decorator.min.js b/dist/bootstrap-decorator.min.js index de2943e6d..7704b0baa 100644 --- a/dist/bootstrap-decorator.min.js +++ b/dist/bootstrap-decorator.min.js @@ -1 +1 @@ -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",'

{{ form.title }}

'),e.put("directives/decorators/bootstrap/checkbox.html",'
'),e.put("directives/decorators/bootstrap/checkboxes.html",'
'),e.put("directives/decorators/bootstrap/default.html",'
'),e.put("directives/decorators/bootstrap/fieldset-trcl.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/fieldset.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/help.html",'
'),e.put("directives/decorators/bootstrap/radio-buttons.html",'
'),e.put("directives/decorators/bootstrap/radios-inline.html",'
'),e.put("directives/decorators/bootstrap/radios.html",'
'),e.put("directives/decorators/bootstrap/readonly.html",'
'),e.put("directives/decorators/bootstrap/section.html",'
'),e.put("directives/decorators/bootstrap/select.html",'
'),e.put("directives/decorators/bootstrap/submit.html",'
'),e.put("directives/decorators/bootstrap/tabarray.html",'
'),e.put("directives/decorators/bootstrap/tabs.html",'
'),e.put("directives/decorators/bootstrap/textarea.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
')}]),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",'

{{ form.title }}

'),e.put("directives/decorators/bootstrap/checkbox.html",'
'),e.put("directives/decorators/bootstrap/checkboxes.html",'
'),e.put("directives/decorators/bootstrap/default.html",'
'),e.put("directives/decorators/bootstrap/fieldset-trcl.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/fieldset.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/help.html",'
'),e.put("directives/decorators/bootstrap/radio-buttons.html",'
'),e.put("directives/decorators/bootstrap/radios-inline.html",'
'),e.put("directives/decorators/bootstrap/radios.html",'
'),e.put("directives/decorators/bootstrap/readonly.html",'
'),e.put("directives/decorators/bootstrap/section.html",'
'),e.put("directives/decorators/bootstrap/select.html",'
'),e.put("directives/decorators/bootstrap/submit.html",'
'),e.put("directives/decorators/bootstrap/tabarray.html",'
'),e.put("directives/decorators/bootstrap/tabs.html",'
'),e.put("directives/decorators/bootstrap/textarea.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
')}]),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 @@ + +