diff --git a/CHANGELOG b/CHANGELOG
index 66bceb2da..956a31f6a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,9 @@
+v0.8.13
+-------
+ * Bugfix copy the initial form default on form redraw @stanimoto
+ * Bugfix for revalidating the model after custom validation event is broadcast @cepauskas
+ * New event value. The form name is now passed in validate and error events @tomsowerby & @LeonardoGentile
+
v0.8.12
-------
* Bugfix for `condition` builder. It had a typo that broke it.
@@ -118,14 +124,14 @@ v0.7.11
v0.7.10
------
-* Accessability additions, thanks @stramel
-* Updates to the gulp tasks, thanks @stramel
-* Updated to bower, thanks @Dervisevic
-* Updates to documentation & README & Contributing.md, thanks @davidlgj @Dervisevic
-* Speed optimization, thanks @stramel
-* Call ngModel.$setDirty() when deleting from array, thanks @qstrahl
-* Don't override readonly if specified on items individually, thanks @qstrahl
-* Update to the examples, thanks @mike-marcacci
+ * Accessability additions, thanks @stramel
+ * Updates to the gulp tasks, thanks @stramel
+ * Updated to bower, thanks @Dervisevic
+ * Updates to documentation & README & Contributing.md, thanks @davidlgj @Dervisevic
+ * Speed optimization, thanks @stramel
+ * Call ngModel.$setDirty() when deleting from array, thanks @qstrahl
+ * Don't override readonly if specified on items individually, thanks @qstrahl
+ * Update to the examples, thanks @mike-marcacci
v0.7.9
------
@@ -264,7 +270,7 @@ We're celebrating actual useful functionality by bumping minor version, yay!
v0.0.4
------
-* Fieldsets now properly merge schema defaults.
-* Directives for "manual" decorator usage.
-* Basic support for buttons.
-* Basic support for custom validation error messages.
+ * Fieldsets now properly merge schema defaults.
+ * Directives for "manual" decorator usage.
+ * Basic support for buttons.
+ * Basic support for custom validation error messages.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c02c039a7..03e9192c0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,6 +11,10 @@ request heck of a lot easier for us.
Please avoid including anything from the `dist/` directory as that can make merging harder, and we
always generate these files when we make a new release.
+**We're currently in transitioning where a large part of the code, i.e. the bootstrap decorator, has been moved to it's own repo. It's here [github.com/Textalk/angular-schema-form-bootstrap](https://github.com/Textalk/angular-schema-form-bootstrap)**
+
+Feel free to submit issues on the main repo anyway though.
+
If its a new field type consider making it an add-on instead,
especially if it has external dependencies. See [extending Schema Form documentation.](docs/extending.md)
diff --git a/README.md b/README.md
index f8250e977..b74720c37 100644
--- a/README.md
+++ b/README.md
@@ -7,28 +7,81 @@ Angular Schema Form
[![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)
+Generate forms from JSON schemas using AngularJS!
-Are you there?
-----------------
-We're still here, in fact we're planning our future right now (2016-03) with a group of **eager contributors**! You may have noticed recent changes to the project GitHub location? Well that is so we can start a discussion in Gitter about how to move forward in a more **open** and **inclusive** way so the burden of development is not all down to David and the good folk at **Textalk**. So please get in touch with the **json-schema-form** org if you want to get involved, otherwise, **get ready for some long overdue progress** soon.
+Recent developments
+===================
+First, as there has been a rather intensive period of planning and change for this project, there have been important new developments for the project.
+Lets get into those first(the normal front page continues below):
+The json-schema-form standard
+-----------------------------
+A standard, json-schema-form, is being created.
+
+The concept of combining data, JSON Schema and a form definition, is something that isn't just usable in a JavaScript Angular web application, but in any framework, on any platform.
+Current ports are [angular-schema-form](https://github.com/json-schema-form/angular-schema-form) and [react-schema-form](https://github.com/networknt/react-schema-form), but delphi-schema-form and laravel-schema-form are planned as well.
+To make these ports easier to do, and for everything to work in harmony, a common ground has to be established, a [standard](https://github.com/json-schema-form/json-schema-form).
+
+Organisational
+--------------
+1. ASF has changed into using a more open governance model. This basically means that ASF is now governed by more people.
+2. An umbrella organisaton, json-schema-form, has been formed. As you can see, this repo is now a part of that Github organisation, not Textalk.
+
+Projects
+--------
+After a phase of planning, the following list of projects has been decided upon:
+https://github.com/json-schema-form/json-schema-form/wiki/Current-projects
+
+Release 1.0
+-----------
+The next major release of ASF will be 1.0.
+
+The goal for that version is to include the breaking changes that is needed for future developments, like *Of and local $refs:
+
+* Break out the non-framework specific parts of ASF into a vanilla ES6 module
+* Remove the built-in bootstrap decorator, and in doing that require that users wanting to use that load that separately. The reason obviously being the material decorator.
+
+The reason for the core break out is for all javascript-based ports of the json-schema-form concept to be able to share the same central code base.
+Work in that direction is being done in the [json-schema-form-core](https://github.com/json-schema-form/json-schema-form-core) repository.
+
+Schema builder UI
+-----------------
+There is now a UI for building schemas and forms being developed at [json-schema-builder repository](https://github.com/json-schema-form/json-schema-builder).
+
+Ralphael Owino (main author), has a [demo up already](http://ralphowino.github.io/schema-form-builder/#/builder).
+
+Schema and form repository
+--------------------------
+This is now a [repository with template schemas and forms](https://github.com/json-schema-form/json-schema-form-repository).
+So far all the [schema.org types](http://schema.org/docs/full.html) has been converted to JSON schema approximations, and also some has been further resolved and had (huge) forms generated. Schema.org is *big*.
+
+Documentation
+-------------
+The documentation is evolving, and it is happening mostly on the wiki:
+* [The ASF wiki](https://github.com/json-schema-form/angular-schema-form/wiki)
+* [The wiki of the json-schema-form organisation](https://github.com/json-schema-form/json-schema-form/wiki)
+
+New Gitter rooms
+----------------
+These are just started.
+* Discussions on the [general json-schema-form projects being carried out](https://gitter.im/json-schema-form/json-schema-form-projects)
+* Discussions on the [angular-specific ASF projects](https://gitter.im/json-schema-form/angular-schema-form-projects)
-Generate forms from JSON schemas using AngularJS!
The Blog / The Web Site / The Twitter / The Movie
-------------------------------------------------
+=================================================
[medium.com/@SchemaFormIO](https://medium.com/@SchemaFormIO) / [schemaform.io](http://schemaform.io) / [@SchemaFormIO](http://twitter.com/SchemaFormIO) / [Movie](https://www.youtube.com/watch?v=duBFMipRq2o)
If you use ASF in your project/company please let us know! We'd love to feature you on the site.
Demo Time!
-----------
+==========
[Try out the example page](http://schemaform.io/examples/bootstrap-example.html). Try editing the schema or form definition and see what comes out!
Hint: By pressing the 'Save to gist' button (top left), you can save your example into a shareable link.
What is it?
-----------
+===========
Schema Form is a set of AngularJS directives (and a couple of services). It can do two things to
make life easier:
diff --git a/bower.json b/bower.json
index 312c2f6a1..50dca085a 100644
--- a/bower.json
+++ b/bower.json
@@ -52,6 +52,7 @@
"angular-mocks": ">= 1.2",
"tx-tinymce": ">= 0.0.5",
"angular-ui-sortable": ">=0.12.11",
- "bootstrap-vertical-tabs": "~1.2.0"
+ "bootstrap-vertical-tabs": "~1.2.0",
+ "angular-schema-form-bootstrap": "~0.1.1"
}
}
diff --git a/dist/WHERE_IS_BOOTSTRAP_DECORATOR.md b/dist/WHERE_IS_BOOTSTRAP_DECORATOR.md
new file mode 100644
index 000000000..6c0da426c
--- /dev/null
+++ b/dist/WHERE_IS_BOOTSTRAP_DECORATOR.md
@@ -0,0 +1,15 @@
+Hello,
+
+the `bootstrap-decorator.js` and `bootstrap-decorator.min.js` files has moved
+to their project repo, https://github.com/Textalk/angular-schema-form-bootstrap
+
+You can install it via bower and npm as well.
+
+```sh
+bower install angular-schema-form-bootstrap
+```
+or
+
+```sh
+npm install angular-schema-form-bootstrap
+```
diff --git a/dist/bootstrap-decorator.js b/dist/bootstrap-decorator.js
deleted file mode 100644
index eabcc2c56..000000000
--- a/dist/bootstrap-decorator.js
+++ /dev/null
@@ -1,90 +0,0 @@
-(function(root, factory) {
- if (typeof define === 'function' && define.amd) {
- define(['schemaForm'], factory);
- } else if (typeof exports === 'object') {
- module.exports = factory(require('schemaForm'));
- } else {
- root.bootstrapDecorator = factory(root.schemaForm);
- }
-}(this, function(schemaForm) {
-angular.module("schemaForm").run(["$templateCache", function($templateCache) {$templateCache.put("directives/decorators/bootstrap/actions-trcl.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
diff --git a/dist/schema-form.js b/dist/schema-form.js
index 7b5b29bde..ff784fa7d 100644
--- a/dist/schema-form.js
+++ b/dist/schema-form.js
@@ -65,81 +65,6 @@ angular.module('schemaForm').provider('sfPath',
};
}]);
-/**
- * @ngdoc service
- * @name sfSelect
- * @kind function
- *
- */
-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;
- }
- //Support [] array syntax
- var parts = typeof projection === 'string' ? sfPath.parse(projection) : projection;
-
- if (typeof valueToSet !== 'undefined' && parts.length === 1) {
- //special case, just setting one variable
- obj[parts[0]] = valueToSet;
- return obj;
- }
-
- if (typeof valueToSet !== 'undefined' &&
- typeof obj[parts[0]] === 'undefined') {
- // We need to look ahead to check if array is appropriate
- obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {};
- }
-
- var value = obj[parts[0]];
- for (var i = 1; i < parts.length; i++) {
- // Special case: We allow JSON Form syntax for arrays using empty brackets
- // These will of course not work here so we exit if they are found.
- if (parts[i] === '') {
- return undefined;
- }
- if (typeof valueToSet !== 'undefined') {
- if (i === parts.length - 1) {
- //last step. Let's set the value
- value[parts[i]] = valueToSet;
- return valueToSet;
- } else {
- // Make sure to create new objects on the way if they are not there.
- // We need to look ahead to check if array is appropriate
- var tmp = value[parts[i]];
- if (typeof tmp === 'undefined' || tmp === null) {
- tmp = numRe.test(parts[i + 1]) ? [] : {};
- value[parts[i]] = tmp;
- }
- value = tmp;
- }
- } else if (value) {
- //Just get nex value.
- value = value[parts[i]];
- }
- }
- return value;
- };
-}]);
-
// FIXME: type template (using custom builder)
angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(sfPathProvider) {
@@ -682,6 +607,9 @@ angular.module('schemaForm').provider('schemaFormDecorators',
scope.ngModel.$setValidity(error, validity === true);
if (validity === true) {
+ // Re-trigger model validator, that model itself would be re-validated
+ scope.ngModel.$validate();
+
// Setting or removing a validity can change the field to believe its valid
// but its not. So lets trigger its validation as well.
scope.$broadcast('schemaFormValidate');
@@ -1553,6 +1481,81 @@ angular.module('schemaForm').provider('schemaForm',
}]);
+/**
+ * @ngdoc service
+ * @name sfSelect
+ * @kind function
+ *
+ */
+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;
+ }
+ //Support [] array syntax
+ var parts = typeof projection === 'string' ? sfPath.parse(projection) : projection;
+
+ if (typeof valueToSet !== 'undefined' && parts.length === 1) {
+ //special case, just setting one variable
+ obj[parts[0]] = valueToSet;
+ return obj;
+ }
+
+ if (typeof valueToSet !== 'undefined' &&
+ typeof obj[parts[0]] === 'undefined') {
+ // We need to look ahead to check if array is appropriate
+ obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {};
+ }
+
+ var value = obj[parts[0]];
+ for (var i = 1; i < parts.length; i++) {
+ // Special case: We allow JSON Form syntax for arrays using empty brackets
+ // These will of course not work here so we exit if they are found.
+ if (parts[i] === '') {
+ return undefined;
+ }
+ if (typeof valueToSet !== 'undefined') {
+ if (i === parts.length - 1) {
+ //last step. Let's set the value
+ value[parts[i]] = valueToSet;
+ return valueToSet;
+ } else {
+ // Make sure to create new objects on the way if they are not there.
+ // We need to look ahead to check if array is appropriate
+ var tmp = value[parts[i]];
+ if (typeof tmp === 'undefined' || tmp === null) {
+ tmp = numRe.test(parts[i + 1]) ? [] : {};
+ value[parts[i]] = tmp;
+ }
+ value = tmp;
+ }
+ } else if (value) {
+ //Just get nex value.
+ value = value[parts[i]];
+ }
+ }
+ return value;
+ };
+}]);
+
/* Common code for validating a value against its form and schema definition */
/* global tv4 */
angular.module('schemaForm').factory('sfValidator', [function() {
@@ -2109,6 +2112,9 @@ angular.module('schemaForm').directive('sfField',
scope.ngModel.$setValidity(error, validity === true);
if (validity === true) {
+ // Re-trigger model validator, that model itself would be re-validated
+ scope.ngModel.$validate();
+
// Setting or removing a validity can change the field to believe its valid
// but its not. So lets trigger its validation as well.
scope.$broadcast('schemaFormValidate');
@@ -2677,7 +2683,7 @@ angular.module('schemaForm')
// part of the form or schema is chnaged without it being a new instance.
scope.$on('schemaFormRedraw', function() {
var schema = scope.schema;
- var form = scope.initialForm || ['*'];
+ var form = scope.initialForm ? angular.copy(scope.initialForm) : ['*'];
if (schema) {
render(schema, form);
}
@@ -2817,7 +2823,13 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse
var schema = form.schema;
// A bit ugly but useful.
- scope.validateField = function() {
+ scope.validateField = function(formName) {
+
+ // If we have specified a form name, and this model is not within
+ // that form, then leave things be.
+ if(formName != undefined && ngModel.$$parentForm.$name !== formName) {
+ return;
+ }
// Special case: arrays
// TODO: Can this be generalized in a way that works consistently?
@@ -2864,7 +2876,9 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', '$parse
});
// Listen to an event so we can validate the input on request
- scope.$on('schemaFormValidate', scope.validateField);
+ scope.$on('schemaFormValidate', function(event, formName) {
+ scope.validateField(formName);
+ });
scope.schemaError = function() {
return error;
diff --git a/dist/schema-form.min.js b/dist/schema-form.min.js
index 5f7b06c45..ffa52766f 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;)m.appendChild(p.childNodes[0]);var v={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)}},y=c.builder||d.builder;"function"==typeof y?y(v):y.forEach(function(e){e(v)}),(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 v=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)]}}})),v()}})}}}])},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&&(m={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]&&m){var n=e.copy(m);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 a.options&&a.options.pristine&&a.options.pristine.success===!1?u.$valid&&!u.$pristine&&!u.$isEmpty(u.$modelValue):u.$valid&&(!u.$pristine||!u.$isEmpty(u.$modelValue))},a.hasError=function(){return a.options&&a.options.pristine&&a.options.pristine.errors===!1?u.$invalid&&!u.$pristine: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.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){i.min=0,i.modelArray=i.$eval(a.sfNewArray);var l=function(){i.modelArray=i.$eval(a.sfNewArray),(!(i.ngModel&&i.ngModel.$pristine&&i.firstDigest)||i.options&&i.options.validateOnRender===!0)&&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=function(){var e=i.modelArray;if(!e){var n=r.parse(a.sfNewArray);e=[],t(n,i,e),i.modelArray=e}return e},c=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=u();n.length>0;)n.pop();e.titleMap.forEach(function(e,r){t[r]&&n.push(e.value)}),i.validateField&&i.validateField()}})}c()}});i.appendToArray=function(){var r,o=u();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.splice(e,1),t};var f=function(e){return function(t){t.key&&(t.key[t.key.indexOf("")]=e)}},d={};i.copyWithIndex=function(t){var r=i.form;if(!d[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,f(t)),d[t]=a}}return d[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&&(v.schema=e,v.form=t,y(e,t))}),s.$on("schemaFormRedraw",function(){var e=s.schema,t=s.initialForm||["*"];e&&y(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
+!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").provider("sfBuilder",["sfPathProvider",function(t){var r=/[A-Z]/g,n=function(e,t){return t=t||"_",e.replace(r,function(e,r){return(r?t:"")+e.toLowerCase()})},i=0,o={sfField:function(e){e.fieldFrag.firstChild.setAttribute("sf-field",i),e.lookup["f"+i]=e.form,i++},ngModel:function(e){if(e.form.key){var r=e.form.key;e.state.keyRedaction&&(r=r.slice(e.state.keyRedaction));var n;if(e.state.modelValue)n=e.state.modelValue;else{var i=t.stringify(r).replace(/"/g,""");n=e.state.modelName||"model",i&&(n+=("["!==i[0]?".":"")+i)}for(var o=e.fieldFrag.querySelectorAll("[sf-field-model]"),a=0;a0&&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 v={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)}},y=c.builder||d.builder;"function"==typeof y?y(v):y.forEach(function(e){e(v)}),(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 v=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.ngModel.$validate(),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)]}}})),v()}})}}}])},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;l2&&t.test(o[1])?[]:{});for(var a=n[o[0]],l=1;l1&&(m={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]&&m){var n=e.copy(m);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 a.options&&a.options.pristine&&a.options.pristine.success===!1?u.$valid&&!u.$pristine&&!u.$isEmpty(u.$modelValue):u.$valid&&(!u.$pristine||!u.$isEmpty(u.$modelValue))},a.hasError=function(){return a.options&&a.options.pristine&&a.options.pristine.errors===!1?u.$invalid&&!u.$pristine: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.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.ngModel.$validate(),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){i.min=0,i.modelArray=i.$eval(a.sfNewArray);var l=function(){i.modelArray=i.$eval(a.sfNewArray),(!(i.ngModel&&i.ngModel.$pristine&&i.firstDigest)||i.options&&i.options.validateOnRender===!0)&&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=function(){var e=i.modelArray;if(!e){var n=r.parse(a.sfNewArray);e=[],t(n,i,e),i.modelArray=e}return e},c=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=u();n.length>0;)n.pop();e.titleMap.forEach(function(e,r){t[r]&&n.push(e.value)}),i.validateField&&i.validateField()}})}c()}});i.appendToArray=function(){var r,o=u();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.splice(e,1),t};var f=function(e){return function(t){t.key&&(t.key[t.key.indexOf("")]=e)}},d={};i.copyWithIndex=function(t){var r=i.form;if(!d[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,f(t)),d[t]=a}}return d[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&&(v.schema=e,v.form=t,y(e,t))}),s.$on("schemaFormRedraw",function(){var t=s.schema,r=s.initialForm?e.copy(s.initialForm):["*"];t&&y(t,r)}),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(e){(void 0==e||a.$$parentForm.$name===e)&&(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",function(e,t){r.validateField(t)}),r.schemaError=function(){return l}}}}]),o});
\ No newline at end of file
diff --git a/docs/extending.md b/docs/extending.md
index 873d156ee..4361488e1 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -1,120 +1,139 @@
Extending Schema Form
=====================
-Schema Form is designed to be easily extended and there are two basic ways to do it:
-1. Add a new type of field
-2. Add a new decorator
+Schema Form is designed to be easily extended. You can add your own custom fields or completely
+change the how the entire form is rendered.
-Adding a Field
---------------
-To add a new field to Schema Form you need to create a new form type and match that form type with
-a template snippet. To do this you use the `schemaFormDecoratorsProvider.addMapping()` function.
+A custom field is called an **add-on** and you can find community add-ons listed over at
+[schemaform.io](http://schemaform.io).
-Ex. from the [datepicker add-on](https://github.com/Textalk/angular-schema-form-datepicker/blob/master/src/bootstrap-datepicker.js#L18)
-```javascript
- schemaFormDecoratorsProvider.addMapping(
- 'bootstrapDecorator',
- 'datepicker',
- 'directives/decorators/bootstrap/datepicker/datepicker.html'
-);
-```
+To completely change how the entire field is rendered you need to create what we call a **decorator**.
+A decorator is actually a collection of add-ons that at least cover the basic field types
+that a schema can default to, but usually a lot more.
-The second argument is the name of your new form type, in this case `datepicker`, and the third is
-the template we bind to it (the first is the decorator, use `bootstrapDecorator` unless you know
-what you are doing).
+But before we get into the details of how you define a decorator or an add-on, let's take a look at how schema form builds forms.
-What this means is that a form definition like this:
-```javascript
-$scope.form = [
+How the form is built
+----------------------
+Schema Form uses the [sfBuilder](https://github.com/Textalk/angular-schema-form/blob/development/src/services/builder.js)
+service to recursively build the DOM elements of the form from a *canonical form definition*, that
+is our fancy word for an internal representation of a merge between the schema and the form.
+
+It's always an array of object and each object at least have the property `type`. If a `type` was
+not set in the form definition given to `sf-form` the schema is used to get a default.
+
+Example canonical form def.
+```js
+[
{
- key: "birthday",
- type: "datepicker"
+ type: 'text'
+ key: 'name'
+ },
+ {
+ type: 'fieldset',
+ item: [
+ {
+ type: 'textarea',
+ key: 'comment'
+ }
+ ]
}
-];
+]
```
-...will result in the `datepicker.html` template to be used to render that field in the form.
-But wait, where is all the code? Basically it's then up to the template to use directives to
-implement whatever it likes to do. It does have some help though, lets look at template example and
-go through the basics.
+#### The actual building
+So to build a form from a canonical form definition as in the example above the builder service loops
+over and for each type asks the decorator for a template, it adds it to a document fragment.
-This is sort of the template for the datepicker:
-```html
-
-
-
-
+After adding the template it also asks the decorator if that type has a *builder*
+function (actually it's usually a list of functions). If so it calls it with the DOM of its template,
+the form definition for that field and other useful stuff. This way the builder can modify and
+prepare the template depending on options in that fields form object.
-
-
-```
+Nested fields, as with the fieldset above in the example above, builds it's children with such a
+*builder* function.
+
+This all happens in one large swoop and the finished document fragment is popped inside the form
+and then `$compile` is used to kick start it's directives.
+
+
+Creating an add-on
+------------------
+
+So to create an add-on you need two things, a template and some *builder functions*. Fortunately
+schema form got you covered with a couple of standard builders so most of the time you will only
+need a template.
+
+To register your template to be used when the form definition has a specific type you use the
+`schemaFormDecoratorsProvider.defineAddOn`.
+
+Ex.
+```js
+angular.module('myAddOnModule', ['schemaForm']).config(function(schemaFormDecoratorsProvider, sfBuilderProvider) {
-### What's on the scope?
-Each form field will be rendered inside a decorator directive, created by the
-`schemaFormDecorators` factory service, *do*
-[check the source](https://github.com/Textalk/angular-schema-form/blob/master/src/services/decorators.js#L33).
+ schemaFormDecoratorsProvider.defineAddOn(
+ 'bootstrapDecorator', // Name of the decorator you want to add to.
+ 'awesome', // Form type that should render this add-on
+ 'templates/my/addon.html', // Template name in $templateCache
+ sfBuilderProvider.stdBuilders // List of builder functions to apply.
+ );
-This means you have several helper functions and values on scope, most important of this `form`. The
-`form` variable contains the merged form definition for that field, i.e. your supplied form object +
-the defaults from the schema (it also has its part of the schema under *form.schema*).
-This is how you define and use new form field options, whatever is set on the form object is
-available here for you to act on.
+});
+```
-| Name | What it does |
-|----------|----------------|
-| form | Form definition object |
-| showTitle() | Shorthand for `form && form.notitle !== true && form.title` |
-| ngModel | The ngModel controller, this will be on scope if you use either the directive `schema-validate` or `sf-array` |
-| evalInScope(expr, locals) | Eval supplied expression, ie scope.$eval |
-| evalExpr(expr, locals) | Eval an expression in the parent scope of the main `sf-schema` directive. |
-| interp(expr, locals) | Interpolate an expression which may or may not contain expression `{{ }}` sequences |
-| buttonClick($event, form) | Use this with ng-click to execute form.onClick |
-| hasSuccess() | Shorthand for `ngModel.$valid && (!ngModel.$pristine || !ngModel.$isEmpty(ngModel.$modelValue))` |
-| hasError() | Shorthand for `ngModel.$invalid && !ngModel.$pristine` |
+The standards builders are `[sfField, ngModel, ngModelOptions, condition]`, see usage details below.
-#### Deprecation warning
-There is still a `errorMessage` function on scope but it's been deprecated. Please use the
-`sf-message` directive instead.
+#### The Template
+So whats in a template? You usually need a couple of things:
+ 1. Usually a top level element that surrounds your template is a good idea. The `sfField` builder
+ slaps on a `sfField` directive that exposes the current form object on scope as `form`.
+ 1. A `sf-field-model` somewhere so that the `ngModel` builder can add a proper `ngModel` to bind
+ your model value to.
+ 1. A `schema-validate="form"` directive on the same element to enable schema validation.
+ 1. A `` to display description or error messages.
-### The magic $$value$$
-Schema Form wants to play nice with the built in Angular directives for form. Especially `ng-model`
-which we want to handle the two way binding against our model value. Also by using `ng-model` we
-get all the nice validation states from the `ngModelController` and `FormController` that we all
-know and love.
+Basic template example:
+```html
+
+
+
+
+
+```
-To get that working properly we had to resort to a bit of trickery, right before we let Angular
-compile the field template we do a simple string replacement of `$$value$$` and replace that
-with the path to the current form field on the model, i.e. `form.key`.
+**BIG FAT CAVEAT**
+Ok, so currently there is something really ugly here. The bootstrap (and material) decorator uses
+a build step (gulp-angular-templatecache) to "compile" the template into a javascript file that
+basically chucks the template into `$templateCache`. Currently schema form does *not* support
+loading the templates any other way. They need to be in `$templateCache` when rendering.
-So `ng-model="$$value$$"` becomes something like `ng-model="model['person']['address']['street']"`,
-you can see this if you inspect the final form in the browser.
+This is really ugly and will be fixed. But you have been warned!
-So basically always have a `ng-model="$$value$$"` (Pro tip: ng-model is fine on any element, put
- it on the same div as your custom directive and require the ngModelController for full control).
-### schema-validate directive
-`schema-validate` is a directive that you should put on the same element as your `ng-model`. It is
-responsible for validating the value against the schema using [tv4js](https://github.com/geraintluff/tv4)
-It takes the form definition as an argument.
+Defining a decorator
+--------------------
+Defining a decorator is basically the same as defining a lot of add-ons. As with add-ons you use
+the `schemaFormDecoratorsProvider` again. This time its
+`schemaFormDecoratorsProvider.defineDecorator`.
+Ex.
+```js
+angular.module('myDecoratorModule', ['schemaForm']).config(function(schemaFormDecoratorsProvider, sfBuilderProvider) {
-### sf-message directive
-Error messages are nice, and the best way to get them is via the `sf-message` directive. It usually
-takes `form.description` as an argument so it can show that until an error occurs.
+ schemaFormDecoratorsProvider.defineDecorator('awesomeDecorator', {
+ textarea: {template: base + 'textarea.html', builder: sfBuilderProvider.stdBuilders},
+ button: {template: base + 'submit.html', builder: sfBuilderProvider.stdBuilders},
+ text: {template: base + 'text.html', builder: sfBuilderProvider.stdBuilders},
+ // The default is special, if the builder can't find a match it uses the default template.
+ 'default': {template: base + 'default.html', builder: sfBuilderProvider.stdBuilders}
+ }, []);
+});
+```
### Setting up schema defaults
-So you got this shiny new add-on that adds a fancy field type, but feel a bit bummed out that you
+So you got this shiny new add-on or decorator that adds a fancy field type, but feel a bit bummed out that you
need to specify it in the form definition all the time? Fear not because you can also add a "rule"
to map certain types and conditions in the schema to default to your type.
@@ -166,49 +185,136 @@ there can be a delay of a day or so.
So [make a bower package](http://bower.io/docs/creating-packages/), add the keyword
`angular-schema-form-add-on` and [register it](http://bower.io/docs/creating-packages/#register)!
-Decorators
-----------
-Decorators are a second way to extend Schema Form, the thought being that you should easily be able
-to change *every* field. Maybe you like it old school and want to use bootstrap 2. Or maybe you like
-to generate a table with the data instead? Right now there are no other decorators than bootstrap 3.
-Basically a *decorator* sets up all the mappings between form types and their respective templates
-using the `schemaFormDecoratorsProvider.createDecorator()` function.
-```javascript
-var base = 'directives/decorators/bootstrap/';
-
-schemaFormDecoratorsProvider.createDecorator('bootstrapDecorator', {
- textarea: base + 'textarea.html',
- fieldset: base + 'fieldset.html',
- array: base + 'array.html',
- tabarray: base + 'tabarray.html',
- tabs: base + 'tabs.html',
- section: base + 'section.html',
- conditional: base + 'section.html',
- actions: base + 'actions.html',
- select: base + 'select.html',
- checkbox: base + 'checkbox.html',
- checkboxes: base + 'checkboxes.html',
- number: base + 'default.html',
- password: base + 'default.html',
- submit: base + 'submit.html',
- button: base + 'submit.html',
- radios: base + 'radios.html',
- 'radios-inline': base + 'radios-inline.html',
- radiobuttons: base + 'radio-buttons.html',
- help: base + 'help.html',
- 'default': base + 'default.html'
-}, [
- function(form) {
- if (form.readonly && form.key && form.type !== 'fieldset') {
- return base + 'readonly.html';
- }
- }
-]);
+
+The builders
+------------
+A collection of useful builders that cover most cases are in the `sfBuilder` service and is accessable
+both from the provider and the service on the property `builders`. There is also a list of "standard"
+builders, when in doubt use those.
+
+```js
+angular.module('myMod').config(function(sfBuildersProvider) {
+
+ // Standard builders
+ sfBuildersProvider.stdBuilders;
+
+ // All builders
+ sfBuildersProvider.builders.sfField;
+ sfBuildersProvider.builders.condition;
+ sfBuildersProvider.builders.ngModel;
+ sfBuildersProvider.builders.ngModelOptions;
+ sfBuildersProvider.builders.simpleTransclusion;
+ sfBuildersProvider.builders.transclusion;
+ sfBuildersProvider.builders.array;
+
+});
+```
+
+Currently the standard builders are:
+```js
+var stdBuilders = [
+ builders.sfField,
+ builders.ngModel,
+ builders.ngModelOptions,
+ builders.condition
+];
+```
+
+
+### builders.sfField
+The `sfField` builder adds the `sf-field="..."` directive to *the first child element* in the template,
+giving it a correct value. The value is an id number that identifies that specific form object.
+
+The `sf-field` directive exports the form definition object as `form` on scope and as a lot of useful functions.
+
+As a rule of thumb you always want this builder.
+
+### builders.condition
+The `condition` builder checks the form definition for the option `condition`. If it's present it adds a
+`ng-if` to all top level elements in the template.
+
+You usually want this as well.
+
+### builder.ngModel
+The `ngModel` builder is maybe the most important builder. It makes sure you get a proper binding to
+your model value.
+
+The `ngModel` builder queries the DOM of the template for all elements that have the attribute `sf-field-model`. Your template may have several of them. `sf-field-model` is *not* a directive,
+but depending on it's value the `ngModel` builder will take three different actions.
+
+
+#### sf-field-model
+Just `sf-field-model` or `sf-field-model=""` tells the builder to add a `ng-model` directive to this element.
+This is a common use case.
+
+Ex:
+DOM before `ngModel` builder:
+```html
+
+
+
+```
+DOM after `ngModel` builder:
+```html
+
+
+
+```
+
+#### sf-field-model=""
+Given a value the `ngModel` builder will treat that value as a *attribute name* and instead of slapping
+on a `ng-model` set the specified attributes value. It sets it to the same value as the `ng-model` would have gotten.
+
+Ex:
+DOM before `ngModel` builder:
+```html
+
+
+
```
-`schemaFormDecoratorsProvider.createDecorator(name, mapping, rules)` takes a name argument, a mapping object
-(type -> template) and an optional list of rule functions.
+DOM after `ngModel` builder:
+```html
+
+
+
+```
+
+#### sf-field-model="replaceAll"
+With the special value *replaceAll* the `ngModel` builder will instead loop over every attribute on the
+element and do a string replacement of `"$$value$$"` with the proper model value.
+
+Ex:
+DOM before `ngModel` builder:
+```html
+
+
+
+```
+DOM after `ngModel` builder:
+```html
+
+
+
+```
+
+### builders.ngModelOptions
+If the form definition has a `ngModelOptions` option specified this builder will slap on a `ng-model-options`
+attribute to *the first child element* in the template.
+
+
+### builder.simpleTransclusion
+The `simpleTransclusion` builder will recurse and build form items, useful for fieldsets etc. This builder
+is simple because it only appends children to the first child element and only checks `form.items`.
+
-When the decorator is trying to match a form type against a tempate it first executes all the rules
-in order. If one returns that is used as template, otherwise it checks the mappings.
+Useful directives
+-----------------
+TODO: more in depth about schema-validate, sf-messages and sf-field
diff --git a/docs/index.md b/docs/index.md
index 9c5093ff5..be05f831e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,3 +1,14 @@
+
+IMPORTANT
+=========
+
+**Angular Schema Form is undergoing a refactoring and the "bootstrap decorator", i.e. the part with
+all the HTML has been moved to [github.com/Textalk/angular-schema-form-bootstrap](https://github.com/Textalk/angular-schema-form-bootstrap).**
+
+The documentation below, especially form options is therefore somewhat bootstrap decorator
+specific. The docs is undergoing updating.
+
+
Documentation
=============
@@ -43,8 +54,13 @@ Documentation
Basic Usage
-----------
-First, expose your schema, form, and model to the $scope.
-Don't forget to load the `schemaForm` module.
+After installing, load the `schemaForm` module in your module definition.
+
+Then, in your controller, expose your [schema](http://json-schema.org/),
+form, and [model](https://docs.angularjs.org/guide/databinding) to the $scope.
+Your schema defines your data structure, the form definition
+draws on this definition to define the user interface, and the
+model binds the user input to the controller.
```javascript
angular.module('myModule', ['schemaForm'])
@@ -72,7 +88,8 @@ angular.module('myModule', ['schemaForm'])
}
```
-Then load them into Schema Form using the `sfSchema`, `sfForm`, and `sfModel` directives.
+Then, in your template, load them into Schema Form using the
+`sfSchema`, `sfForm`, and `sfModel` directives.
```html
@@ -941,7 +958,7 @@ element to the select.
{
type: "actions",
items: [
- { type: 'submit', title: 'Ok' }
+ { type: 'submit', title: 'Ok' },
{ type: 'button', title: 'Cancel', onClick: "cancel()" }
]
}
@@ -953,7 +970,7 @@ We can change this with ```style``` attribute:
{
type: "actions",
items: [
- { type: 'submit', style: 'btn-success', title: 'Ok' }
+ { type: 'submit', style: 'btn-success', title: 'Ok' },
{ type: 'button', style: 'btn-info', title: 'Cancel', onClick: "cancel()" }
]
}
@@ -967,7 +984,7 @@ the ```sf-schema``` directive.
```javascript
[
- { type: 'submit', title: 'Ok', onClick: function(){ ... } }
+ { type: 'submit', title: 'Ok', onClick: function(){ ... } },
{ type: 'button', title: 'Cancel', onClick: "cancel()" }
[
```
@@ -976,7 +993,7 @@ The submit and other buttons have btn-default as default.
We can change this with ```style``` attribute:
```javascript
[
- { type: 'submit', style: 'btn-warning', title: 'Ok', onClick: function(){ ... } }
+ { type: 'submit', style: 'btn-warning', title: 'Ok', onClick: function(){ ... } },
{ type: 'button', style: 'btn-danger', title: 'Cancel', onClick: "cancel()" }
[
```
@@ -1021,7 +1038,7 @@ function FormCtrl($scope) {
type: "radiobuttons",
titleMap: [
{ value: "one", name: "One" },
- { value, "two", name: "More..." }
+ { value: "two", name: "More..." }
]
}
];
@@ -1222,8 +1239,9 @@ could be changed using attribute `add`, see example below.
If you like to have drag and drop reordering of arrays you also need
[ui-sortable](https://github.com/angular-ui/ui-sortable) and its dependencies
[jQueryUI](http://jqueryui.com/), see *ui-sortable* documentation for details of
-what parts of jQueryUI that is needed. You can safely ignore these if you don't
-need the reordering.
+what parts of jQueryUI that is needed. You can also pass options to the *ui-sortable* directive
+by including a `sortOptions` key on the form object. Check the *ui-sortable* documentation
+for a complete list of available options. You can safely ignore these if you don't need the reordering.
In the form definition you can refer to properties of an array item by the empty
bracket notation. In the `key` simply end the name of the array with `[]`
diff --git a/examples/bootstrap-example.html b/examples/bootstrap-example.html
deleted file mode 100644
index 89e57fdf3..000000000
--- a/examples/bootstrap-example.html
+++ /dev/null
@@ -1,389 +0,0 @@
-
-
-
-
- Bootstrap Schema Form example
-
-
-
-
-
-
-
-
-
-
-
-
-
Schema Form does not support oneOf (yet), but you can do a workaround and simulate certain scenarios with 'condition' and 'required' (and/or 'readonly') in the form.