diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 000000000..aaf0dab9c --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,6 @@ +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. diff --git a/README.md b/README.md index c3c2c579a..c67e0997a 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Schema Form currently supports the following form field types: |:--------------|:------------------------| | fieldset | a fieldset with legend | | section | just a div | -| actions | horizontal button list, can only submit buttons as items | +| actions | horizontal button list, can only submit and buttons as items | | text | input with type text | | textarea | a textarea | | number | input type number | @@ -69,6 +69,7 @@ Schema Form currently supports the following form field types: | checkboxes | list of checkboxes | | select | a select (single value)| | submit | a submit button | +| button | a button | @@ -171,6 +172,29 @@ General options most field types can handle: title: "Street", //Title of field, taken from schema if available notitle: false, //Set to true to hide title description: "Street name", //A description, taken from schema if available + validationMessage: "Oh noes, please write a proper address" //A custom validation error message +} +``` + +Validation Messages +------------------- +Per default all error messages but "Required" comes from the schema validator +[tv4](https://github.com/geraintluff/tv4), this might or might not work for you. +If you supply a ´´´validationMessage´´´ proṕerty in the form definition, and if its value is a +string that will be used instead on any validation error. + +If you need more fine grained control you can supply an object instead with keys matching the error +codes of [tv4](https://github.com/geraintluff/tv4). See ```tv4.errorCodes``` + +Ex. +```javascript +{ + key: "address.street", + validationMessage: { + tv4.errorCodes.STRING_LENGTH_SHORT: "Address is too short, man.", + "default": "Just write a proper address, will you?", //Special catch all error message + "required": "I needz an address plz" //Used for required if specified + } } ``` @@ -209,7 +233,16 @@ and the value is the title of the option. type: "actions", items: [ { type: 'submit', title: 'Ok' } + { type: 'button', title: 'Cancel', onClick: "cancel()" } ] } ``` +*button* can have a ```onClick``` attribute that either, as in JSON Form, is a function *or* a +string with an angular expression, as with ng-click. +[ + { type: 'button', title: 'Ok', onClick: function(){ ... } } + { type: 'button', title: 'Cancel', onClick: "cancel()" } +[ +``` + diff --git a/bower.json b/bower.json index f01b764e2..363afd744 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-schema-form", - "version": "0.0.3", + "version": "0.0.4", "authors": [ "Textalk", "David Jensen " diff --git a/dist/bootstrap-decorator.min.js b/dist/bootstrap-decorator.min.js index 45acec573..3ddc52176 100644 --- a/dist/bootstrap-decorator.min.js +++ b/dist/bootstrap-decorator.min.js @@ -1 +1 @@ -!function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/actions.html",'
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/checkbox.html",'
{{form.description}}
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/checkboxes.html",'
{{form.description}}
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/default.html",'
{{form.description}} {{schemaError}}
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/fieldset.html",'
{{ form.title }}
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/readonly.html",'
{{form.description}}
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/section.html",'
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/select.html",'
{{form.description}} {{schemaError}}
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/submit.html",'
')}])}(),function(e){try{e=angular.module("schemaForm")}catch(r){e=angular.module("schemaForm",[])}e.run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/textarea.html",'
{{form.description}} {{schemaError}} {{ngModel.$error}}
')}])}(),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){e.create("bootstrapDecorator",{textarea:"directives/decorators/bootstrap/textarea.html",fieldset:"directives/decorators/bootstrap/fieldset.html",section:"directives/decorators/bootstrap/section.html",actions:"directives/decorators/bootstrap/actions.html",select:"directives/decorators/bootstrap/select.html",checkbox:"directives/decorators/bootstrap/checkbox.html",checkboxes:"directives/decorators/bootstrap/checkboxes.html",number:"directives/decorators/bootstrap/default.html",submit:"directives/decorators/bootstrap/submit.html","default":"directives/decorators/bootstrap/default.html"},[function(e){return e.readonly&&e.key&&"fieldset"!==e.type?"directives/decorators/bootstrap/readonly.html":void 0}])}]); \ No newline at end of file +angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/actions-trcl.html",'
'),e.put("directives/decorators/bootstrap/actions.html",'
'),e.put("directives/decorators/bootstrap/checkbox.html",'
{{form.description}}
'),e.put("directives/decorators/bootstrap/checkboxes.html",'
{{form.description}}
'),e.put("directives/decorators/bootstrap/default.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
'),e.put("directives/decorators/bootstrap/fieldset-trcl.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/fieldset.html",'
{{ form.title }}
'),e.put("directives/decorators/bootstrap/readonly.html",'
{{form.description}}
'),e.put("directives/decorators/bootstrap/section.html",'
'),e.put("directives/decorators/bootstrap/select.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
'),e.put("directives/decorators/bootstrap/submit.html",'
'),e.put("directives/decorators/bootstrap/textarea.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
')}]),angular.module("schemaForm").config(["schemaFormDecoratorsProvider",function(e){var t="directives/decorators/bootstrap/";e.createDecorator("bootstrapDecorator",{textarea:t+"textarea.html",fieldset:t+"fieldset.html",section:t+"section.html",actions:t+"actions.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","default":t+"default.html"},[function(e){return e.readonly&&e.key&&"fieldset"!==e.type?t+"readonly.html":void 0}]),e.createDirectives({textarea:t+"textarea.html",select:t+"select.html",checkbox:t+"checkbox.html",checkboxes:t+"checkboxes.html",number:t+"default.html",submit:t+"submit.html",button:t+"submit.html",text:t+"default.html",date:t+"default.html",password:t+"default.html",input:t+"default.html"})}]).directive("sfFieldset",function(){return{transclude:!0,scope:!0,templateUrl:"directives/decorators/bootstrap/fieldset-trcl.html",link:function(e,t,r){e.title=e.$eval(r.title)}}}); \ No newline at end of file diff --git a/dist/schema-form.min.js b/dist/schema-form.min.js index 835d0daa8..192062728 100644 --- a/dist/schema-form.min.js +++ b/dist/schema-form.min.js @@ -1 +1 @@ -angular.module("schemaForm",[]),angular.module("schemaForm").provider("schemaFormDecorators",["$compileProvider",function(e){var r="",t={},n=function(e,n){"sfDecorator"===e&&(e=r);for(var a=t[e],i=a.rules,o=0;o',link:function(e,t,n){var a={items:"c",titleMap:"c",schema:"c"},i={type:r},o=!0;angular.forEach(n,function(r,t){if("$"!==t[0]&&0!==t.indexOf("ng")&&"sfField"!==t){var u=function(r){angular.isDefined(r)&&r!==i[t]&&(i[t]=r,o&&i.type&&(i.key||angular.isUndefined(n.key))&&(e.form=i,o=!1))};"model"===t?e.$watch(r,function(r){r&&e.model!==r&&(e.model=r)}):"c"===a[t]?e.$watchCollection(r,u):n.$observe(t,u)}})}}})};this.createDecorator=function(e,n,i){t[e]={mappings:n||{},rules:i||[]},t[r]||(r=e),a(e)},this.createDirective=i,this.createDirectives=function(e){angular.forEach(e,function(e,r){i(r,e)})},this.directive=function(e){return e=e||r,t[e]},this.$get=function(){return{directive:function(e){return t[e]},defaultDecorator:r}},a("sfDecorator")}]),angular.module("schemaForm").factory("schemaForm",[function(){var e={};e.merge=function(r,t,n){t=t||["*"];var a=e.defaults(r,n),i=t.indexOf("*");if(-1!==i)return t=t.slice(0,i).concat(a.form).concat(t.slice(i+1));var o=a.lookup;return t.map(function(t){return"string"==typeof t&&(t={key:t}),t.items&&(t.items=e.merge(r,t.items,n)),t.key&&o[t.key]?angular.extend(o[t.key],t):t})};var r=function(e,r){var t={};return e.title&&(t.title=e.title),e.description&&(t.description=e.description),(r.required===!0||e.required===!0)&&(t.required=!0),e.default&&(t.default=e.default),e.maxLength&&(t.maxlength=e.maxLength),e.minLength&&(t.minlength=e.maxLength),(e.readOnly||e.readonly)&&(t.readonly=e.readOnly||e.readonly),e.minimum&&(t.minimum=e.minimum+(e.exclusiveMinimum?1:0)),e.maximum&&(t.maximum=e.maximum-(e.exclusiveMaximum?1:0)),e.validationMessage&&(t.validationMessage=e.validationMessage),e.enumNames&&(t.titleMap=e.enumNames),t.schema=e,t},t=function(e,t,n){if("string"===t.type&&!t.enum){var a=r(t,n);return a.key=n.path,a.type="text",n.lookup[n.path]=a,a}},n=function(e,t,n){if("number"===t.type){var a=r(t,n);return a.key=n.path,a.type="number",n.lookup[n.path]=a,a}},a=function(e,t,n){if("integer"===t.type){var a=r(t,n);return a.key=n.path,a.type="number",n.lookup[n.path]=a,a}},i=function(e,t,n){if("boolean"===t.type){var a=r(t,n);return a.key=n.path,a.type="checkbox",n.lookup[n.path]=a,a}},o=function(e,t,n){if("string"===t.type&&t.enum){var a=r(t,n);return a.key=n.path,a.type="select",a.titleMap||(a.titleMap={},t.enum.forEach(function(e){a.titleMap[e]=e})),n.lookup[n.path]=a,a}},u=function(e,t,n){if("array"===t.type&&t.items&&t.items.enum){var a=r(t,n);return a.key=n.path,a.type="checkboxes",a.titleMap||(a.titleMap={},t.items.enum.forEach(function(e){a.titleMap[e]=e})),n.lookup[n.path]=a,a}},c=function(e,t,n){if("object"===t.type){var a=r(t,n);return a.type="fieldset",a.items=[],n.lookup[n.path]=a,angular.forEach(t.properties,function(e,r){var i=n.path+"."+r;if(n.ignore[i]!==!0){var o=t.required&&-1!==t.required.indexOf(r),u=f(r,e,{path:i,required:o||!1,lookup:n.lookup,ignore:n.ignore});u&&a.items.push(u)}}),a}},l=[t,o,c,n,a,i,u],f=function(e,r,t){for(var n,a=0;a defaultForm, -#2 defaultForm+form => finalForm -#3 loopa finalForm och skapa html, dvs all info för validering i formuläret. - -Kruxet är att formulär definitionen i json form *inte* innehåller tillräckligt med valideringar -(den innehåller några) utan jag måste lägga till form attribut för dem. (json form tar dem från -schemat) - -tex så om schema innehåller "maxLength" attribut så sätts "maxlength" på input fältet, men om form -definitionen innehåller "maxLength" (eller "maxlength" för den delen) så händer inget. - -I min algoritm ovan där form definitionen måste innehålla all info för generering av html och -de attribut som styr upp validering så måste jag helt enkelt hitta på eget. - -Frågan är hur ska detta egna se ut? - - -#1 Egna form attribut -När det inte finns ngt form attribut i json forms defintion så skapar jag ett eget, t.ex. så skulle -maxLength bara kopieras till form objektet. -+ minsta motståndets lag -- kluddig lösning, motverkar lite att vara json form kompatibel -- kan allt stödjas? - - -#2 Eget schema directive -Istället skapa ett eget directive som antingen validerar själv eller lägger till de attribut som -behövs för att validera. Schemat fås genom require och ng-model värdet pekar ut vart i schemat. -Kan möjligtvis implementeras med tv4. -+ snygg lösning -- krångligt directive, måste ta hänsyn till om tex maximum, required osv redan satts via form -definitionen. - - - - diff --git a/src/bootstrap-example.html b/src/bootstrap-example.html index e865bb02f..11df7a66f 100644 --- a/src/bootstrap-example.html +++ b/src/bootstrap-example.html @@ -5,12 +5,19 @@ @@ -22,16 +29,16 @@

Schema Form Example

The Generated Form

+

Model

+
{{pretty()}}
-

Schema

-

Form

-

Model

-
{{pretty()}}
+ ng-class="{red: !itParsesForm}" ng-model="formJson" class="form-control form">
+

Schema

+
@@ -120,7 +127,7 @@

Model

type: 'actions', items: [ { type: 'submit', title: 'Do It!'}, - { type: 'button', title: 'Noooooooooooo'} + { type: 'button', title: 'Noooooooooooo', onClick: 'sayNo()'} ] } ]; @@ -160,6 +167,9 @@

Model

}; + $scope.sayNo = function() { + alert('Noooooooo') + } } diff --git a/src/directives/decorators/bootstrap/actions-trcl.html b/src/directives/decorators/bootstrap/actions-trcl.html new file mode 100644 index 000000000..092439c40 --- /dev/null +++ b/src/directives/decorators/bootstrap/actions-trcl.html @@ -0,0 +1 @@ +
diff --git a/src/directives/decorators/bootstrap/actions.html b/src/directives/decorators/bootstrap/actions.html index bb2be41da..2ed3bce84 100644 --- a/src/directives/decorators/bootstrap/actions.html +++ b/src/directives/decorators/bootstrap/actions.html @@ -4,5 +4,8 @@ class="btn btn-primary" value="{{item.title}}" ng-if="item.type === 'submit'"> - + diff --git a/src/directives/decorators/bootstrap/bootstrap-decorator.js b/src/directives/decorators/bootstrap/bootstrap-decorator.js index a3455b0d9..90146e5cd 100644 --- a/src/directives/decorators/bootstrap/bootstrap-decorator.js +++ b/src/directives/decorators/bootstrap/bootstrap-decorator.js @@ -1,25 +1,51 @@ angular.module('schemaForm').config(['schemaFormDecoratorsProvider',function(decoratorsProvider){ + var base = 'directives/decorators/bootstrap/'; - decoratorsProvider.create('bootstrapDecorator',{ - textarea: 'directives/decorators/bootstrap/textarea.html', - fieldset: 'directives/decorators/bootstrap/fieldset.html', - section: 'directives/decorators/bootstrap/section.html', - actions: 'directives/decorators/bootstrap/actions.html', - select: 'directives/decorators/bootstrap/select.html', - checkbox: 'directives/decorators/bootstrap/checkbox.html', - checkboxes: 'directives/decorators/bootstrap/checkboxes.html', - number: 'directives/decorators/bootstrap/default.html', - submit: 'directives/decorators/bootstrap/submit.html', - 'default': 'directives/decorators/bootstrap/default.html' + decoratorsProvider.createDecorator('bootstrapDecorator',{ + textarea: base+'textarea.html', + fieldset: base+'fieldset.html', + section: base+'section.html', + actions: base+'actions.html', + select: base+'select.html', + checkbox: base+'checkbox.html', + checkboxes: base+'checkboxes.html', + number: base+'default.html', + submit: base+'submit.html', + button: base+'submit.html', + 'default': base+'default.html' },[ function(form){ if (form.readonly && form.key && form.type !== 'fieldset') { - return 'directives/decorators/bootstrap/readonly.html'; + return base+'readonly.html'; } } ]); -}]); + //manual use directives + decoratorsProvider.createDirectives({ + textarea: base+'textarea.html', + select: base+'select.html', + checkbox: base+'checkbox.html', + checkboxes: base+'checkboxes.html', + number: base+'default.html', + submit: base+'submit.html', + button: base+'submit.html', + text: base+'default.html', + date: base+'default.html', + password: base+'default.html', + input: base+'default.html' + }); + +}]).directive('sfFieldset',function(){ + return { + transclude: true, + scope: true, + templateUrl: 'directives/decorators/bootstrap/fieldset-trcl.html', + link: function(scope,element,attrs) { + scope.title = scope.$eval(attrs.title); + } + }; +}); diff --git a/src/directives/decorators/bootstrap/default.html b/src/directives/decorators/bootstrap/default.html index 9600587a1..704c6001d 100644 --- a/src/directives/decorators/bootstrap/default.html +++ b/src/directives/decorators/bootstrap/default.html @@ -1,13 +1,13 @@
- - {{form.description}} - {{schemaError}} + {{ (hasError() && errorMessage(schemaError())) || form.description}}
\ No newline at end of file diff --git a/src/directives/decorators/bootstrap/fieldset-trcl.html b/src/directives/decorators/bootstrap/fieldset-trcl.html new file mode 100644 index 000000000..5dacedb92 --- /dev/null +++ b/src/directives/decorators/bootstrap/fieldset-trcl.html @@ -0,0 +1,4 @@ +
+ {{ form.title }} +
+
\ No newline at end of file diff --git a/src/directives/decorators/bootstrap/fieldset.html b/src/directives/decorators/bootstrap/fieldset.html index a3cc6bf86..1f4ebd19f 100644 --- a/src/directives/decorators/bootstrap/fieldset.html +++ b/src/directives/decorators/bootstrap/fieldset.html @@ -1,4 +1,4 @@
{{ form.title }} - +
\ No newline at end of file diff --git a/src/directives/decorators/bootstrap/select.html b/src/directives/decorators/bootstrap/select.html index 11681c309..67f5b7565 100644 --- a/src/directives/decorators/bootstrap/select.html +++ b/src/directives/decorators/bootstrap/select.html @@ -8,6 +8,5 @@ ng-required="form.required" ng-options="val as name for (val,name) in form.titleMap"> - {{form.description}} - {{schemaError}} + {{ (hasError() && errorMessage(schemaError())) || form.description}} \ No newline at end of file diff --git a/src/directives/decorators/bootstrap/submit.html b/src/directives/decorators/bootstrap/submit.html index 697439576..3c7dab2bc 100644 --- a/src/directives/decorators/bootstrap/submit.html +++ b/src/directives/decorators/bootstrap/submit.html @@ -1,3 +1,10 @@
- + +
\ No newline at end of file diff --git a/src/directives/decorators/bootstrap/textarea.html b/src/directives/decorators/bootstrap/textarea.html index 1c8fc3729..035d8d55a 100644 --- a/src/directives/decorators/bootstrap/textarea.html +++ b/src/directives/decorators/bootstrap/textarea.html @@ -5,6 +5,5 @@ ng-model="$$value$$" schema-validate="form.schema"> - {{form.description}} - {{schemaError}} {{ngModel.$error}} + {{ (hasError() && errorMessage(schemaError())) || form.description}} \ No newline at end of file diff --git a/src/directives/schema-form.js b/src/directives/schema-form.js index 0f964850b..4f4496b52 100644 --- a/src/directives/schema-form.js +++ b/src/directives/schema-form.js @@ -32,6 +32,11 @@ function($compile, schemaForm, schemaFormDecorators){ initialForm: '=sfForm', model: '=sfModel' }, + controller: ['$scope',function($scope){ + this.evalInParentScope = function(expr,locals){ + $scope.$parent.$eval(expr,locals); + }; + }], replace: false, restrict: "A", transclude: true, diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index 595b4a390..373670872 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -6,6 +6,7 @@ angular.module('schemaForm').directive('schemaValidate',function(){ require: 'ngModel', link: function(scope,element,attrs,ngModel) { scope.ngModel = ngModel; + var error = null; var schema = scope.$eval(attrs.schemaValidate); ngModel.$parsers.unshift(function(viewValue) { @@ -13,6 +14,11 @@ angular.module('schemaForm').directive('schemaValidate',function(){ schema = scope.$eval(attrs.schemaValidate); } + //Still might be undefined, especially if form has no schema... + if (!schema) { + return viewValue; + } + //required is handled by ng-required if (angular.isUndefined(viewValue)) { return undefined; @@ -38,11 +44,12 @@ angular.module('schemaForm').directive('schemaValidate',function(){ if (result.valid) { // it is valid ngModel.$setValidity('schema', true); + error = null; return viewValue; } else { // it is invalid, return undefined (no model update) ngModel.$setValidity('schema', false); - scope.schemaError = result.error.message; + error = result.error; return undefined; } }); @@ -52,6 +59,10 @@ angular.module('schemaForm').directive('schemaValidate',function(){ return scope.ngModel.$invalid && !scope.ngModel.$pristine; }; + scope.schemaError = function() { + return error; + }; + } }; }); \ No newline at end of file diff --git a/src/field-example.html b/src/field-example.html new file mode 100644 index 000000000..a349ace14 --- /dev/null +++ b/src/field-example.html @@ -0,0 +1,81 @@ + + + + Boostrap Schema Form example + + + + + + +
+

Field Example

+ +
+
+ + + + + + + + + +
+

Model

+
{{pretty()}}
+
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/services/decorators.js b/src/services/decorators.js index 540eb00a2..002246cce 100644 --- a/src/services/decorators.js +++ b/src/services/decorators.js @@ -38,11 +38,11 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider' replace: true, transclude: false, scope: true, - link: function(scope,element,attrs) { + require: '?^sfSchema', + link: function(scope,element,attrs,sfSchema) { //rebind our part of the form to the scope. var once = scope.$watch(attrs.form,function(form){ - if (form) { scope.form = form; @@ -51,7 +51,7 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider' //for fieldsets to recurse properly. var url = templateUrl(name,form); $http.get(url,{ cache: $templateCache }).then(function(res){ - var template = res.data.replace(/\$\$value\$\$/g,'model.'+form.key); + var template = res.data.replace(/\$\$value\$\$/g,'model.'+(form.key || "")); $compile(template)(scope,function(clone){ element.replaceWith(clone); }); @@ -74,29 +74,125 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider' }); return lst; }; + + scope.buttonClick = function($event,form) { + if (angular.isFunction(form.onClick)) { + form.onClick($event,form); + } else if (angular.isString(form.onClick)) { + if (sfSchema) { + //evaluating in scope outside of sfSchemas isolated scope + sfSchema.evalInParentScope(form.onClick,{'$event':$event,form:form}); + } else { + scope.$eval(form.onClick,{'$event':$event,form:form}); + } + } + }; + + /** + * Error message handler + * An error can either be a schema validation message or a angular js validtion + * error (i.e. required) + */ + scope.errorMessage = function(schemaError) { + //User has supplied validation messages + if (scope.form.validationMessage) { + if (schemaError) { + if (angular.isString(scope.form.validationMessage)) { + return scope.form.validationMessage; + } + + return scope.form.validationMessage[schemaError.code] || scope.form.validationMessage['default']; + } else { + return scope.form.validationMessage.required || scope.form.validationMessage['default'] || scope.form.validationMessage; + } + } + + //No user supplied validation message. + if (schemaError) { + return schemaError.message; //use tv4.js validation message + } + + //Otherwise we only use required so it must be it. + return "Required"; + + }; } }; }]); }; + var createManualDirective = function(type,templateUrl,transclude) { + transclude = angular.isDefined(transclude)? transclude : false; + $compileProvider.directive('sf'+angular.uppercase(type[0])+type.substr(1), function(){ + return { + restrict: "EAC", + scope: true, + replace: true, + transclude: transclude, + template: '', + link: function(scope,element,attrs) { + var watchThis = { + 'items': 'c', + 'titleMap': 'c', + 'schema': 'c' + }; + var form = { type: type }; + var once = true; + angular.forEach(attrs,function(value,name){ + if (name[0] !== '$' && name.indexOf('ng') !== 0 && name !== 'sfField') { + + var updateForm = function(val){ + if (angular.isDefined(val) && val !== form[name]) { + form[name] = val; + + //when we have type, and if specified key we apply it on scope. + if (once && form.type && (form.key || angular.isUndefined(attrs.key))) { + scope.form = form; + once = false; + } + } + }; + + if (name === 'model') { + //"model" is bound to scope under the name "model" since this is what the decorators + //know and love. + scope.$watch(value,function(val){ + if (val && scope.model !== val) { + scope.model = val; + } + }); + } else if (watchThis[name] === 'c') { + //watch collection + scope.$watchCollection(value,updateForm); + } else { + //$observe + attrs.$observe(name,updateForm); + } + } + }); + } + }; + }); + }; + + + /** - * Create a decorator directive + * Create a decorator directive and its sibling "manual" use directives. * 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. * - * ex. Basic usage with form and schema - * - * - * ex. "Manual" usage - * + ** * @param {string} name directive name (CamelCased) * @param {Object} mappings, an object that maps "type" => "templateUrl" * @param {Array} rules (optional) a list of functions, function(form){}, that are each tried in turn, * if they return a string then that is used as the templateUrl. Rules come before * mappings. */ - this.create = function(name,mappings,rules){ + this.createDecorator = function(name,mappings,rules){ directives[name] = { mappings: mappings || {}, rules: rules || [] @@ -108,6 +204,30 @@ angular.module('schemaForm').provider('schemaFormDecorators',['$compileProvider' createDirective(name); }; + /** + * Creates a directive of a decorator + * Usable when you want to use the decorators without using directive. + * Specifically when you need to reuse styling. + * + * ex. createDirective('text','...') + * + * + * @param {string} type The type of the directive, resulting directive will have sf- prefixed + * @param {string} templateUrl + * @param {boolean} transclude (optional) sets transclude option of directive, defaults to false. + */ + this.createDirective = createManualDirective; + + /** + * Same as createDirective, but takes an object where key is 'type' and value is 'templateUrl' + * Useful for batching. + * @param {Object} mappings + */ + this.createDirectives = function(mappings) { + angular.forEach(mappings,function(url,type){ + createManualDirective(type,url); + }); + }; /** * Getter for directive mappings diff --git a/src/services/schema-form.js b/src/services/schema-form.js index 4e8e29bb4..e6f767440 100644 --- a/src/services/schema-form.js +++ b/src/services/schema-form.js @@ -30,6 +30,12 @@ angular.module('schemaForm').factory('schemaForm',[function(){ obj = { key: obj }; } + //if it's a type with items, merge 'em! + if (obj.items) { + obj.items = service.merge(schema,obj.items,ignore); + } + + //extend with std form from schema. if (obj.key && lookup[obj.key]) { return angular.extend(lookup[obj.key],obj); @@ -51,6 +57,10 @@ angular.module('schemaForm').factory('schemaForm',[function(){ if (schema.readOnly || schema.readonly) f.readonly = schema.readOnly || schema.readonly; if (schema.minimum) f.minimum = schema.minimum + (schema.exclusiveMinimum?1:0); if (schema.maximum) f.maximum = schema.maximum - (schema.exclusiveMaximum?1:0); + + //Non standard attributes + if (schema.validationMessage) f.validationMessage = schema.validationMessage; + if (schema.enumNames) f.titleMap = schema.enumNames; f.schema = schema; return f; }; @@ -104,10 +114,12 @@ angular.module('schemaForm').factory('schemaForm',[function(){ var f = stdFormObj(schema,options); f.key = options.path; f.type = 'select'; - f.titleMap = {}; - schema.enum.forEach(function(name){ - f.titleMap[name] = name; - }); + if (!f.titleMap) { + f.titleMap = {}; + schema.enum.forEach(function(name){ + f.titleMap[name] = name; + }); + } options.lookup[options.path] = f; return f; } @@ -118,10 +130,12 @@ angular.module('schemaForm').factory('schemaForm',[function(){ var f = stdFormObj(schema,options); f.key = options.path; f.type = 'checkboxes'; - f.titleMap = {}; - schema.items.enum.forEach(function(name){ - f.titleMap[name] = name; - }); + if (!f.titleMap) { + f.titleMap = {}; + schema.items.enum.forEach(function(name){ + f.titleMap[name] = name; + }); + } options.lookup[options.path] = f; return f; } @@ -133,6 +147,7 @@ angular.module('schemaForm').factory('schemaForm',[function(){ var f = stdFormObj(schema,options); f.type = 'fieldset'; f.items = []; + options.lookup[options.path] = f; //recurse down into properties angular.forEach(schema.properties,function(v,k){ diff --git a/test/schema-form-test.js b/test/schema-form-test.js index bddf3a846..245ac5c76 100644 --- a/test/schema-form-test.js +++ b/test/schema-form-test.js @@ -200,7 +200,35 @@ describe('Schema form',function(){ tmpl.children().eq(1).children('select').length.should.equal(1); tmpl.children().eq(2).find('input').is('input[type=submit]').should.be.true; tmpl.children().eq(2).find('input').val().should.be.equal('Okidoki'); + }); + }); + + it('should handle buttons',function(){ + + inject(function($compile,$rootScope){ + var scope = $rootScope.$new(); + scope.obj = {}; + + scope.schema = exampleSchema; + + scope.form = ["*",{ type: 'button',title: 'Okidoki', onClick: sinon.spy()}]; + + var tmpl = angular.element('
'); + + $compile(tmpl)(scope); + $rootScope.$apply(); + tmpl.children().length.should.be.equal(3); + tmpl.children().eq(0).is('div.form-group').should.be.true; + tmpl.children().eq(0).find('input').is('input[type="text"]').should.be.true; + tmpl.children().eq(1).is('div.form-group').should.be.true; + tmpl.children().eq(1).children('select').length.should.equal(1); + tmpl.children().eq(2).find('button').length.should.be.equal(1); + tmpl.children().eq(2).find('button').text().should.be.equal('Okidoki'); + + scope.form[1].onClick.should.not.have.beenCalled; + tmpl.children().eq(2).find('button').click(); + scope.form[1].onClick.should.have.beenCalledOnce; }); }); @@ -300,6 +328,56 @@ describe('Schema form',function(){ }); }); + it('should display custom validationMessages when specified',function(done){ + + inject(function($compile,$rootScope){ + var scope = $rootScope.$new(); + scope.person = {}; + + scope.schema = { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z]+", + "validationMessage": "You are only allowed lower case letters in name." + }, + "nick": { + "type": "string", + "pattern": "^[a-z]+", + }, + } + }; + + scope.form = [ + "name", + { + key: 'nick', + validationMessage: 'Foobar' + } + ]; + + var tmpl = angular.element('
'); + + $compile(tmpl)(scope); + $rootScope.$apply(); + tmpl.find('input').each(function(){ + $(this).scope().ngModel.$setViewValue('AÖ'); + }); + + var errors = tmpl.find('.help-block'); + + //timeout so we can do a second $apply + setTimeout(function(){ + $rootScope.$apply(); //this actually updates the view with error messages + errors.eq(0).text().should.be.equal("You are only allowed lower case letters in name."); + errors.eq(1).text().should.be.equal("Foobar"); + done(); + },0); + + }); + }); + it('should use ng-required on required fields',function(){ @@ -440,6 +518,65 @@ describe('Schema form',function(){ }); }); + it('should handle schema form defaults in deep structure',function(){ + + inject(function($compile,$rootScope){ + var scope = $rootScope.$new(); + scope.person = { + name: 'Foobar' + }; + + scope.schema = { + "type": "object", + "properties": { + "props" : { + "type": "object", + "title": "Person", + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "nick": { + "type": "string", + "title": "Nick" + }, + "alias": { + "type": "string", + "title": "Alias" + } + } + } + } + }; + + //The form defines a fieldset for person, and changes the order of fields + //but titles should come from the schema + scope.form = [{ + type: 'fieldset', + key: 'props', + items: [ + 'props.nick', + 'props.name', + 'props.alias' + ] + }]; + + var tmpl = angular.element('
'); + + $compile(tmpl)(scope); + $rootScope.$apply(); + + tmpl.children().length.should.be.eq(1); + var labels = tmpl.children().children().find('label'); + labels.eq(0).text().should.equal('Nick'); + labels.eq(1).text().should.equal('Name'); + labels.eq(2).text().should.equal('Alias'); + + }); + }); + + it('should skip title if form says "notitle"',function(){ inject(function($compile,$rootScope){ @@ -775,9 +912,9 @@ describe('Schema form',function(){ }); describe('decorator factory service',function(){ - it.only('should enable you to create new decorator directives',function(){ + it('should enable you to create new decorator directives',function(){ module(function(schemaFormDecoratorsProvider){ - schemaFormDecoratorsProvider.create('foobar',{ 'foo':'/bar.html' },[angular.noop]); + schemaFormDecoratorsProvider.createDecorator('foobar',{ 'foo':'/bar.html' },[angular.noop]); }); inject(function($rootScope,$compile,$templateCache){