diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 8bed0ce6b..000000000 --- a/.coveralls.yml +++ /dev/null @@ -1,2 +0,0 @@ -service_name: travis-ci - diff --git a/.gitignore b/.gitignore index 26eca7a67..a51be4445 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -node_modules -coverage +.bower.json bower_components +coverage +node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 70e0bf43c..000000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js -node_js: - - 0.10 - -before_script: - - npm install -g bower - - bower install diff --git a/CHANGELOG b/CHANGELOG index 71ffe35c1..66bceb2da 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,38 @@ +v0.8.12 +------- + * Bugfix for `condition` builder. It had a typo that broke it. + * Bugfix for `transclusion` builder. + + +v0.8.11 +------- + * Bugfix for checkboxes validation in the new builder. + +v0.8.10 +------- + * Bugfix for checkboxes when model array is undefined. + +v0.8.9 +------ + * Bugfix for radios and radios-inline. Validation should now work. + * Bugfix so model changes from outside the form trigger validation. + * New global option `pristine`. Sets if errors and success states + should be visible when form field are `$pristine`. Default is `{errors: true, success: true}` + +v0.8.8 +------ + * Don't rely on documentFragment.children @davidlgj + * Restored "template" type support with the builder. @davidlgj + * Fixed defaults in array items. @davidlgj + +v0.8.7 +------ + * Moved common builder functions from angular-schema-form-bootstrap decorator. @davidlgj + * Bugfx for the new builder. @davidlgj + v0.8.6 - * Removed left over console.timeEnd +------ + * Removed left over console.timeEnd @davidlgj v0.8.5 ------ diff --git a/README.md b/README.md index fa38ffb09..1e17018fa 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ Angular Schema Form [![npm version](https://img.shields.io/npm/v/angular-schema-form.svg?style=flat-square)](https://www.npmjs.org/package/angular-schema-form) [![npm downloads](https://img.shields.io/npm/dm/angular-schema-form.svg?style=flat-square)](http://npm-stat.com/charts.html?package=angular-schema-form&from=2015-01-01) [![Gitter](https://img.shields.io/badge/GITTER-JOIN%20CHAT%20%E2%86%92-ff69b4.svg?style=flat-square)](https://gitter.im/Textalk/angular-schema-form?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://img.shields.io/travis/Textalk/angular-schema-form.svg?style=flat-square)](https://travis-ci.org/Textalk/angular-schema-form) [![Build Status](https://img.shields.io/coveralls/jekyll/jekyll.svg?style=flat-square)](https://coveralls.io/r/Textalk/angular-schema-form?branch=development) diff --git a/dist/bootstrap-decorator.js b/dist/bootstrap-decorator.js index cfd86cb0f..fa1f8b47a 100644 --- a/dist/bootstrap-decorator.js +++ b/dist/bootstrap-decorator.js @@ -9,22 +9,22 @@ }(this, function(schemaForm) { angular.module("schemaForm").run(["$templateCache", function($templateCache) {$templateCache.put("directives/decorators/bootstrap/actions-trcl.html","
"); $templateCache.put("directives/decorators/bootstrap/actions.html","
"); -$templateCache.put("directives/decorators/bootstrap/array.html","
"); -$templateCache.put("directives/decorators/bootstrap/checkbox.html","
"); -$templateCache.put("directives/decorators/bootstrap/checkboxes.html","
"); -$templateCache.put("directives/decorators/bootstrap/default.html","
{{ hasSuccess() ? \'(success)\' : \'(error)\' }}
"); +$templateCache.put("directives/decorators/bootstrap/array.html","
"); +$templateCache.put("directives/decorators/bootstrap/checkbox.html","
"); +$templateCache.put("directives/decorators/bootstrap/checkboxes.html","
"); +$templateCache.put("directives/decorators/bootstrap/default.html","
{{ hasSuccess() ? \'(success)\' : \'(error)\' }}
"); $templateCache.put("directives/decorators/bootstrap/fieldset-trcl.html","
{{ form.title }}
"); $templateCache.put("directives/decorators/bootstrap/fieldset.html","
{{ form.title }}
"); $templateCache.put("directives/decorators/bootstrap/help.html","
"); $templateCache.put("directives/decorators/bootstrap/radio-buttons.html","
"); -$templateCache.put("directives/decorators/bootstrap/radios-inline.html","
"); -$templateCache.put("directives/decorators/bootstrap/radios.html","
"); +$templateCache.put("directives/decorators/bootstrap/radios-inline.html","
"); +$templateCache.put("directives/decorators/bootstrap/radios.html","
"); $templateCache.put("directives/decorators/bootstrap/section.html","
"); -$templateCache.put("directives/decorators/bootstrap/select.html","
"); +$templateCache.put("directives/decorators/bootstrap/select.html","
"); $templateCache.put("directives/decorators/bootstrap/submit.html","
"); -$templateCache.put("directives/decorators/bootstrap/tabarray.html","
"); -$templateCache.put("directives/decorators/bootstrap/tabs.html","
"); -$templateCache.put("directives/decorators/bootstrap/textarea.html","
");}]); +$templateCache.put("directives/decorators/bootstrap/tabarray.html","
"); +$templateCache.put("directives/decorators/bootstrap/tabs.html","
"); +$templateCache.put("directives/decorators/bootstrap/textarea.html","
");}]); angular.module('schemaForm').config(['schemaFormDecoratorsProvider', function(decoratorsProvider) { var base = 'directives/decorators/bootstrap/'; diff --git a/dist/bootstrap-decorator.min.js b/dist/bootstrap-decorator.min.js index 476ba5ae3..7a62fb2c7 100644 --- a/dist/bootstrap-decorator.min.js +++ b/dist/bootstrap-decorator.min.js @@ -1 +1 @@ -!function(e,t){"function"==typeof define&&define.amd?define(["schemaForm"],t):"object"==typeof exports?module.exports=t(require("schemaForm")):e.bootstrapDecorator=t(e.schemaForm)}(this,function(e){return 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",'
'),e.put("directives/decorators/bootstrap/checkbox.html",'
'),e.put("directives/decorators/bootstrap/checkboxes.html",'
'),e.put("directives/decorators/bootstrap/default.html",'
{{ hasSuccess() ? \'(success)\' : \'(error)\' }}
'),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/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",'
')}]),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){var t="directives/decorators/bootstrap/";e.defineDecorator("bootstrapDecorator",{textarea:{template:t+"textarea.html",replace:!1},fieldset:{template:t+"fieldset.html",replace:!1},array:{template:t+"array.html",replace:!1},tabarray:{template:t+"tabarray.html",replace:!1},tabs:{template:t+"tabs.html",replace:!1},section:{template:t+"section.html",replace:!1},conditional:{template:t+"section.html",replace:!1},actions:{template:t+"actions.html",replace:!1},select:{template:t+"select.html",replace:!1},checkbox:{template:t+"checkbox.html",replace:!1},checkboxes:{template:t+"checkboxes.html",replace:!1},number:{template:t+"default.html",replace:!1},password:{template:t+"default.html",replace:!1},submit:{template:t+"submit.html",replace:!1},button:{template:t+"submit.html",replace:!1},radios:{template:t+"radios.html",replace:!1},"radios-inline":{template:t+"radios-inline.html",replace:!1},radiobuttons:{template:t+"radio-buttons.html",replace:!1},help:{template:t+"help.html",replace:!1},"default":{template:t+"default.html",replace:!1}},[]),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,s){e.title=e.$eval(s.title)}}}),e}); \ No newline at end of file +!function(e,t){"function"==typeof define&&define.amd?define(["schemaForm"],t):"object"==typeof exports?module.exports=t(require("schemaForm")):e.bootstrapDecorator=t(e.schemaForm)}(this,function(e){return 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",'
'),e.put("directives/decorators/bootstrap/checkbox.html",'
'),e.put("directives/decorators/bootstrap/checkboxes.html",'
'),e.put("directives/decorators/bootstrap/default.html",'
{{ hasSuccess() ? \'(success)\' : \'(error)\' }}
'),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/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",'
')}]),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){var t="directives/decorators/bootstrap/";e.defineDecorator("bootstrapDecorator",{textarea:{template:t+"textarea.html",replace:!1},fieldset:{template:t+"fieldset.html",replace:!1},array:{template:t+"array.html",replace:!1},tabarray:{template:t+"tabarray.html",replace:!1},tabs:{template:t+"tabs.html",replace:!1},section:{template:t+"section.html",replace:!1},conditional:{template:t+"section.html",replace:!1},actions:{template:t+"actions.html",replace:!1},select:{template:t+"select.html",replace:!1},checkbox:{template:t+"checkbox.html",replace:!1},checkboxes:{template:t+"checkboxes.html",replace:!1},number:{template:t+"default.html",replace:!1},password:{template:t+"default.html",replace:!1},submit:{template:t+"submit.html",replace:!1},button:{template:t+"submit.html",replace:!1},radios:{template:t+"radios.html",replace:!1},"radios-inline":{template:t+"radios-inline.html",replace:!1},radiobuttons:{template:t+"radio-buttons.html",replace:!1},help:{template:t+"help.html",replace:!1},"default":{template:t+"default.html",replace:!1}},[]),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,a){e.title=e.$eval(a.title)}}}),e}); \ No newline at end of file diff --git a/dist/schema-form.js b/dist/schema-form.js index 3e4387908..a64dc2441 100644 --- a/dist/schema-form.js +++ b/dist/schema-form.js @@ -232,12 +232,14 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s for (var i = 0; i < transclusions.length; i++) { var n = transclusions[i]; - // The sf-transclude attribute is not a directive, but has the name of what we're supposed to - // traverse. - var sub = args.form[n.getAttribute('sf-field-transclude')]; - if (sub) { - sub = Array.isArray(sub) ? sub : [sub]; - var childFrag = args.build(sub, args.path + '.' + sub, args.state); + // The sf-transclude attribute is not a directive, + // but has the name of what we're supposed to + // traverse. Default to `items` + var sub = n.getAttribute('sf-field-transclude') || 'items'; + var items = args.form[sub]; + + if (items) { + var childFrag = args.build(items, args.path + '.' + sub, args.state); n.appendChild(childFrag); } } @@ -248,14 +250,14 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s // but be nice to existing ng-if. if (args.form.condition) { var evalExpr = 'evalExpr(' + args.path + - '.contidion, { model: model, "arrayIndex": $index})'; + '.condition, { model: model, "arrayIndex": $index})'; if (args.form.key) { var strKey = sfPathProvider.stringify(args.form.key); evalExpr = 'evalExpr(' + args.path + '.condition,{ model: model, "arrayIndex": $index, ' + '"modelValue": model' + (strKey[0] === '[' ? '' : '.') + strKey + '})'; } - var children = args.fieldFrag.children; + var children = args.fieldFrag.children || args.fieldFrag.childNodes; for (var i = 0; i < children.length; i++) { var child = children[i]; var ngIf = child.getAttribute('ng-if'); @@ -299,6 +301,13 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s } }; this.builders = builders; + var stdBuilders = [ + builders.sfField, + builders.ngModel, + builders.ngModelOptions, + builders.condition + ]; + this.stdBuilders = stdBuilders; this.$get = ['$templateCache', 'schemaFormDecorators', 'sfPath', function($templateCache, schemaFormDecorators, sfPath) { @@ -326,7 +335,7 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s // Sanity check. if (!f.type) { - return; + return frag; } var field = decorator[f.type] || decorator['default']; @@ -351,7 +360,7 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s // measure optmization. A good start is probably a cache of DOM nodes for a particular // template that can be cloned instead of using innerHTML var div = document.createElement('div'); - var template = templateFn(field.template) || templateFn([decorator['default'].template]); + var template = templateFn(f, field) || templateFn(f, decorator['default']); div.innerHTML = template; // Move node to a document fragment, we don't want the div. @@ -375,11 +384,14 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s }; + // Let the form definiton override builders if it wants to. + var builderFn = f.builder || field.builder; + // Builders are either a function or a list of functions. - if (typeof field.builder === 'function') { - field.builder(args); + if (typeof builderFn === 'function') { + builderFn(args); } else { - field.builder.forEach(function(fn) { fn(args); }); + builderFn.forEach(function(fn) { fn(args); }); } // Append @@ -396,12 +408,16 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s * Builds a form from a canonical form definition */ build: function(form, decorator, slots, lookup) { - return build(form, decorator, function(url) { - return $templateCache.get(url); + return build(form, decorator, function(form, field) { + if (form.type === 'template') { + return form.template; + } + return $templateCache.get(field.template); }, slots, undefined, undefined, lookup); }, builder: builders, + stdBuilders: stdBuilders, internalBuild: build }; }]; @@ -429,8 +445,16 @@ angular.module('schemaForm').provider('schemaFormDecorators', return decorator['default'].template; }; - var createDirective = function(name) { - $compileProvider.directive(name, + /************************************************** + * DEPRECATED * + * The new builder and sf-field is preferred, but * + * we keep this in during a transitional period * + * so that add-ons that don't use the new builder * + * works. * + **************************************************/ + //TODO: Move to a compatability extra script. + var createDirective = function(name) { + $compileProvider.directive(name, ['$parse', '$compile', '$http', '$templateCache', '$interpolate', '$q', 'sfErrorMessage', 'sfPath','sfSelect', function($parse, $compile, $http, $templateCache, $interpolate, $q, sfErrorMessage, @@ -798,17 +822,22 @@ angular.module('schemaForm').provider('schemaFormDecorators', /** - * Create a decorator directive and its sibling "manual" use decorators. - * The directive can be used to create form fields or other form entities. - * It can be used in conjunction with directive in which case the decorator is - * given it's configuration via a the "form" attribute. + * Define a decorator. A decorator is a set of form types with templates and builder functions + * that help set up the form. * - * ex. Basic usage - * - ** * @param {string} name directive name (CamelCased) * @param {Object} fields, an object that maps "type" => `{ template, builder, replace}`. attributes `builder` and `replace` are optional, and replace defaults to true. + + `template` should be the key of the template to load and it should be pre-loaded + in `$templateCache`. + + `builder` can be a function or an array of functions. They will be called in + the order they are supplied. + + `replace` (DEPRECATED) is for backwards compatability. If false the builder + will use the "old" way of building that form field using a + directive. */ this.defineDecorator = function(name, fields) { decorators[name] = {'__name': name}; // TODO: this feels like a hack, come up with a better way. @@ -826,6 +855,7 @@ angular.module('schemaForm').provider('schemaFormDecorators', }; /** + * DEPRECATED * Creates a directive of a decorator * Usable when you want to use the decorators without using directive. * Specifically when you need to reuse styling. @@ -840,6 +870,7 @@ angular.module('schemaForm').provider('schemaFormDecorators', this.createDirective = createManualDirective; /** + * DEPRECATED * Same as createDirective, but takes an object where key is 'type' and value is 'templateUrl' * Useful for batching. * @param {Object} templates @@ -862,6 +893,7 @@ angular.module('schemaForm').provider('schemaFormDecorators', /** + * DEPRECATED use defineAddOn() instead. * Adds a mapping to an existing decorator. * @param {String} name Decorator name * @param {String} type Form type for the mapping @@ -879,6 +911,25 @@ angular.module('schemaForm').provider('schemaFormDecorators', } }; + /** + * Adds an add-on to an existing decorator. + * @param {String} name Decorator name + * @param {String} type Form type for the mapping + * @param {String} url The template url + * @param {Function|Array} builder (optional) builder function(s), + */ + this.defineAddOn = function(name, type, url, builder) { + if (decorators[name]) { + decorators[name][type] = { + template: url, + builder: builder, + replace: true + }; + } + }; + + + //Service is just a getter for directive templates and rules this.$get = function() { return { @@ -1324,7 +1375,7 @@ angular.module('schemaForm').provider('schemaForm', var service = {}; - service.merge = function(schema, form, ignore, options, readonly) { + service.merge = function(schema, form, ignore, options, readonly, asyncTemplates) { form = form || ['*']; options = options || {}; @@ -1395,13 +1446,13 @@ angular.module('schemaForm').provider('schemaForm', //if it's a type with items, merge 'em! if (obj.items) { - obj.items = service.merge(schema, obj.items, ignore, options, obj.readonly); + obj.items = service.merge(schema, obj.items, ignore, options, obj.readonly, asyncTemplates); } //if its has tabs, merge them also! if (obj.tabs) { angular.forEach(obj.tabs, function(tab) { - tab.items = service.merge(schema, tab.items, ignore, options, obj.readonly); + tab.items = service.merge(schema, tab.items, ignore, options, obj.readonly, asyncTemplates); }); } @@ -1410,6 +1461,13 @@ angular.module('schemaForm').provider('schemaForm', if (obj.type === 'checkbox' && angular.isUndefined(obj.schema['default'])) { obj.schema['default'] = false; } + + // Special case: template type with tempplateUrl that's needs to be loaded before rendering + // TODO: this is not a clean solution. Maybe something cleaner can be made when $ref support + // is introduced since we need to go async then anyway + if (asyncTemplates && obj.type === 'template' && !obj.template && obj.templateUrl) { + asyncTemplates.push(obj); + } return obj; })); @@ -1558,8 +1616,8 @@ angular.module('schemaForm').factory('sfValidator', [function() { * Directive that handles the model arrays * DEPRECATED with the new builder use the sfNewArray instead. */ -angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sfValidator', 'sfPath', - function(sfSelect, schemaForm, sfValidator, sfPath) { +angular.module('schemaForm').directive('sfArray', ['$timeout', 'sfSelect', 'schemaForm', 'sfValidator', 'sfPath', + function($timeout, sfSelect, schemaForm, sfValidator, sfPath) { var setIndex = function(index) { return function(form) { @@ -1692,8 +1750,42 @@ angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sf return list; }; + function destroyArrayItem(item) { + item._destroy = true; + + Object.keys(item).filter(function(key) { + return !/^_(destroy|id)$/.test(key); + }).forEach(function(key) { + delete item[key]; + }); + + return item; + } + scope.deleteFromArray = function(index) { - list.splice(index, 1); + var hasAcm; + + if (angular.isObject(list[index])) { + // Check to see if this object has ACM, anywhere. + angular.forEach(list[index], function fn(value, key) { + if (hasAcm || (hasAcm = key === 'acm')) { + // If we've already found ACM, then no need to continue. + } else if (angular.isObject(value)) { + angular.forEach(value, fn); + } + }); + } + + if (list[index].$$new || !list[index]._id) { + list.splice(index, 1); + } else { + destroyArrayItem(list[index]); + } + + // Only emit the 'setCapco' event if the deleted object had CAPCO. + if (hasAcm) { + scope.$emit('setCapco'); + } // Trigger validation. scope.validateArray(); @@ -1796,11 +1888,27 @@ angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sf scope.$on('schemaFormValidate', scope.validateArray); scope.hasSuccess = function() { - return ngModel.$valid && !ngModel.$pristine; + if (scope.options && scope.options.pristine && + scope.options.pristine.success === false) { + return ngModel.$valid && + !ngModel.$pristine && !ngModel.$isEmpty(ngModel.$modelValue); + } else { + return ngModel.$valid && + (!ngModel.$pristine || !ngModel.$isEmpty(ngModel.$modelValue)); + } }; scope.hasError = function() { - return ngModel.$invalid; + if (!scope.options || !scope.options.pristine || scope.options.pristine.errors !== false) { + // Show errors in pristine forms. The default. + // Note that "validateOnRender" option defaults to *not* validate initial form. + // so as a default there won't be any error anyway, but if the model is modified + // from the outside the error will show even if the field is pristine. + return ngModel.$invalid; + } else { + // Don't show errors in pristine forms. + return ngModel.$invalid && !ngModel.$pristine; + } }; scope.schemaError = function() { @@ -1835,7 +1943,7 @@ angular.module('schemaForm').directive('sfChanged', function() { if (form && form.onChange) { ctrl.$viewChangeListeners.push(function() { if (angular.isFunction(form.onChange)) { - form.onChange(ctrl.$modelValue, form); + form.onChange(ctrl.$modelValue, form, scope); } else { scope.evalExpr(form.onChange, {'modelValue': ctrl.$modelValue, form: form}); } @@ -1954,20 +2062,35 @@ angular.module('schemaForm').directive('sfField', return (expression && $interpolate(expression)(locals)); }; - //This works since we ot the ngModel from the array or the schema-validate directive. + //This works since we get the ngModel from the array or the schema-validate directive. scope.hasSuccess = function() { if (!scope.ngModel) { return false; } - return scope.ngModel.$valid && + if (scope.options && scope.options.pristine && + scope.options.pristine.success === false) { + return scope.ngModel.$valid && + !scope.ngModel.$pristine && !scope.ngModel.$isEmpty(scope.ngModel.$modelValue); + } else { + return scope.ngModel.$valid && (!scope.ngModel.$pristine || !scope.ngModel.$isEmpty(scope.ngModel.$modelValue)); + } }; scope.hasError = function() { if (!scope.ngModel) { return false; } - return scope.ngModel.$invalid && !scope.ngModel.$pristine; + if (!scope.options || !scope.options.pristine || scope.options.pristine.errors !== false) { + // Show errors in pristine forms. The default. + // Note that "validateOnRender" option defaults to *not* validate initial form. + // so as a default there won't be any error anyway, but if the model is modified + // from the outside the error will show even if the field is pristine. + return scope.ngModel.$invalid; + } else { + // Don't show errors in pristine forms. + return scope.ngModel.$invalid && !scope.ngModel.$pristine; + } }; /** @@ -2025,7 +2148,8 @@ angular.module('schemaForm').directive('sfField', scope.$broadcast('schemaFormValidate'); } } - }); + } + ); // Clean up the model when the corresponding form field is $destroy-ed. // Default behavior can be supplied as a globalOption, and behavior can be overridden @@ -2092,65 +2216,85 @@ angular.module('schemaForm').directive('sfMessage', scope.$watch(attrs.sfMessage, function(msg) { if (msg) { message = $sanitize(msg); - if (scope.ngModel) { - update(scope.ngModel.$valid); - } else { - update(); - } + update(!!scope.ngModel); } }); } - var update = function(valid) { - if (valid && !scope.hasError()) { - element.html(message); - } else { - var errors = []; - angular.forEach(((scope.ngModel && scope.ngModel.$error) || {}), function(status, code) { - if (status) { - // if true then there is an error - // Angular 1.3 removes properties, so we will always just have errors. - // Angular 1.2 sets them to false. - errors.push(code); - } - }); + var currentMessage; + // Only call html() if needed. + var setMessage = function(msg) { + if (msg !== currentMessage) { + element.html(msg); + currentMessage = msg; + } + }; - // In Angular 1.3 we use one $validator to stop the model value from getting updated. - // this means that we always end up with a 'schemaForm' error. - errors = errors.filter(function(e) { return e !== 'schemaForm'; }); - - // We only show one error. - // TODO: Make that optional - var error = errors[0]; - - if (error) { - element.html(sfErrorMessage.interpolate( - error, - scope.ngModel.$modelValue, - scope.ngModel.$viewValue, - scope.form, - scope.options && scope.options.validationMessage - )); + var update = function(checkForErrors) { + if (checkForErrors) { + if (!scope.hasError()) { + setMessage(message); } else { - element.html(message); + var errors = []; + angular.forEach(scope.ngModel && scope.ngModel.$error, function(status, code) { + if (status) { + // if true then there is an error + // Angular 1.3 removes properties, so we will always just have errors. + // Angular 1.2 sets them to false. + errors.push(code); + } + }); + + // In Angular 1.3 we use one $validator to stop the model value from getting updated. + // this means that we always end up with a 'schemaForm' error. + errors = errors.filter(function(e) { return e !== 'schemaForm'; }); + + // We only show one error. + // TODO: Make that optional + var error = errors[0]; + + if (error) { + setMessage(sfErrorMessage.interpolate( + error, + scope.ngModel.$modelValue, + scope.ngModel.$viewValue, + scope.form, + scope.options && scope.options.validationMessage + )); + } else { + setMessage(message); + } } + } else { + setMessage(message); } }; // Update once. update(); - scope.$watchCollection('ngModel.$error', function() { - if (scope.ngModel) { - update(scope.ngModel.$valid); + var once = scope.$watch('ngModel',function(ngModel) { + if (ngModel) { + // We also listen to changes of the model via parsers and formatters. + // This is since both the error message can change and given a pristine + // option to not show errors the ngModel.$error might not have changed + // but we're not pristine any more so we should change! + ngModel.$parsers.push(function(val) { update(true); return val; }); + ngModel.$formatters.push(function(val) { update(true); return val; }); + once(); } }); + // We watch for changes in $error + scope.$watchCollection('ngModel.$error', function() { + update(!!scope.ngModel); + }); + } }; }]); -/** +/* * Directive that handles the model arrays */ angular.module('schemaForm').directive('sfNewArray', ['sfSelect', 'sfPath', 'schemaForm', @@ -2170,7 +2314,10 @@ function(sel, sfPath, schemaForm) { //scope.modelArray = modelArray; scope.modelArray = scope.$eval(attrs.sfNewArray); // validateField method is exported by schema-validate - if (scope.validateField) { + if (scope.ngModel && scope.ngModel.$pristine && scope.firstDigest && + (!scope.options || scope.options.validateOnRender !== true)) { + return; + } else if (scope.validateField) { scope.validateField(); } }; @@ -2185,6 +2332,18 @@ function(sel, sfPath, schemaForm) { } }; + // If model is undefined make sure it gets set. + var getOrCreateModel = function() { + var model = scope.modelArray; + if (!model) { + var selection = sfPath.parse(attrs.sfNewArray); + model = []; + sel(selection, scope, model); + scope.modelArray = model; + } + return model; + }; + // We need the form definition to make a decision on how we should listen. var once = scope.$watch('form', function(form) { if (!form) { @@ -2255,7 +2414,7 @@ function(sel, sfPath, schemaForm) { //To get two way binding we also watch our titleMapValues scope.$watchCollection('titleMapValues', function(vals, old) { if (vals && vals !== old) { - var arr = scope.modelArray; + var arr = getOrCreateModel(); // Apparently the fastest way to clear an array, readable too. // http://jsperf.com/array-destroy/32 @@ -2281,38 +2440,79 @@ function(sel, sfPath, schemaForm) { }); scope.appendToArray = function() { - var empty; + // Create and set an array if needed. + var model = getOrCreateModel(); + // Same old add empty things to the array hack :( - if (scope.form && scope.form.schema) { - if (scope.form.schema.items) { - if (scope.form.schema.items.type === 'object') { - empty = {}; - } else if (scope.form.schema.items.type === 'array') { - empty = []; + if (scope.form && scope.form.schema && scope.form.schema.items) { + + var items = scope.form.schema.items; + if (items.type && items.type.indexOf('object') !== -1) { + empty = {}; + + // Check for possible defaults + if (!scope.options || scope.options.setSchemaDefaults !== false) { + empty = angular.isDefined(items['default']) ? items['default'] : empty; + + // Check for defaults further down in the schema. + // If the default instance sets the new array item to something falsy, i.e. null + // then there is no need to go further down. + if (empty) { + schemaForm.traverseSchema(items, function(prop, path) { + if (angular.isDefined(prop['default'])) { + sel(path, empty, prop['default']); + } + }); + } } - } - } - var model = scope.modelArray; - if (!model) { - // Create and set an array if needed. - var selection = sfPath.parse(attrs.sfNewArray); - model = []; - sel(selection, scope, model); - scope.modelArray = model; + } else if (items.type && items.type.indexOf('array') !== -1) { + empty = []; + if (!scope.options || scope.options.setSchemaDefaults !== false) { + empty = items['default'] || empty; + } + } else { + // No type? could still have defaults. + if (!scope.options || scope.options.setSchemaDefaults !== false) { + empty = items['default'] || empty; + } + } } model.push(empty); return model; }; + function destroyArrayItem(item) { + item._destroy = true; + + Object.keys(item).filter(function(key) { + return !/^_(destroy|id)$/.test(key); + }).forEach(function(key) { + delete item[key]; + }); + + return item; + } + scope.deleteFromArray = function(index) { var model = scope.modelArray; if (model) { - model.splice(index, 1); + if (model[index].$$new || !model[index]._id) { + model.splice(index, 1); + } else { + destroyArrayItem(model[index]); + + $timeout(function() { + destroyArrayItem(model[index]); + }); + } } + + scope.$emit('setCapco'); + return model; }; @@ -2369,8 +2569,8 @@ FIXME: real documentation angular.module('schemaForm') .directive('sfSchema', -['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', 'sfBuilder', - function($compile, schemaForm, schemaFormDecorators, sfSelect, sfPath, sfBuilder) { +['$compile', '$http', '$templateCache', '$q','schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', 'sfBuilder', + function($compile, $http, $templateCache, $q, schemaForm, schemaFormDecorators, sfSelect, sfPath, sfBuilder) { return { scope: { @@ -2429,8 +2629,27 @@ angular.module('schemaForm') // Common renderer function, can either be triggered by a watch or by an event. var render = function(schema, form) { - var merged = schemaForm.merge(schema, form, ignore, scope.options); + var asyncTemplates = []; + var merged = schemaForm.merge(schema, form, ignore, scope.options, undefined, asyncTemplates); + + if (asyncTemplates.length > 0) { + // Pre load all async templates and put them on the form for the builder to use. + $q.all(asyncTemplates.map(function(form) { + return $http.get(form.templateUrl, {cache: $templateCache}).then(function(res) { + form.template = res.data; + }); + })).then(function() { + internalRender(schema, form, merged); + }); + + } else { + internalRender(schema, form, merged); + } + + }; + + var internalRender = function(schema, form, merged) { // Create a new form and destroy the old one. // Not doing keeps old form elements hanging around after // they have been removed from the DOM @@ -2463,6 +2682,16 @@ angular.module('schemaForm') var lookup = Object.create(null); scope.lookup(lookup); // give the new lookup to the controller. element[0].appendChild(sfBuilder.build(merged, decorator, slots, lookup)); + + // We need to know if we're in the first digest looping + // I.e. just rendered the form so we know not to validate + // empty fields. + childScope.firstDigest = true; + // We use a ordinary timeout since we don't need a digest after this. + setTimeout(function() { + childScope.firstDigest = false; + }, 0); + //compile only children $compile(element.children())(childScope); @@ -2563,11 +2792,13 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse sfSelect(path, scope.model, ngModel.$modelValue); }); }); - } + }; + // Validate against the schema. var validate = function(viewValue) { + //console.log('validate called', viewValue) //Still might be undefined if (!form) { return viewValue; @@ -2579,7 +2810,7 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse } var result = sfValidator.validate(form, viewValue); - + //console.log('result is', result) // Since we might have different tv4 errors we must clear all // errors that start with tv4- Object.keys(ngModel.$error) @@ -2634,6 +2865,7 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse // updating if we've found an error. if (ngModel.$validators) { ngModel.$validators.schemaForm = function() { + //console.log('validators called.') // Any error and we're out of here! return !Object.keys(ngModel.$error).some(function(e) { return e !== 'schemaForm';}); }; @@ -2677,6 +2909,17 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse } }; + ngModel.$formatters.push(function(val) { + // When a form first loads this will be called for each field. + // we usually don't want that. + if (ngModel.$pristine && scope.firstDigest && + (!scope.options || scope.options.validateOnRender !== true)) { + return val; + } + validate(ngModel.$modelValue); + return val; + }); + // Listen to an event so we can validate the input on request scope.$on('schemaFormValidate', scope.validateField); diff --git a/dist/schema-form.min.js b/dist/schema-form.min.js index d9662f710..7b6a4f04f 100644 --- a/dist/schema-form.min.js +++ b/dist/schema-form.min.js @@ -1 +1 @@ -!function(e,t){"function"==typeof define&&define.amd?define(["angular","objectpath","tv4"],t):"object"==typeof exports?module.exports=t(require("angular"),require("objectpath"),require("tv4")):e.schemaForm=t(e.angular,e.objectpath,e.tv4)}(this,function(e,t,r){var n=[];try{e.module("ngSanitize"),n.push("ngSanitize")}catch(i){}try{e.module("ui.sortable"),n.push("ui.sortable")}catch(i){}try{e.module("angularSpectrumColorpicker"),n.push("angularSpectrumColorpicker")}catch(i){}var o=e.module("schemaForm",n);return e.module("schemaForm").provider("sfPath",[function(){var r=window.ObjectPath||t,n={parse:r.parse};1===e.version.major&&e.version.minor<3?n.stringify=function(e){return Array.isArray(e)?e.join("."):e.toString()}:n.stringify=r.stringify,n.normalize=function(e,t){return n.stringify(Array.isArray(e)?e:n.parse(e),t)},this.parse=n.parse,this.stringify=n.stringify,this.normalize=n.normalize,this.$get=function(){return n}}]),e.module("schemaForm").factory("sfSelect",["sfPath",function(e){var t=/^\d+$/;return function(r,n,i){n||(n=this);var o="string"==typeof r?e.parse(r):r;if("undefined"!=typeof i&&1===o.length)return n[o[0]]=i,n;"undefined"!=typeof i&&"undefined"==typeof n[o[0]]&&(n[o[0]]=o.length>2&&t.test(o[1])?[]:{});for(var a=n[o[0]],l=1;l0&&e.fieldFrag.firstChild.setAttribute("ng-model-options",JSON.stringify(e.form.ngModelOptions))},transclusion:function(e){var t=e.fieldFrag.querySelectorAll("[sf-field-transclude]");if(t.length)for(var r=0;r0;)d.appendChild(p.childNodes[0]);var y={fieldFrag:d,form:c,lookup:u,state:s,path:l+"["+f+"]",build:function(e,n,i){return a(e,t,r,o,n,i,u)}};"function"==typeof m.builder?m.builder(y):m.builder.forEach(function(e){e(y)}),(i(c,o)||e).appendChild(d)}else{var v=document.createElement(n(t.__name,"-"));s.arrayCompatFlag?v.setAttribute("form","copyWithIndex($index)"):v.setAttribute("form",l+"["+f+"]"),(i(c,o)||e).appendChild(v)}return e}},c),c};return{build:function(t,r,n,i){return a(t,r,function(t){return e.get(t)},n,void 0,void 0,i)},builder:o,internalBuild:a}}]}]),e.module("schemaForm").provider("schemaFormDecorators",["$compileProvider","sfPathProvider",function(t,r){var n="",i={},o=function(e,t){"sfDecorator"===e&&(e=n);var r=i[e];return r[t.type]?r[t.type].template:r["default"].template},a=function(n){t.directive(n,["$parse","$compile","$http","$templateCache","$interpolate","$q","sfErrorMessage","sfPath","sfSelect",function(t,i,a,l,s,u,c,f,m){return{restrict:"AE",replace:!1,transclude:!1,scope:!0,require:"?^sfSchema",link:function(t,d,p,h){t.$on("schemaFormPropagateNgModelController",function(e,r){e.stopPropagation(),e.preventDefault(),t.ngModel=r}),t.showTitle=function(){return t.form&&t.form.notitle!==!0&&t.form.title},t.listToCheckboxValues=function(t){var r={};return e.forEach(t,function(e){r[e]=!0}),r},t.checkboxValuesToList=function(t){var r=[];return e.forEach(t,function(e,t){e&&r.push(t)}),r},t.buttonClick=function(r,n){e.isFunction(n.onClick)?n.onClick(r,n):e.isString(n.onClick)&&(h?h.evalInParentScope(n.onClick,{$event:r,form:n}):t.$eval(n.onClick,{$event:r,form:n}))},t.evalExpr=function(e,r){return h?h.evalInParentScope(e,r):t.$eval(e,r)},t.evalInScope=function(e,r){return e?t.$eval(e,r):void 0},t.interp=function(e,t){return e&&s(e)(t)},t.hasSuccess=function(){return t.ngModel?t.ngModel.$valid&&(!t.ngModel.$pristine||!t.ngModel.$isEmpty(t.ngModel.$modelValue)):!1},t.hasError=function(){return t.ngModel?t.ngModel.$invalid&&!t.ngModel.$pristine:!1},t.errorMessage=function(e){return c.interpolate(e&&e.code+""||"default",t.ngModel&&t.ngModel.$modelValue||"",t.ngModel&&t.ngModel.$viewValue||"",t.form,t.options&&t.options.validationMessage)};var y=t.$watch(p.form,function(s){if(s){s.ngModelOptions=s.ngModelOptions||{},t.form=s;var c;if("template"===s.type&&s.template)c=u.when(s.template);else{var p="template"===s.type?s.templateUrl:o(n,s);c=a.get(p,{cache:l}).then(function(e){return e.data})}c.then(function(n){if(s.key){var o=s.key?r.stringify(s.key).replace(/"/g,"""):"";n=n.replace(/\$\$value\$\$/g,"model"+("["!==o[0]?".":"")+o)}if(d.html(n),s.condition){var a='evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex})';s.key&&(a='evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex, "modelValue": model'+f.stringify(s.key)+"})"),e.forEach(d.children(),function(e){var t=e.getAttribute("ng-if");e.setAttribute("ng-if",t?"("+t+") || ("+a+")":a)})}i(d.contents())(t)}),s.key&&(t.$on("schemaForm.error."+s.key.join("."),function(e,r,n,i){(n===!0||n===!1)&&(i=n,n=void 0),t.ngModel&&r&&(t.ngModel.$setDirty?t.ngModel.$setDirty():(t.ngModel.$dirty=!0,t.ngModel.$pristine=!1),n&&(s.validationMessage||(s.validationMessage={}),s.validationMessage[r]=n),t.ngModel.$setValidity(r,i===!0),i===!0&&t.$broadcast("schemaFormValidate"))}),t.$on("$destroy",function(){if(!t.externalDestructionInProgress){var e=s.destroyStrategy||t.options&&t.options.destroyStrategy||"remove";if(s.key&&"retain"!==e){var r=t.model;if(s.key.length>1&&(r=m(s.key.slice(0,s.key.length-1),r)),void 0===r)return;var n=s.schema&&s.schema.type||"";"empty"===e&&-1!==n.indexOf("string")?r[s.key.slice(-1)]="":"empty"===e&&-1!==n.indexOf("object")?r[s.key.slice(-1)]={}:"empty"===e&&-1!==n.indexOf("array")?r[s.key.slice(-1)]=[]:"null"===e?r[s.key.slice(-1)]=null:delete r[s.key.slice(-1)]}}})),y()}})}}}])},l=function(r,n,i){i=e.isDefined(i)?i:!1,t.directive("sf"+e.uppercase(r[0])+r.substr(1),function(){return{restrict:"EAC",scope:!0,replace:!0,transclude:i,template:'',link:function(t,n,i){var o={items:"c",titleMap:"c",schema:"c"},a={type:r},l=!0;e.forEach(i,function(r,n){if("$"!==n[0]&&0!==n.indexOf("ng")&&"sfField"!==n){var s=function(r){e.isDefined(r)&&r!==a[n]&&(a[n]=r,l&&a.type&&(a.key||e.isUndefined(i.key))&&(t.form=a,l=!1))};"model"===n?t.$watch(r,function(e){e&&t.model!==e&&(t.model=e)}):"c"===o[n]?t.$watchCollection(r,s):i.$observe(n,s)}})}}})};this.createDecorator=function(t,r){i[t]={__name:t},e.forEach(r,function(e,r){i[t][r]={template:e,replace:!1,builder:[]}}),i[n]||(n=t),a(t)},this.defineDecorator=function(t,r){i[t]={__name:t},e.forEach(r,function(r,n){r.builder=r.builder||[],r.replace=e.isDefined(r.replace)?r.replace:!0,i[t][n]=r}),i[n]||(n=t),a(t)},this.createDirective=l,this.createDirectives=function(t){e.forEach(t,function(e,t){l(t,e)})},this.decorator=function(e){return e=e||n,i[e]},this.addMapping=function(e,t,r,n,o){i[e]&&(i[e][t]={template:r,builder:n,replace:!!o})},this.$get=function(){return{decorator:function(e){return i[e]||i[n]},defaultDecorator:n}},a("sfDecorator")}]),e.module("schemaForm").provider("sfErrorMessage",function(){var t={"default":"Field does not validate",0:"Invalid type, expected {{schema.type}}",1:"No enum match for: {{viewValue}}",10:'Data does not match any schemas from "anyOf"',11:'Data does not match any schemas from "oneOf"',12:'Data is valid against more than one schema from "oneOf"',13:'Data matches schema from "not"',100:"Value is not a multiple of {{schema.multipleOf}}",101:"{{viewValue}} is less than the allowed minimum of {{schema.minimum}}",102:"{{viewValue}} is equal to the exclusive minimum {{schema.minimum}}",103:"{{viewValue}} is greater than the allowed maximum of {{schema.maximum}}",104:"{{viewValue}} is equal to the exclusive maximum {{schema.maximum}}",105:"Value is not a valid number",200:"String is too short ({{viewValue.length}} chars), minimum {{schema.minLength}}",201:"String is too long ({{viewValue.length}} chars), maximum {{schema.maxLength}}",202:"String does not match pattern: {{schema.pattern}}",300:"Too few properties defined, minimum {{schema.minProperties}}",301:"Too many properties defined, maximum {{schema.maxProperties}}",302:"Required",303:"Additional properties not allowed",304:"Dependency failed - key must exist",400:"Array is too short ({{value.length}}), minimum {{schema.minItems}}",401:"Array is too long ({{value.length}}), maximum {{schema.maxItems}}",402:"Array items are not unique",403:"Additional items not allowed",500:"Format validation failed",501:'Keyword failed: "{{title}}"',600:"Circular $refs",1e3:"Unknown property (not in schema)"};t.number=t[105],t.required=t[302],t.min=t[101],t.max=t[103],t.maxlength=t[201],t.minlength=t[200],t.pattern=t[202],this.setDefaultMessages=function(e){t=e},this.getDefaultMessages=function(){return t},this.setDefaultMessage=function(e,r){t[e]=r},this.$get=["$interpolate",function(r){var n={};return n.defaultMessages=t,n.interpolate=function(n,i,o,a,l){l=l||{};var s=a.validationMessage||{};0===n.indexOf("tv4-")&&(n=n.substring(4));var u=s["default"]||l["default"]||"";[s,l,t].some(function(t){return e.isString(t)||e.isFunction(t)?(u=t,!0):t&&t[n]?(u=t[n],!0):void 0});var c={error:n,value:i,viewValue:o,form:a,schema:a.schema,title:a.title||a.schema&&a.schema.title};return e.isFunction(u)?u(c):r(u)(c)},n}]}),e.module("schemaForm").provider("schemaForm",["sfPathProvider",function(t){var r=function(e){if(Array.isArray(e)&&2==e.length){if("null"===e[0])return e[1];if("null"===e[1])return e[0]}return e},n=function(e){var t=[];return e.forEach(function(e){t.push({name:e,value:e})}),t},i=function(t,r){if(!e.isArray(t)){var n=[];return r?e.forEach(r,function(e,r){n.push({name:t[e],value:e})}):e.forEach(t,function(e,t){n.push({name:e,value:t})}),n}return t},o=function(t,n,i){var o=h[r(n.type)];if(o)for(var a,l=0;l1&&(d={type:"section",items:l.items.map(function(t){return t.ngModelOptions=l.ngModelOptions,e.isUndefined(t.readonly)&&(t.readonly=l.readonly),t})})}if(a.copyWithIndex=function(t){if(!c[t]&&d){var n=e.copy(d);n.arrayIndex=t,r.traverseForm(n,o(t)),c[t]=n}return c[t]},a.appendToArray=function(){var n=s.length,i=a.copyWithIndex(n);if(r.traverseForm(i,function(r){if(r.key){var n;e.isDefined(r["default"])&&(n=r["default"]),e.isDefined(r.schema)&&e.isDefined(r.schema["default"])&&(n=r.schema["default"]),e.isDefined(n)&&t(r.key,a.model,n)}}),n===s.length){var o,u=t("schema.items.type",l);"object"===u?o={}:"array"===u&&(o=[]),s.push(o)}return a.validateArray(),s},a.deleteFromArray=function(e){return s.splice(e,1),a.validateArray(),u&&u.$setDirty&&u.$setDirty(),s},l.titleMap||l.startEmpty===!0||0!==s.length||a.appendToArray(),l.titleMap&&l.titleMap.length>0){a.titleMapValues=[];var p=function(e){a.titleMapValues=[],e=e||[],l.titleMap.forEach(function(t){a.titleMapValues.push(-1!==e.indexOf(t.value))})};p(a.modelArray),a.$watchCollection("modelArray",p),a.$watchCollection("titleMapValues",function(e,t){if(e&&e!==t){for(var r=a.modelArray;r.length>0;)r.pop();l.titleMap.forEach(function(t,n){e[n]&&r.push(t.value)}),a.validateArray()}})}if(u){var h;a.validateArray=function(){var e=n.validate(l,a.modelArray.length>0?a.modelArray:void 0);Object.keys(u.$error).filter(function(e){return 0===e.indexOf("tv4-")}).forEach(function(e){u.$setValidity(e,!0)}),e.valid!==!1||!e.error||""!==e.error.dataPath&&e.error.dataPath!=="/"+l.key[l.key.length-1]||(u.$setViewValue(a.modelArray),h=e.error,u.$setValidity("tv4-"+e.error.code,!1))},a.$on("schemaFormValidate",a.validateArray),a.hasSuccess=function(){return u.$valid&&!u.$pristine},a.hasError=function(){return u.$invalid},a.schemaError=function(){return h}}f()}})}}}]),e.module("schemaForm").directive("sfChanged",function(){return{require:"ngModel",restrict:"AC",scope:!1,link:function(t,r,n,i){var o=t.$eval(n.sfChanged);o&&o.onChange&&i.$viewChangeListeners.push(function(){e.isFunction(o.onChange)?o.onChange(i.$modelValue,o):t.evalExpr(o.onChange,{modelValue:i.$modelValue,form:o})})}}}),e.module("schemaForm").directive("sfField",["$parse","$compile","$http","$templateCache","$interpolate","$q","sfErrorMessage","sfPath","sfSelect",function(t,r,n,i,o,a,l,s,u){return{restrict:"AE",replace:!1,transclude:!1,scope:!0,require:"^sfSchema",link:{pre:function(e,t,r,n){e.$on("schemaFormPropagateNgModelController",function(t,r){t.stopPropagation(),t.preventDefault(),e.ngModel=r}),e.form=n.lookup["f"+r.sfField]},post:function(t,r,n,i){t.showTitle=function(){return t.form&&t.form.notitle!==!0&&t.form.title},t.listToCheckboxValues=function(t){var r={};return e.forEach(t,function(e){r[e]=!0}),r},t.checkboxValuesToList=function(t){var r=[];return e.forEach(t,function(e,t){e&&r.push(t)}),r},t.buttonClick=function(r,n){e.isFunction(n.onClick)?n.onClick(r,n):e.isString(n.onClick)&&(i?i.evalInParentScope(n.onClick,{$event:r,form:n}):t.$eval(n.onClick,{$event:r,form:n}))},t.evalExpr=function(e,r){return i?i.evalInParentScope(e,r):t.$eval(e,r)},t.evalInScope=function(e,r){return e?t.$eval(e,r):void 0},t.interp=function(e,t){return e&&o(e)(t)},t.hasSuccess=function(){return t.ngModel?t.ngModel.$valid&&(!t.ngModel.$pristine||!t.ngModel.$isEmpty(t.ngModel.$modelValue)):!1},t.hasError=function(){return t.ngModel?t.ngModel.$invalid&&!t.ngModel.$pristine:!1},t.errorMessage=function(e){return l.interpolate(e&&e.code+""||"default",t.ngModel&&t.ngModel.$modelValue||"",t.ngModel&&t.ngModel.$viewValue||"",t.form,t.options&&t.options.validationMessage)};var a=t.form;a.key&&(t.$on("schemaForm.error."+a.key.join("."),function(e,r,n,i){(n===!0||n===!1)&&(i=n,n=void 0),t.ngModel&&r&&(t.ngModel.$setDirty?t.ngModel.$setDirty():(t.ngModel.$dirty=!0,t.ngModel.$pristine=!1),n&&(a.validationMessage||(a.validationMessage={}),a.validationMessage[r]=n),t.ngModel.$setValidity(r,i===!0),i===!0&&t.$broadcast("schemaFormValidate"))}),t.$on("$destroy",function(){if(!t.externalDestructionInProgress){var e=a.destroyStrategy||t.options&&t.options.destroyStrategy||"remove";if(a.key&&"retain"!==e){var r=t.model;if(a.key.length>1&&(r=u(a.key.slice(0,a.key.length-1),r)),void 0===r)return;var n=a.schema&&a.schema.type||"";"empty"===e&&-1!==n.indexOf("string")?r[a.key.slice(-1)]="":"empty"===e&&-1!==n.indexOf("object")?r[a.key.slice(-1)]={}:"empty"===e&&-1!==n.indexOf("array")?r[a.key.slice(-1)]=[]:"null"===e?r[a.key.slice(-1)]=null:delete r[a.key.slice(-1)]}}}))}}}}]),e.module("schemaForm").directive("sfMessage",["$injector","sfErrorMessage",function(t,r){var n=t.has("$sanitize")?t.get("$sanitize"):function(e){return e};return{scope:!1,restrict:"EA",link:function(t,i,o){var a="";o.sfMessage&&t.$watch(o.sfMessage,function(e){e&&(a=n(e),t.ngModel?l(t.ngModel.$valid):l())});var l=function(n){if(n&&!t.hasError())i.html(a);else{var o=[];e.forEach(t.ngModel&&t.ngModel.$error||{},function(e,t){e&&o.push(t)}),o=o.filter(function(e){return"schemaForm"!==e});var l=o[0];l?i.html(r.interpolate(l,t.ngModel.$modelValue,t.ngModel.$viewValue,t.form,t.options&&t.options.validationMessage)):i.html(a)}};l(),t.$watchCollection("ngModel.$error",function(){t.ngModel&&l(t.ngModel.$valid)})}}}]),e.module("schemaForm").directive("sfNewArray",["sfSelect","sfPath","schemaForm",function(t,r,n){return{scope:!1,link:function(i,o,a){i.min=0,i.modelArray=i.$eval(a.sfNewArray);var l=function(){i.modelArray=i.$eval(a.sfNewArray),i.validateField&&i.validateField()},s=function(){i.form&&i.form.onChange&&(e.isFunction(i.form.onChange)?i.form.onChange(i.modelArray,i.form):i.evalExpr(i.form.onChange,{modelValue:i.modelArray,form:i.form}))},u=i.$watch("form",function(e){if(e){if(e.titleMap||e.startEmpty===!0||i.modelArray&&0!==i.modelArray.length||i.appendToArray(),i.form&&i.form.schema&&i.form.schema.uniqueItems===!0?(i.$watch(a.sfNewArray,l,!0),i.$watch([a.sfNewArray,a.sfNewArray+".length"],s)):i.$watchGroup?i.$watchGroup([a.sfNewArray,a.sfNewArray+".length"],function(){l(),s()}):(i.$watch(a.sfNewArray,function(){l(),s()}),i.$watch(a.sfNewArray+".length",function(){l(),s()})),e.titleMap&&e.titleMap.length>0){i.titleMapValues=[];var t=function(t){i.titleMapValues=[],t=t||[],e.titleMap.forEach(function(e){i.titleMapValues.push(-1!==t.indexOf(e.value))})};t(i.modelArray),i.$watchCollection("modelArray",t),i.$watchCollection("titleMapValues",function(t,r){if(t&&t!==r){for(var n=i.modelArray;n.length>0;)n.pop();e.titleMap.forEach(function(e,r){t[r]&&n.push(e.value)}),i.validateField&&i.validateField()}})}u()}});i.appendToArray=function(){var e;i.form&&i.form.schema&&i.form.schema.items&&("object"===i.form.schema.items.type?e={}:"array"===i.form.schema.items.type&&(e=[]));var n=i.modelArray;if(!n){var o=r.parse(a.sfNewArray);n=[],t(o,i,n),i.modelArray=n}return n.push(e),n},i.deleteFromArray=function(e){var t=i.modelArray;return t&&t.splice(e,1),t};var c=function(e){return function(t){t.key&&(t.key[t.key.indexOf("")]=e)}},f={};i.copyWithIndex=function(t){var r=i.form;if(!f[t]){var o=r.items[0];if(r.items.length>1&&(o={type:"section",items:r.items.map(function(t){return t.ngModelOptions=r.ngModelOptions,e.isUndefined(t.readonly)&&(t.readonly=r.readonly),t})}),o){var a=e.copy(o);a.arrayIndex=t,n.traverseForm(a,c(t)),f[t]=a}}return f[t]}}}}]),e.module("schemaForm").directive("sfSchema",["$compile","schemaForm","schemaFormDecorators","sfSelect","sfPath","sfBuilder",function(t,r,n,i,o,a){return{scope:{schema:"=sfSchema",initialForm:"=sfForm",model:"=sfModel",options:"=sfOptions"},controller:["$scope",function(e){this.evalInParentScope=function(t,r){return e.$parent.$eval(t,r)};var t=this;e.lookup=function(e){return e&&(t.lookup=e),t.lookup}}],replace:!1,restrict:"A",transclude:!0,require:"?form",link:function(o,l,s,u,c){o.formCtrl=u;var f={};c(o,function(e){if(e.addClass("schema-form-ignore"),l.prepend(e),l[0].querySelectorAll){var t=l[0].querySelectorAll("[ng-model]");if(t)for(var r=0;r0&&(d.schema=e,d.form=t,p(e,t))}),o.$on("schemaFormRedraw",function(){var e=o.schema,t=o.initialForm||["*"];e&&p(e,t)}),o.$on("$destroy",function(){o.externalDestructionInProgress=!0}),o.evalExpr=function(e,t){return o.$parent.$eval(e,t)}}}}]),e.module("schemaForm").directive("schemaValidate",["sfValidator","$parse","sfSelect",function(t,r,n){return{restrict:"A",scope:!1,priority:500,require:"ngModel",link:function(r,i,o,a){r.$emit("schemaFormPropagateNgModelController",a);var l=null,s=r.$eval(o.schemaValidate);s.copyValueTo&&a.$viewChangeListeners.push(function(){var t=s.copyValueTo;e.forEach(t,function(e){n(e,r.model,a.$modelValue)})});var u=function(e){if(!s)return e;if(r.options&&r.options.tv4Validation===!1)return e;var n=t.validate(s,e);return Object.keys(a.$error).filter(function(e){return 0===e.indexOf("tv4-")}).forEach(function(e){a.$setValidity(e,!0)}),n.valid?e:(a.$setValidity("tv4-"+n.error.code,!1),l=n.error,a.$validators?e:void 0)};"function"==typeof s.ngModel&&s.ngModel(a),["$parsers","$viewChangeListeners","$formatters"].forEach(function(e){s[e]&&a[e]&&s[e].forEach(function(t){a[e].push(t)})}),["$validators","$asyncValidators"].forEach(function(t){s[t]&&a[t]&&e.forEach(s[t],function(e,r){a[t][r]=e})}),a.$parsers.push(u),a.$validators&&(a.$validators.schemaForm=function(){return!Object.keys(a.$error).some(function(e){return"schemaForm"!==e})});var c=s.schema;r.validateField=function(){c&&-1!==c.type.indexOf("array")&&u(a.$modelValue),a.$setDirty?(a.$setDirty(),a.$setViewValue(a.$viewValue),a.$commitViewValue(),s.required&&a.$isEmpty(a.$modelValue)&&a.$setValidity("tv4-302",!1)):a.$setViewValue(a.$viewValue)},r.$on("schemaFormValidate",r.validateField),r.schemaError=function(){return l}}}}]),o}); \ No newline at end of file +!function(e,t){"function"==typeof define&&define.amd?define(["angular","objectpath","tv4"],t):"object"==typeof exports?module.exports=t(require("angular"),require("objectpath"),require("tv4")):e.schemaForm=t(e.angular,e.objectpath,e.tv4)}(this,function(e,t,r){var n=[];try{e.module("ngSanitize"),n.push("ngSanitize")}catch(i){}try{e.module("ui.sortable"),n.push("ui.sortable")}catch(i){}try{e.module("angularSpectrumColorpicker"),n.push("angularSpectrumColorpicker")}catch(i){}var o=e.module("schemaForm",n);return e.module("schemaForm").provider("sfPath",[function(){var r=window.ObjectPath||t,n={parse:r.parse};1===e.version.major&&e.version.minor<3?n.stringify=function(e){return Array.isArray(e)?e.join("."):e.toString()}:n.stringify=r.stringify,n.normalize=function(e,t){return n.stringify(Array.isArray(e)?e:n.parse(e),t)},this.parse=n.parse,this.stringify=n.stringify,this.normalize=n.normalize,this.$get=function(){return n}}]),e.module("schemaForm").factory("sfSelect",["sfPath",function(e){var t=/^\d+$/;return function(r,n,i){n||(n=this);var o="string"==typeof r?e.parse(r):r;if("undefined"!=typeof i&&1===o.length)return n[o[0]]=i,n;"undefined"!=typeof i&&"undefined"==typeof n[o[0]]&&(n[o[0]]=o.length>2&&t.test(o[1])?[]:{});for(var a=n[o[0]],l=1;l0&&e.fieldFrag.firstChild.setAttribute("ng-model-options",JSON.stringify(e.form.ngModelOptions))},transclusion:function(e){var t=e.fieldFrag.querySelectorAll("[sf-field-transclude]");if(t.length)for(var r=0;r0;)m.appendChild(p.childNodes[0]);var y={fieldFrag:m,form:c,lookup:u,state:s,path:a+"["+f+"]",build:function(e,n,i){return l(e,t,r,o,n,i,u)}},v=c.builder||d.builder;"function"==typeof v?v(y):v.forEach(function(e){e(y)}),(i(c,o)||e).appendChild(m)}else{var g=document.createElement(n(t.__name,"-"));s.arrayCompatFlag?g.setAttribute("form","copyWithIndex($index)"):g.setAttribute("form",a+"["+f+"]"),(i(c,o)||e).appendChild(g)}return e},c),c};return{build:function(t,r,n,i){return l(t,r,function(t,r){return"template"===t.type?t.template:e.get(r.template)},n,void 0,void 0,i)},builder:o,stdBuilders:a,internalBuild:l}}]}]),e.module("schemaForm").provider("schemaFormDecorators",["$compileProvider","sfPathProvider",function(t,r){var n="",i={},o=function(e,t){"sfDecorator"===e&&(e=n);var r=i[e];return r[t.type]?r[t.type].template:r["default"].template},a=function(n){t.directive(n,["$parse","$compile","$http","$templateCache","$interpolate","$q","sfErrorMessage","sfPath","sfSelect",function(t,i,a,l,s,u,c,f,d){return{restrict:"AE",replace:!1,transclude:!1,scope:!0,require:"?^sfSchema",link:function(t,m,p,h){t.$on("schemaFormPropagateNgModelController",function(e,r){e.stopPropagation(),e.preventDefault(),t.ngModel=r}),t.showTitle=function(){return t.form&&t.form.notitle!==!0&&t.form.title},t.listToCheckboxValues=function(t){var r={};return e.forEach(t,function(e){r[e]=!0}),r},t.checkboxValuesToList=function(t){var r=[];return e.forEach(t,function(e,t){e&&r.push(t)}),r},t.buttonClick=function(r,n){e.isFunction(n.onClick)?n.onClick(r,n):e.isString(n.onClick)&&(h?h.evalInParentScope(n.onClick,{$event:r,form:n}):t.$eval(n.onClick,{$event:r,form:n}))},t.evalExpr=function(e,r){return h?h.evalInParentScope(e,r):t.$eval(e,r)},t.evalInScope=function(e,r){return e?t.$eval(e,r):void 0},t.interp=function(e,t){return e&&s(e)(t)},t.hasSuccess=function(){return t.ngModel?t.ngModel.$valid&&(!t.ngModel.$pristine||!t.ngModel.$isEmpty(t.ngModel.$modelValue)):!1},t.hasError=function(){return t.ngModel?t.ngModel.$invalid&&!t.ngModel.$pristine:!1},t.errorMessage=function(e){return c.interpolate(e&&e.code+""||"default",t.ngModel&&t.ngModel.$modelValue||"",t.ngModel&&t.ngModel.$viewValue||"",t.form,t.options&&t.options.validationMessage)};var y=t.$watch(p.form,function(s){if(s){s.ngModelOptions=s.ngModelOptions||{},t.form=s;var c;if("template"===s.type&&s.template)c=u.when(s.template);else{var p="template"===s.type?s.templateUrl:o(n,s);c=a.get(p,{cache:l}).then(function(e){return e.data})}c.then(function(n){if(s.key){var o=s.key?r.stringify(s.key).replace(/"/g,"""):"";n=n.replace(/\$\$value\$\$/g,"model"+("["!==o[0]?".":"")+o)}if(m.html(n),s.condition){var a='evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex})';s.key&&(a='evalExpr(form.condition,{ model: model, "arrayIndex": arrayIndex, "modelValue": model'+f.stringify(s.key)+"})"),e.forEach(m.children(),function(e){var t=e.getAttribute("ng-if");e.setAttribute("ng-if",t?"("+t+") || ("+a+")":a)})}i(m.contents())(t)}),s.key&&(t.$on("schemaForm.error."+s.key.join("."),function(e,r,n,i){(n===!0||n===!1)&&(i=n,n=void 0),t.ngModel&&r&&(t.ngModel.$setDirty?t.ngModel.$setDirty():(t.ngModel.$dirty=!0,t.ngModel.$pristine=!1),n&&(s.validationMessage||(s.validationMessage={}),s.validationMessage[r]=n),t.ngModel.$setValidity(r,i===!0),i===!0&&t.$broadcast("schemaFormValidate"))}),t.$on("$destroy",function(){if(!t.externalDestructionInProgress){var e=s.destroyStrategy||t.options&&t.options.destroyStrategy||"remove";if(s.key&&"retain"!==e){var r=t.model;if(s.key.length>1&&(r=d(s.key.slice(0,s.key.length-1),r)),void 0===r)return;var n=s.schema&&s.schema.type||"";"empty"===e&&-1!==n.indexOf("string")?r[s.key.slice(-1)]="":"empty"===e&&-1!==n.indexOf("object")?r[s.key.slice(-1)]={}:"empty"===e&&-1!==n.indexOf("array")?r[s.key.slice(-1)]=[]:"null"===e?r[s.key.slice(-1)]=null:delete r[s.key.slice(-1)]}}})),y()}})}}}])},l=function(r,n,i){i=e.isDefined(i)?i:!1,t.directive("sf"+e.uppercase(r[0])+r.substr(1),function(){return{restrict:"EAC",scope:!0,replace:!0,transclude:i,template:'',link:function(t,n,i){var o={items:"c",titleMap:"c",schema:"c"},a={type:r},l=!0;e.forEach(i,function(r,n){if("$"!==n[0]&&0!==n.indexOf("ng")&&"sfField"!==n){var s=function(r){e.isDefined(r)&&r!==a[n]&&(a[n]=r,l&&a.type&&(a.key||e.isUndefined(i.key))&&(t.form=a,l=!1))};"model"===n?t.$watch(r,function(e){e&&t.model!==e&&(t.model=e)}):"c"===o[n]?t.$watchCollection(r,s):i.$observe(n,s)}})}}})};this.createDecorator=function(t,r){i[t]={__name:t},e.forEach(r,function(e,r){i[t][r]={template:e,replace:!1,builder:[]}}),i[n]||(n=t),a(t)},this.defineDecorator=function(t,r){i[t]={__name:t},e.forEach(r,function(r,n){r.builder=r.builder||[],r.replace=e.isDefined(r.replace)?r.replace:!0,i[t][n]=r}),i[n]||(n=t),a(t)},this.createDirective=l,this.createDirectives=function(t){e.forEach(t,function(e,t){l(t,e)})},this.decorator=function(e){return e=e||n,i[e]},this.addMapping=function(e,t,r,n,o){i[e]&&(i[e][t]={template:r,builder:n,replace:!!o})},this.defineAddOn=function(e,t,r,n){i[e]&&(i[e][t]={template:r,builder:n,replace:!0})},this.$get=function(){return{decorator:function(e){return i[e]||i[n]},defaultDecorator:n}},a("sfDecorator")}]),e.module("schemaForm").provider("sfErrorMessage",function(){var t={"default":"Field does not validate",0:"Invalid type, expected {{schema.type}}",1:"No enum match for: {{viewValue}}",10:'Data does not match any schemas from "anyOf"',11:'Data does not match any schemas from "oneOf"',12:'Data is valid against more than one schema from "oneOf"',13:'Data matches schema from "not"',100:"Value is not a multiple of {{schema.multipleOf}}",101:"{{viewValue}} is less than the allowed minimum of {{schema.minimum}}",102:"{{viewValue}} is equal to the exclusive minimum {{schema.minimum}}",103:"{{viewValue}} is greater than the allowed maximum of {{schema.maximum}}",104:"{{viewValue}} is equal to the exclusive maximum {{schema.maximum}}",105:"Value is not a valid number",200:"String is too short ({{viewValue.length}} chars), minimum {{schema.minLength}}",201:"String is too long ({{viewValue.length}} chars), maximum {{schema.maxLength}}",202:"String does not match pattern: {{schema.pattern}}",300:"Too few properties defined, minimum {{schema.minProperties}}",301:"Too many properties defined, maximum {{schema.maxProperties}}",302:"Required",303:"Additional properties not allowed",304:"Dependency failed - key must exist",400:"Array is too short ({{value.length}}), minimum {{schema.minItems}}",401:"Array is too long ({{value.length}}), maximum {{schema.maxItems}}",402:"Array items are not unique",403:"Additional items not allowed",500:"Format validation failed",501:'Keyword failed: "{{title}}"',600:"Circular $refs",1e3:"Unknown property (not in schema)"};t.number=t[105],t.required=t[302],t.min=t[101],t.max=t[103],t.maxlength=t[201],t.minlength=t[200],t.pattern=t[202],this.setDefaultMessages=function(e){t=e},this.getDefaultMessages=function(){return t},this.setDefaultMessage=function(e,r){t[e]=r},this.$get=["$interpolate",function(r){var n={};return n.defaultMessages=t,n.interpolate=function(n,i,o,a,l){l=l||{};var s=a.validationMessage||{};0===n.indexOf("tv4-")&&(n=n.substring(4));var u=s["default"]||l["default"]||"";[s,l,t].some(function(t){return e.isString(t)||e.isFunction(t)?(u=t,!0):t&&t[n]?(u=t[n],!0):void 0});var c={error:n,value:i,viewValue:o,form:a,schema:a.schema,title:a.title||a.schema&&a.schema.title};return e.isFunction(u)?u(c):r(u)(c)},n}]}),e.module("schemaForm").provider("schemaForm",["sfPathProvider",function(t){var r=function(e){if(Array.isArray(e)&&2==e.length){if("null"===e[0])return e[1];if("null"===e[1])return e[0]}return e},n=function(e){var t=[];return e.forEach(function(e){t.push({name:e,value:e})}),t},i=function(t,r){if(!e.isArray(t)){var n=[];return r?e.forEach(r,function(e,r){n.push({name:t[e],value:e})}):e.forEach(t,function(e,t){n.push({name:e,value:t})}),n}return t},o=function(t,n,i){var o=h[r(n.type)];if(o)for(var a,l=0;l1&&(p={type:"section",items:l.items.map(function(t){return t.ngModelOptions=l.ngModelOptions,e.isUndefined(t.readonly)&&(t.readonly=l.readonly),t})})}if(t.copyWithIndex=function(t){if(!c[t]&&p){var r=e.copy(p);r.arrayIndex=t,n.traverseForm(r,a(t)),c[t]=r}return c[t]},t.appendToArray=function(){var i=d.length,o=t.copyWithIndex(i);if(n.traverseForm(o,function(n){if(n.key){var i;e.isDefined(n["default"])&&(i=n["default"]),e.isDefined(n.schema)&&e.isDefined(n.schema["default"])&&(i=n.schema["default"]),e.isDefined(i)&&r(n.key,t.model,i)}}),i===d.length){var a,s=r("schema.items.type",l);"object"===s?a={}:"array"===s&&(a=[]),d.push(a)}return t.validateArray(),d},t.deleteFromArray=function(r){var n;return e.isObject(d[r])&&e.forEach(d[r],function i(t,r){n||(n="acm"===r)||e.isObject(t)&&e.forEach(t,i)}),d[r].$$new||!d[r]._id?d.splice(r,1):s(d[r]),n&&t.$emit("setCapco"),t.validateArray(),u&&u.$setDirty&&u.$setDirty(),d},l.titleMap||l.startEmpty===!0||0!==d.length||t.appendToArray(),l.titleMap&&l.titleMap.length>0){t.titleMapValues=[];var h=function(e){t.titleMapValues=[],e=e||[],l.titleMap.forEach(function(r){t.titleMapValues.push(-1!==e.indexOf(r.value))})};h(t.modelArray),t.$watchCollection("modelArray",h),t.$watchCollection("titleMapValues",function(e,r){if(e&&e!==r){for(var n=t.modelArray;n.length>0;)n.pop();l.titleMap.forEach(function(t,r){e[r]&&n.push(t.value)}),t.validateArray()}})}if(u){var y;t.validateArray=function(){var e=i.validate(l,t.modelArray.length>0?t.modelArray:void 0);Object.keys(u.$error).filter(function(e){return 0===e.indexOf("tv4-")}).forEach(function(e){u.$setValidity(e,!0)}),e.valid!==!1||!e.error||""!==e.error.dataPath&&e.error.dataPath!=="/"+l.key[l.key.length-1]||(u.$setViewValue(t.modelArray),y=e.error,u.$setValidity("tv4-"+e.error.code,!1))},t.$on("schemaFormValidate",t.validateArray),t.hasSuccess=function(){return t.options&&t.options.pristine&&t.options.pristine.success===!1?u.$valid&&!u.$pristine&&!u.$isEmpty(u.$modelValue):u.$valid&&(!u.$pristine||!u.$isEmpty(u.$modelValue))},t.hasError=function(){return t.options&&t.options.pristine&&t.options.pristine.errors===!1?u.$invalid&&!u.$pristine:u.$invalid},t.schemaError=function(){return y}}f()}})}}}]),e.module("schemaForm").directive("sfChanged",function(){return{require:"ngModel",restrict:"AC",scope:!1,link:function(t,r,n,i){var o=t.$eval(n.sfChanged);o&&o.onChange&&i.$viewChangeListeners.push(function(){e.isFunction(o.onChange)?o.onChange(i.$modelValue,o,t):t.evalExpr(o.onChange,{modelValue:i.$modelValue,form:o})})}}}),e.module("schemaForm").directive("sfField",["$parse","$compile","$http","$templateCache","$interpolate","$q","sfErrorMessage","sfPath","sfSelect",function(t,r,n,i,o,a,l,s,u){return{restrict:"AE",replace:!1,transclude:!1,scope:!0,require:"^sfSchema",link:{pre:function(e,t,r,n){e.$on("schemaFormPropagateNgModelController",function(t,r){t.stopPropagation(),t.preventDefault(),e.ngModel=r}),e.form=n.lookup["f"+r.sfField]},post:function(t,r,n,i){t.showTitle=function(){return t.form&&t.form.notitle!==!0&&t.form.title},t.listToCheckboxValues=function(t){var r={};return e.forEach(t,function(e){r[e]=!0}),r},t.checkboxValuesToList=function(t){var r=[];return e.forEach(t,function(e,t){e&&r.push(t)}),r},t.buttonClick=function(r,n){e.isFunction(n.onClick)?n.onClick(r,n):e.isString(n.onClick)&&(i?i.evalInParentScope(n.onClick,{$event:r,form:n}):t.$eval(n.onClick,{$event:r,form:n}))},t.evalExpr=function(e,r){return i?i.evalInParentScope(e,r):t.$eval(e,r)},t.evalInScope=function(e,r){return e?t.$eval(e,r):void 0},t.interp=function(e,t){return e&&o(e)(t)},t.hasSuccess=function(){return t.ngModel?t.options&&t.options.pristine&&t.options.pristine.success===!1?t.ngModel.$valid&&!t.ngModel.$pristine&&!t.ngModel.$isEmpty(t.ngModel.$modelValue):t.ngModel.$valid&&(!t.ngModel.$pristine||!t.ngModel.$isEmpty(t.ngModel.$modelValue)):!1},t.hasError=function(){return t.ngModel?t.options&&t.options.pristine&&t.options.pristine.errors===!1?t.ngModel.$invalid&&!t.ngModel.$pristine:t.ngModel.$invalid:!1},t.errorMessage=function(e){return l.interpolate(e&&e.code+""||"default",t.ngModel&&t.ngModel.$modelValue||"",t.ngModel&&t.ngModel.$viewValue||"",t.form,t.options&&t.options.validationMessage)};var a=t.form;a.key&&(t.$on("schemaForm.error."+a.key.join("."),function(e,r,n,i){(n===!0||n===!1)&&(i=n,n=void 0),t.ngModel&&r&&(t.ngModel.$setDirty?t.ngModel.$setDirty():(t.ngModel.$dirty=!0,t.ngModel.$pristine=!1),n&&(a.validationMessage||(a.validationMessage={}),a.validationMessage[r]=n),t.ngModel.$setValidity(r,i===!0),i===!0&&t.$broadcast("schemaFormValidate"))}),t.$on("$destroy",function(){if(!t.externalDestructionInProgress){var e=a.destroyStrategy||t.options&&t.options.destroyStrategy||"remove";if(a.key&&"retain"!==e){var r=t.model;if(a.key.length>1&&(r=u(a.key.slice(0,a.key.length-1),r)),void 0===r)return;var n=a.schema&&a.schema.type||"";"empty"===e&&-1!==n.indexOf("string")?r[a.key.slice(-1)]="":"empty"===e&&-1!==n.indexOf("object")?r[a.key.slice(-1)]={}:"empty"===e&&-1!==n.indexOf("array")?r[a.key.slice(-1)]=[]:"null"===e?r[a.key.slice(-1)]=null:delete r[a.key.slice(-1)]}}}))}}}}]),e.module("schemaForm").directive("sfMessage",["$injector","sfErrorMessage",function(t,r){var n=t.has("$sanitize")?t.get("$sanitize"):function(e){return e};return{scope:!1,restrict:"EA",link:function(t,i,o){var a="";o.sfMessage&&t.$watch(o.sfMessage,function(e){e&&(a=n(e),u(!!t.ngModel))});var l,s=function(e){e!==l&&(i.html(e),l=e)},u=function(n){if(n)if(t.hasError()){var i=[];e.forEach(t.ngModel&&t.ngModel.$error,function(e,t){e&&i.push(t)}),i=i.filter(function(e){return"schemaForm"!==e});var o=i[0];s(o?r.interpolate(o,t.ngModel.$modelValue,t.ngModel.$viewValue,t.form,t.options&&t.options.validationMessage):a)}else s(a);else s(a)};u();var c=t.$watch("ngModel",function(e){e&&(e.$parsers.push(function(e){return u(!0),e}),e.$formatters.push(function(e){return u(!0),e}),c())});t.$watchCollection("ngModel.$error",function(){u(!!t.ngModel)})}}}]),e.module("schemaForm").directive("sfNewArray",["sfSelect","sfPath","schemaForm",function(t,r,n){return{scope:!1,link:function(i,o,a){function l(e){return e._destroy=!0,Object.keys(e).filter(function(e){return!/^_(destroy|id)$/.test(e)}).forEach(function(t){delete e[t]}),e}i.min=0,i.modelArray=i.$eval(a.sfNewArray);var s=function(){i.modelArray=i.$eval(a.sfNewArray),(!(i.ngModel&&i.ngModel.$pristine&&i.firstDigest)||i.options&&i.options.validateOnRender===!0)&&i.validateField&&i.validateField()},u=function(){i.form&&i.form.onChange&&(e.isFunction(i.form.onChange)?i.form.onChange(i.modelArray,i.form):i.evalExpr(i.form.onChange,{modelValue:i.modelArray,form:i.form}))},c=function(){var e=i.modelArray;if(!e){var n=r.parse(a.sfNewArray);e=[],t(n,i,e),i.modelArray=e}return e},f=i.$watch("form",function(e){if(e){if(e.titleMap||e.startEmpty===!0||i.modelArray&&0!==i.modelArray.length||i.appendToArray(),i.form&&i.form.schema&&i.form.schema.uniqueItems===!0?(i.$watch(a.sfNewArray,s,!0),i.$watch([a.sfNewArray,a.sfNewArray+".length"],u)):i.$watchGroup?i.$watchGroup([a.sfNewArray,a.sfNewArray+".length"],function(){s(),u()}):(i.$watch(a.sfNewArray,function(){s(),u()}),i.$watch(a.sfNewArray+".length",function(){s(),u()})),e.titleMap&&e.titleMap.length>0){i.titleMapValues=[];var t=function(t){i.titleMapValues=[],t=t||[],e.titleMap.forEach(function(e){i.titleMapValues.push(-1!==t.indexOf(e.value))})};t(i.modelArray),i.$watchCollection("modelArray",t),i.$watchCollection("titleMapValues",function(t,r){if(t&&t!==r){for(var n=c();n.length>0;)n.pop();e.titleMap.forEach(function(e,r){t[r]&&n.push(e.value)}),i.validateField&&i.validateField()}})}f()}});i.appendToArray=function(){var r,o=c();if(i.form&&i.form.schema&&i.form.schema.items){var a=i.form.schema.items;a.type&&-1!==a.type.indexOf("object")?(r={},i.options&&i.options.setSchemaDefaults===!1||(r=e.isDefined(a["default"])?a["default"]:r,r&&n.traverseSchema(a,function(n,i){e.isDefined(n["default"])&&t(i,r,n["default"])}))):a.type&&-1!==a.type.indexOf("array")?(r=[],i.options&&i.options.setSchemaDefaults===!1||(r=a["default"]||r)):i.options&&i.options.setSchemaDefaults===!1||(r=a["default"]||r)}return o.push(r),o},i.deleteFromArray=function(e){var t=i.modelArray;return t&&(t[e].$$new||!t[e]._id?t.splice(e,1):(l(t[e]),$timeout(function(){l(t[e])}))),i.$emit("setCapco"),t};var d=function(e){return function(t){t.key&&(t.key[t.key.indexOf("")]=e)}},m={};i.copyWithIndex=function(t){var r=i.form;if(!m[t]){var o=r.items[0];if(r.items.length>1&&(o={type:"section",items:r.items.map(function(t){return t.ngModelOptions=r.ngModelOptions,e.isUndefined(t.readonly)&&(t.readonly=r.readonly),t})}),o){var a=e.copy(o);a.arrayIndex=t,n.traverseForm(a,d(t)),m[t]=a}}return m[t]}}}}]),e.module("schemaForm").directive("sfSchema",["$compile","$http","$templateCache","$q","schemaForm","schemaFormDecorators","sfSelect","sfPath","sfBuilder",function(t,r,n,i,o,a,l,s,u){return{scope:{schema:"=sfSchema",initialForm:"=sfForm",model:"=sfModel",options:"=sfOptions"},controller:["$scope",function(e){this.evalInParentScope=function(t,r){return e.$parent.$eval(t,r)};var t=this;e.lookup=function(e){return e&&(t.lookup=e),t.lookup}}],replace:!1,restrict:"A",transclude:!0,require:"?form",link:function(s,c,f,d,m){s.formCtrl=d;var p={};m(s,function(e){if(e.addClass("schema-form-ignore"),c.prepend(e),c[0].querySelectorAll){var t=c[0].querySelectorAll("[ng-model]");if(t)for(var r=0;r0?i.all(a.map(function(e){return r.get(e.templateUrl,{cache:n}).then(function(t){e.template=t.data})})).then(function(){g(e,t,l)}):g(e,t,l)},g=function(r,n,i){h&&(s.externalDestructionInProgress=!0,h.$destroy(),s.externalDestructionInProgress=!1),h=s.$new(),h.schemaForm={form:i,schema:r},c.children(":not(.schema-form-ignore)").remove();for(var d={},m=c[0].querySelectorAll("*[sf-insert-field]"),p=0;p0&&(y.schema=e,y.form=t,v(e,t))}),s.$on("schemaFormRedraw",function(){var e=s.schema,t=s.initialForm||["*"];e&&v(e,t)}),s.$on("$destroy",function(){s.externalDestructionInProgress=!0}),s.evalExpr=function(e,t){return s.$parent.$eval(e,t)}}}}]),e.module("schemaForm").directive("schemaValidate",["sfValidator","$parse","sfSelect",function(t,r,n){return{restrict:"A",scope:!1,priority:500,require:"ngModel",link:function(r,i,o,a){r.$emit("schemaFormPropagateNgModelController",a);var l=null,s=r.$eval(o.schemaValidate);s.copyValueTo&&a.$viewChangeListeners.push(function(){var t=s.copyValueTo;e.forEach(t,function(e){n(e,r.model,a.$modelValue)})});var u=function(e){if(!s)return e;if(r.options&&r.options.tv4Validation===!1)return e;var n=t.validate(s,e);return Object.keys(a.$error).filter(function(e){return 0===e.indexOf("tv4-")}).forEach(function(e){a.$setValidity(e,!0)}),n.valid?e:(a.$setValidity("tv4-"+n.error.code,!1),l=n.error,a.$validators?e:void 0)};"function"==typeof s.ngModel&&s.ngModel(a),["$parsers","$viewChangeListeners","$formatters"].forEach(function(e){s[e]&&a[e]&&s[e].forEach(function(t){a[e].push(t)})}),["$validators","$asyncValidators"].forEach(function(t){s[t]&&a[t]&&e.forEach(s[t],function(e,r){a[t][r]=e})}),a.$parsers.push(u),a.$validators&&(a.$validators.schemaForm=function(){return!Object.keys(a.$error).some(function(e){return"schemaForm"!==e})});var c=s.schema;r.validateField=function(){c&&-1!==c.type.indexOf("array")&&u(a.$modelValue),a.$setDirty?(a.$setDirty(),a.$setViewValue(a.$viewValue),a.$commitViewValue(),s.required&&a.$isEmpty(a.$modelValue)&&a.$setValidity("tv4-302",!1)):a.$setViewValue(a.$viewValue)},a.$formatters.push(function(e){return!a.$pristine||!r.firstDigest||r.options&&r.options.validateOnRender===!0?(u(a.$modelValue),e):e}),r.$on("schemaFormValidate",r.validateField),r.schemaError=function(){return l}}}}]),o}); \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 40db0478c..9c5093ff5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,7 +44,7 @@ Basic Usage ----------- First, expose your schema, form, and model to the $scope. -Don't forget to load the ``schemaForm` module. +Don't forget to load the `schemaForm` module. ```javascript angular.module('myModule', ['schemaForm']) @@ -187,13 +187,15 @@ attribute which should be placed along side `sf-schema`. `sf-options` takes an object with the following possible attributes. -| Attribute | | -|:--------------|:------------------------| -| supressPropertyTitles | by default schema form uses the property name in the schema as a title if none is specified, set this to true to disable that behavior | -| formDefaults | an object that will be used as a default for all form definitions | -| validationMessage | an object or a function that will be used as default validation message for all fields. See [Validation Messages](#validation-messages) for details. | -| setSchemaDefaults | boolean, set to false an no defaults from the schema will be set on the model. | -| destroyStrategy | the default strategy to use for cleaning the model when a form element is removed. see [destroyStrategy](#destroyStrategy) below | +| Attribute | Type | | +|:--------------|:------|:-------------------| +| supressPropertyTitles | boolean |by default schema form uses the property name in the schema as a title if none is specified, set this to true to disable that behavior | +| formDefaults | object | an object that will be used as a default for all form definitions | +| validationMessage | object or function | Object or a function that will be used as default validation message for all fields. See [Validation Messages](#validation-messages) for details. | +| setSchemaDefaults | boolean | Should schema defaults be set on model. | +| destroyStrategy | string | the default strategy to use for cleaning the model when a form element is removed. see [destroyStrategy](#destroyStrategy) below | +| pristine | Object `{errors ,success}` | Sets if errors and success states should be visible when form field are `$pristine`. Default is `{errors: true, success: true}` | +| validateOnRender | boolean | Should form be validated on initial render? Default `false` | *formDefaults* is mostly useful for setting global [ngModelOptions](#ngmodeloptions) i.e. changing the entire form to validate on blur. @@ -361,7 +363,7 @@ scope.$broadcast('schemaForm.error.name','usernameAlreadyTaken','The username is This will invalidate the field and therefore the form and show the error message where it normally pops up, under the field for instance. -There is a catch though, schema form can't now when this field is valid s you have to tell it by +There is a catch though, schema form can't know when this field is valid so you have to tell it by sending an event again, this time switch out the validation message for validity of the field, i.e. `true`. diff --git a/package.json b/package.json index c836bc7d8..85ebdd8e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-schema-form", - "version": "0.8.6", + "version": "0.8.12", "description": "Create complex forms from a JSON schema with angular.", "repository": "Textalk/angular-schema-form", "main": "dist/schema-form.min.js", diff --git a/src/directives/array.js b/src/directives/array.js index 90c0c9557..874e4da28 100644 --- a/src/directives/array.js +++ b/src/directives/array.js @@ -2,8 +2,8 @@ * Directive that handles the model arrays * DEPRECATED with the new builder use the sfNewArray instead. */ -angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sfValidator', 'sfPath', - function(sfSelect, schemaForm, sfValidator, sfPath) { +angular.module('schemaForm').directive('sfArray', ['$timeout', 'sfSelect', 'schemaForm', 'sfValidator', 'sfPath', + function($timeout, sfSelect, schemaForm, sfValidator, sfPath) { var setIndex = function(index) { return function(form) { @@ -136,8 +136,42 @@ angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sf return list; }; + function destroyArrayItem(item) { + item._destroy = true; + + Object.keys(item).filter(function(key) { + return !/^_(destroy|id)$/.test(key); + }).forEach(function(key) { + delete item[key]; + }); + + return item; + } + scope.deleteFromArray = function(index) { - list.splice(index, 1); + var hasAcm; + + if (angular.isObject(list[index])) { + // Check to see if this object has ACM, anywhere. + angular.forEach(list[index], function fn(value, key) { + if (hasAcm || (hasAcm = key === 'acm')) { + // If we've already found ACM, then no need to continue. + } else if (angular.isObject(value)) { + angular.forEach(value, fn); + } + }); + } + + if (list[index].$$new || !list[index]._id) { + list.splice(index, 1); + } else { + destroyArrayItem(list[index]); + } + + // Only emit the 'setCapco' event if the deleted object had CAPCO. + if (hasAcm) { + scope.$emit('setCapco'); + } // Trigger validation. scope.validateArray(); @@ -240,11 +274,27 @@ angular.module('schemaForm').directive('sfArray', ['sfSelect', 'schemaForm', 'sf scope.$on('schemaFormValidate', scope.validateArray); scope.hasSuccess = function() { - return ngModel.$valid && !ngModel.$pristine; + if (scope.options && scope.options.pristine && + scope.options.pristine.success === false) { + return ngModel.$valid && + !ngModel.$pristine && !ngModel.$isEmpty(ngModel.$modelValue); + } else { + return ngModel.$valid && + (!ngModel.$pristine || !ngModel.$isEmpty(ngModel.$modelValue)); + } }; scope.hasError = function() { - return ngModel.$invalid; + if (!scope.options || !scope.options.pristine || scope.options.pristine.errors !== false) { + // Show errors in pristine forms. The default. + // Note that "validateOnRender" option defaults to *not* validate initial form. + // so as a default there won't be any error anyway, but if the model is modified + // from the outside the error will show even if the field is pristine. + return ngModel.$invalid; + } else { + // Don't show errors in pristine forms. + return ngModel.$invalid && !ngModel.$pristine; + } }; scope.schemaError = function() { diff --git a/src/directives/changed.js b/src/directives/changed.js index 8fc948c3f..0d31a336c 100644 --- a/src/directives/changed.js +++ b/src/directives/changed.js @@ -17,7 +17,7 @@ angular.module('schemaForm').directive('sfChanged', function() { if (form && form.onChange) { ctrl.$viewChangeListeners.push(function() { if (angular.isFunction(form.onChange)) { - form.onChange(ctrl.$modelValue, form); + form.onChange(ctrl.$modelValue, form, scope); } else { scope.evalExpr(form.onChange, {'modelValue': ctrl.$modelValue, form: form}); } diff --git a/src/directives/decorators/bootstrap/array.html b/src/directives/decorators/bootstrap/array.html index f46838639..77570ab6f 100644 --- a/src/directives/decorators/bootstrap/array.html +++ b/src/directives/decorators/bootstrap/array.html @@ -3,8 +3,9 @@
  1. -
-
diff --git a/src/directives/decorators/bootstrap/checkbox.html b/src/directives/decorators/bootstrap/checkbox.html index d6ad64d4b..37d52760e 100644 --- a/src/directives/decorators/bootstrap/checkbox.html +++ b/src/directives/decorators/bootstrap/checkbox.html @@ -8,7 +8,7 @@ ng-model-options="form.ngModelOptions" schema-validate="form" class="{{form.fieldHtmlClass}}" - name="{{form.key.slice(-1)[0]}}"> + name="{{form.key.join('.')}}">
diff --git a/src/directives/decorators/bootstrap/checkboxes.html b/src/directives/decorators/bootstrap/checkboxes.html index 45b514135..549d0c7b5 100644 --- a/src/directives/decorators/bootstrap/checkboxes.html +++ b/src/directives/decorators/bootstrap/checkboxes.html @@ -9,7 +9,7 @@ sf-changed="form" class="{{form.fieldHtmlClass}}" ng-model="titleMapValues[$index]" - name="{{form.key.slice(-1)[0]}}"> + name="{{form.key.join('.')}}"> diff --git a/src/directives/decorators/bootstrap/default.html b/src/directives/decorators/bootstrap/default.html index b5685042b..d2b78be0f 100644 --- a/src/directives/decorators/bootstrap/default.html +++ b/src/directives/decorators/bootstrap/default.html @@ -1,23 +1,23 @@
- + - + name="{{form.key.join('.')}}" + aria-describedby="{{form.key.join('-') + 'Status'}}"> -
+ name="{{form.key.join('.')}}" + aria-describedby="{{form.key.join('-') + 'Status'}}"> {{ hasSuccess() ? '(success)' : '(error)' }}
diff --git a/src/directives/decorators/bootstrap/radios-inline.html b/src/directives/decorators/bootstrap/radios-inline.html index 6c3d07928..174a1587a 100644 --- a/src/directives/decorators/bootstrap/radios-inline.html +++ b/src/directives/decorators/bootstrap/radios-inline.html @@ -1,17 +1,19 @@
- +
-
diff --git a/src/directives/decorators/bootstrap/radios.html b/src/directives/decorators/bootstrap/radios.html index f3b73189b..49f84ee8b 100644 --- a/src/directives/decorators/bootstrap/radios.html +++ b/src/directives/decorators/bootstrap/radios.html @@ -1,5 +1,9 @@ -
- +
+