├── examples ├── src ├── index.html └── simple │ └── index.html ├── .gitattributes ├── .travis.yml ├── .gitignore ├── .editorconfig ├── .jshintrc ├── test ├── e2e │ └── simple.spec.js └── unit │ └── angular-server-form.spec.js ├── bower.json ├── protractor.conf.js ├── LICENSE ├── package.json ├── karma.conf.js ├── dist ├── angular-server-form.min.js └── angular-server-form.js ├── gulpfile.js ├── src └── angular-server-form.js └── README.md /examples/src: -------------------------------------------------------------------------------- 1 | ../src -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | 5 | before_install: 6 | - npm install -g bower 7 | - bower install 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependency directory 6 | node_modules 7 | bower_components 8 | 9 | # OS X 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples 7 | 8 | 9 | 10 |

Examples

11 |
12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": false, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "validthis": true, 22 | "globals": { 23 | "angular": false, 24 | "afterEach": false, 25 | "beforeEach": false, 26 | "browser": false, 27 | "by": false, 28 | "element": false, 29 | "spyOn": false, 30 | "describe": false, 31 | "expect": false, 32 | "inject": false, 33 | "it": false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/e2e/simple.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var simple = { 4 | favorite: element(by.name('favorite')), 5 | submit: element(by.buttonText('Submit')), 6 | messages: element(by.id('messages')) 7 | }; 8 | 9 | describe('Simple Form', function () { 10 | 11 | beforeEach(function () { 12 | browser.get('/simple'); 13 | }); 14 | 15 | it('shows error when you submit empty favorite', function () { 16 | simple.favorite.clear(); 17 | simple.submit.click(); 18 | expect(simple.messages.getText()).toEqual('cannot be empty'); 19 | }); 20 | 21 | it('shows saved message when you input a value', function () { 22 | simple.favorite.clear(); 23 | simple.favorite.sendKeys('banana'); 24 | simple.submit.click(); 25 | expect(simple.messages.getText()).toEqual('Saved!'); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-server-form", 3 | "description": "simple server validation with angular", 4 | "version": "0.2.0", 5 | "homepage": "https://github.com/cesarandreu/angular-server-form", 6 | "authors": [ 7 | "Cesar Andreu " 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/cesarandreu/angular-server-form.git" 12 | }, 13 | "keywords": [], 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "src", 20 | "gulpfile.js", 21 | "*.conf.js", 22 | "package.json", 23 | "bower.json" 24 | ], 25 | "main": "dist/angular-server-form.js", 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "angular": "~1.3.0", 29 | "angular-mocks": "~1.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.config = { 4 | specs: [ 5 | 'test/e2e/*.spec.js', 6 | ], 7 | 8 | capabilities: { 9 | browserName: 'phantomjs', 10 | 'phantomjs.binary.path': './node_modules/phantomjs/bin/phantomjs' 11 | }, 12 | 13 | baseUrl: 'http://localhost:9999', 14 | rootElement: 'body', 15 | allScriptsTimeout: 11000, 16 | getPageTimeout: 10000, 17 | 18 | framework: 'jasmine', 19 | 20 | // Options to be passed to minijasminenode. 21 | // 22 | // See the full list at https://github.com/juliemr/minijasminenode/tree/jasmine1 23 | jasmineNodeOpts: { 24 | // If true, display spec names. 25 | isVerbose: false, 26 | // If true, print colors to the terminal. 27 | showColors: true, 28 | // If true, include stack traces in failures. 29 | includeStackTrace: true, 30 | // Default time to wait in ms before a test fails. 31 | defaultTimeoutInterval: 30000 32 | } 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Cesar Andreu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-server-form", 3 | "description": "Simple server validation with angular", 4 | "license": "MIT", 5 | "main": "", 6 | "keywords": [], 7 | "version": "0.2.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/cesarandreu/angular-server-form.git" 11 | }, 12 | "author": { 13 | "name": "Cesar Andreu", 14 | "email": "cesarandreu@gmail.com", 15 | "url": "https://github.com/cesarandreu" 16 | }, 17 | "engines": { 18 | "node": ">=0.10.0" 19 | }, 20 | "scripts": { 21 | "build": "./node_modules/.bin/gulp build", 22 | "tdd": "./node_modules/.bin/gulp tdd", 23 | "unit": "./node_modules/.bin/gulp unit", 24 | "e2e": "./node_modules/.bin/gulp e2e", 25 | "examples": "./node_modules/.bin/gulp server", 26 | "test": "./node_modules/.bin/gulp unit && ./node_modules/.bin/gulp e2e" 27 | }, 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "express": "^4.7.2", 31 | "gulp": "^3.8.6", 32 | "gulp-protractor": "0.0.11", 33 | "gulp-rename": "^1.2.0", 34 | "gulp-uglify": "^0.3.1", 35 | "karma": "^0.12.17", 36 | "karma-jasmine": "^0.1.5", 37 | "karma-phantomjs-launcher": "^0.1.4", 38 | "phantomjs": "^1.9.7-15", 39 | "protractor": "^1.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 'use strict'; 3 | var configuration = { 4 | 5 | frameworks: ['jasmine'], 6 | 7 | // list of files / patterns to load in the browser 8 | files: [ 9 | 'bower_components/angular/angular.js', 10 | 'bower_components/angular-mocks/angular-mocks.js', 11 | 'src/*.js', 12 | 'test/unit/*.spec.js', 13 | ], 14 | 15 | // test results reporter to use 16 | // possible values: 'dots', 'progress', 'junit' 17 | reporters: ['progress'], 18 | 19 | // web server port 20 | port: 9876, 21 | 22 | // cli runner port 23 | runnerPort: 9100, 24 | 25 | // enable / disable colors in the output (reporters and logs) 26 | colors: true, 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: true, 30 | 31 | // Start these browsers, currently available: 32 | // - Chrome 33 | // - ChromeCanary 34 | // - Firefox 35 | // - Opera 36 | // - Safari (only Mac) 37 | // - PhantomJS 38 | // - IE (only Windows) 39 | browsers: ['PhantomJS'], 40 | 41 | // If browser does not capture in given timeout [ms], kill it 42 | captureTimeout: 60000, 43 | 44 | // Continuous Integration mode 45 | // if true, it capture browsers, run tests and exit 46 | singleRun: false 47 | 48 | }; 49 | 50 | if (config) { 51 | config.set(configuration); 52 | } else { 53 | return configuration; 54 | } 55 | }; 56 | 57 | 58 | -------------------------------------------------------------------------------- /dist/angular-server-form.min.js: -------------------------------------------------------------------------------- 1 | !function(r,e){"use strict";e.module("angular-server-form",[]).provider("serverForm",[function(){var r=this;r.errorsKey="errors",r.logging=!0,r.$get=["$http","$q","$log",function(n,o,t){function i(r,e){(r.$render||r.$setViewValue)&&r.$setValidity("server",!1),r.$setPristine(),r.$server=e}function s(r){var e={};for(var n in r)r.hasOwnProperty(n)&&"$"!==n[0]&&(e[n]=r[n].hasOwnProperty("$modelValue")?r[n].$modelValue:s(r[n]));return e}var a=this;return a.serialize=function(r){var e={};return r.$name?e[r.$name]=s(r):e=s(r),e},a.applyErrors=function(n,o){if(e.isString(o))i(n,o);else if(e.isArray(o))i(n,o.join(", "));else for(var s in o)o.hasOwnProperty(s)&&(n.hasOwnProperty(s)?a.applyErrors(n[s],o[s]):n.$name===s?a.applyErrors(n,o[s]):r.logging&&t.warn("No place to render property",s,"in form",n))},a.clearErrors=function u(r){(r.$render||r.$setViewValue)&&r.$setValidity("server",!0),r.$setPristine(),r.$server="";for(var e in r)r.hasOwnProperty(e)&&"$"!==e[0]&&u(r[e])},a.submit=function(e,o){return a.clearErrors(e),e.$submitting=!0,e.$saved=!1,n(o).success(function(){e.$saved=!0}).error(function(n,o){e.$saved=!1,422===o&&(r.errorsKey?a.applyErrors(e,n[r.errorsKey]):a.applyErrors(e,n))}).finally(function(){e.$submitting=!1})},a}]}]).directive("serverForm",["serverForm",function(r){return{restrict:"A",scope:{url:"@",method:"@",onSuccess:"=",onError:"="},require:"form",link:function(e,n,o,t){function i(n){e.$apply(function(){if(!t.$submitting){var o={url:e.url,method:e.method?e.method:"POST",data:r.serialize(t)};r.submit(t,o).then(e.onSuccess,e.onError)}n.preventDefault()})}n.on("submit",i),e.$on("destroy",function(){n.off("submit",i)})}}}])}(window,window.angular); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | uglify = require('gulp-uglify'), 5 | rename = require('gulp-rename'), 6 | karma = require('karma').server, 7 | express = require('express'), 8 | protractor = require('gulp-protractor'), 9 | server; 10 | 11 | gulp.task('default', ['unit', 'e2e', 'build']); 12 | 13 | // Copy and minify project 14 | gulp.task('build', function () { 15 | return gulp.src('src/*.js') 16 | .pipe(gulp.dest('dist')) 17 | .pipe(uglify()) 18 | .pipe(rename({ 19 | extname: '.min.js' 20 | })) 21 | .pipe(gulp.dest('dist')); 22 | }); 23 | 24 | // Examples server 25 | gulp.task('server', function (done) { 26 | server = express(); 27 | server.use(express.static(__dirname + '/examples/')); 28 | server = server.listen(9999, function () { 29 | console.log('Listening on port 9999'); 30 | done(); 31 | }); 32 | }); 33 | 34 | // Run unit tests once and exit 35 | gulp.task('unit', function (done) { 36 | var config = require('./karma.conf.js')(); 37 | config.singleRun = true; 38 | karma.start(config, done); 39 | }); 40 | 41 | // Continuously run unit tests 42 | gulp.task('tdd', function (done) { 43 | var config = require('./karma.conf.js')(); 44 | karma.start(config, done); 45 | }); 46 | 47 | /// Run e2e tests once and exit 48 | gulp.task('webdriverUpdate', protractor.webdriver_update); 49 | gulp.task('e2e', ['server', 'webdriverUpdate'], function (done) { 50 | var close = function (err) { 51 | server.close(function () { 52 | done(err); 53 | }); 54 | }; 55 | 56 | gulp.src('test/e2e/*.spec.js') 57 | .pipe(protractor.protractor({ 58 | configFile: 'protractor.conf.js', 59 | args: ['--baseUrl', 'http://' + server.address().address + ':' + server.address().port] 60 | })) 61 | .on('error', close) 62 | .on('end', close); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Form 7 | 8 | 9 | 10 | 37 | 38 | 39 | 40 |
41 |

Fruits

42 |
43 | 46 | 47 |
48 |
49 |
50 |
51 | Saved! 52 |
53 |
54 | 55 | 56 |
57 | 58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /dist/angular-server-form.js: -------------------------------------------------------------------------------- 1 | (function (window, angular) { 2 | 'use strict'; 3 | 4 | angular.module('angular-server-form', []) 5 | .provider('serverForm', [function () { 6 | 7 | var provider = this; 8 | 9 | provider.errorsKey = 'errors'; 10 | provider.logging = true; 11 | provider.$get = ['$http', '$q', '$log', function ($http, $q, $log) { 12 | 13 | // Private 14 | // Set error message on form value 15 | function setError (form, message) { 16 | // Only NgModelController has $render and $setViewValue 17 | // Don't call $setValidity on FormController 18 | if (form.$render || form.$setViewValue) { 19 | form.$setValidity('server', false); 20 | } 21 | form.$setPristine(); 22 | form.$server = message; 23 | } 24 | 25 | // Private 26 | // recurse over the form 27 | // if the property is a value it gets assigned 28 | // otherwise dive inside 29 | function formCrawler (form) { 30 | var response = {}; 31 | for (var prop in form) { 32 | if (form.hasOwnProperty(prop) && prop[0] !== '$') { 33 | if (form[prop].hasOwnProperty('$modelValue')) { 34 | response[prop] = form[prop].$modelValue; 35 | } else { 36 | response[prop] = formCrawler(form[prop]); 37 | } 38 | } 39 | } 40 | return response; 41 | } 42 | 43 | var self = this; 44 | 45 | // Serialize form to data object 46 | self.serialize = function serialize (form) { 47 | var response = {}; 48 | 49 | // set root if form name is set 50 | if (form.$name) { 51 | response[form.$name] = formCrawler(form); 52 | } else { 53 | response = formCrawler(form); 54 | } 55 | return response; 56 | }; 57 | 58 | // Apply all messages from errors object on form 59 | // The error value must be an array of strings or a string 60 | self.applyErrors = function applyErrors (form, errors) { 61 | if (angular.isString(errors)) { 62 | // If it's a string then set the error message 63 | setError(form, errors); 64 | } else if (angular.isArray(errors)) { 65 | // If it's an array then join them and set the error message 66 | setError(form, errors.join(', ')); 67 | } else { 68 | // Otherwise we must crawl the errors 69 | for (var prop in errors) { 70 | if (errors.hasOwnProperty(prop)) { 71 | if (form.hasOwnProperty(prop)) { 72 | // If the form has a property with the same error name 73 | self.applyErrors(form[prop], errors[prop]); 74 | } else if (form.$name === prop) { 75 | // errors object is for the whole form, dive in 76 | self.applyErrors(form, errors[prop]); 77 | } else { 78 | if (provider.logging) { 79 | $log.warn('No place to render property', prop, 'in form', form); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | }; 86 | 87 | // Crawl form and reset errors 88 | self.clearErrors = function clearErrors (form) { 89 | // Only NgModelController has $render and $setViewValue 90 | // Don't call $setValidity on FormController 91 | if (form.$render || form.$setViewValue) { 92 | form.$setValidity('server', true); 93 | } 94 | form.$setPristine(); 95 | form.$server = ''; 96 | 97 | for (var prop in form) { 98 | if (form.hasOwnProperty(prop) && prop[0] !== '$') { 99 | clearErrors(form[prop]); 100 | } 101 | } 102 | }; 103 | 104 | self.submit = function submit (form, config) { 105 | self.clearErrors(form); // resets previous server errors 106 | form.$submitting = true; 107 | form.$saved = false; 108 | 109 | return $http(config) 110 | .success(function () { 111 | form.$saved = true; 112 | }) 113 | .error(function(res, status) { 114 | form.$saved = false; 115 | if (status === 422) { 116 | if (provider.errorsKey) { 117 | self.applyErrors(form, res[provider.errorsKey]); 118 | } else { 119 | self.applyErrors(form, res); 120 | } 121 | } 122 | }) 123 | .finally(function () { 124 | form.$submitting = false; 125 | }); 126 | }; 127 | 128 | return self; 129 | }]; 130 | }]) 131 | .directive('serverForm', ['serverForm', function (serverForm) { 132 | 133 | return { 134 | restrict: 'A', 135 | scope: { 136 | url: '@', 137 | method: '@', 138 | onSuccess: '=', 139 | onError: '=' 140 | }, 141 | require: 'form', 142 | link: function postLink(scope, iElement, iAttrs, form) { 143 | 144 | function submitForm (ev) { 145 | scope.$apply(function () { 146 | if (!form.$submitting) { 147 | var config = { 148 | url: scope.url, 149 | method: scope.method ? scope.method : 'POST', 150 | data: serverForm.serialize(form) 151 | }; 152 | 153 | serverForm.submit(form, config) 154 | .then(scope.onSuccess, scope.onError); 155 | } 156 | 157 | ev.preventDefault(); 158 | }); 159 | } 160 | 161 | iElement.on('submit', submitForm); 162 | scope.$on('destroy', function () { 163 | iElement.off('submit', submitForm); 164 | }); 165 | 166 | } 167 | }; 168 | }]); 169 | 170 | })(window, window.angular); 171 | -------------------------------------------------------------------------------- /src/angular-server-form.js: -------------------------------------------------------------------------------- 1 | (function (window, angular) { 2 | 'use strict'; 3 | 4 | angular.module('angular-server-form', []) 5 | .provider('serverForm', [function () { 6 | 7 | var provider = this; 8 | 9 | provider.errorsKey = 'errors'; 10 | provider.logging = true; 11 | provider.$get = ['$http', '$q', '$log', function ($http, $q, $log) { 12 | 13 | // Private 14 | // Set error message on form value 15 | function setError (form, message) { 16 | // Only NgModelController has $render and $setViewValue 17 | // Don't call $setValidity on FormController 18 | if (form.$render || form.$setViewValue) { 19 | form.$setValidity('server', false); 20 | } 21 | form.$setPristine(); 22 | form.$server = message; 23 | } 24 | 25 | // Private 26 | // recurse over the form 27 | // if the property is a value it gets assigned 28 | // otherwise dive inside 29 | function formCrawler (form) { 30 | var response = {}; 31 | for (var prop in form) { 32 | if (form.hasOwnProperty(prop) && prop[0] !== '$') { 33 | if (form[prop].hasOwnProperty('$modelValue')) { 34 | response[prop] = form[prop].$modelValue; 35 | } else { 36 | response[prop] = formCrawler(form[prop]); 37 | } 38 | } 39 | } 40 | return response; 41 | } 42 | 43 | var self = this; 44 | 45 | // Serialize form to data object 46 | self.serialize = function serialize (form) { 47 | var response = {}; 48 | 49 | // set root if form name is set 50 | if (form.$name) { 51 | response[form.$name] = formCrawler(form); 52 | } else { 53 | response = formCrawler(form); 54 | } 55 | return response; 56 | }; 57 | 58 | // Apply all messages from errors object on form 59 | // The error value must be an array of strings or a string 60 | self.applyErrors = function applyErrors (form, errors) { 61 | if (angular.isString(errors)) { 62 | // If it's a string then set the error message 63 | setError(form, errors); 64 | } else if (angular.isArray(errors)) { 65 | // If it's an array then join them and set the error message 66 | setError(form, errors.join(', ')); 67 | } else { 68 | // Otherwise we must crawl the errors 69 | for (var prop in errors) { 70 | if (errors.hasOwnProperty(prop)) { 71 | if (form.hasOwnProperty(prop)) { 72 | // If the form has a property with the same error name 73 | self.applyErrors(form[prop], errors[prop]); 74 | } else if (form.$name === prop) { 75 | // errors object is for the whole form, dive in 76 | self.applyErrors(form, errors[prop]); 77 | } else { 78 | if (provider.logging) { 79 | $log.warn('No place to render property', prop, 'in form', form); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | }; 86 | 87 | // Crawl form and reset errors 88 | self.clearErrors = function clearErrors (form) { 89 | // Only NgModelController has $render and $setViewValue 90 | // Don't call $setValidity on FormController 91 | if (form.$render || form.$setViewValue) { 92 | form.$setValidity('server', true); 93 | } 94 | form.$setPristine(); 95 | form.$server = ''; 96 | 97 | for (var prop in form) { 98 | if (form.hasOwnProperty(prop) && prop[0] !== '$') { 99 | clearErrors(form[prop]); 100 | } 101 | } 102 | }; 103 | 104 | self.submit = function submit (form, config) { 105 | self.clearErrors(form); // resets previous server errors 106 | form.$submitting = true; 107 | form.$saved = false; 108 | 109 | return $http(config) 110 | .success(function () { 111 | form.$saved = true; 112 | }) 113 | .error(function(res, status) { 114 | form.$saved = false; 115 | if (status === 422) { 116 | if (provider.errorsKey) { 117 | self.applyErrors(form, res[provider.errorsKey]); 118 | } else { 119 | self.applyErrors(form, res); 120 | } 121 | } 122 | }) 123 | .finally(function () { 124 | form.$submitting = false; 125 | }); 126 | }; 127 | 128 | return self; 129 | }]; 130 | }]) 131 | .directive('serverForm', ['serverForm', function (serverForm) { 132 | 133 | return { 134 | restrict: 'A', 135 | scope: { 136 | url: '@', 137 | method: '@', 138 | onSuccess: '=', 139 | onError: '=' 140 | }, 141 | require: 'form', 142 | link: function postLink(scope, iElement, iAttrs, form) { 143 | 144 | function submitForm (ev) { 145 | scope.$apply(function () { 146 | if (!form.$submitting) { 147 | var config = { 148 | url: scope.url, 149 | method: scope.method ? scope.method : 'POST', 150 | data: serverForm.serialize(form) 151 | }; 152 | 153 | serverForm.submit(form, config) 154 | .then(scope.onSuccess, scope.onError); 155 | } 156 | 157 | ev.preventDefault(); 158 | }); 159 | } 160 | 161 | iElement.on('submit', submitForm); 162 | scope.$on('destroy', function () { 163 | iElement.off('submit', submitForm); 164 | }); 165 | 166 | } 167 | }; 168 | }]); 169 | 170 | })(window, window.angular); 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-server-form 2 | 3 | [![Build Status](https://travis-ci.org/cesarandreu/angular-server-form.svg?branch=master)](https://travis-ci.org/cesarandreu/angular-server-form) 4 | 5 | angular-server-form provides a directive and service to simplify server-side validation. 6 | It provides automatic propagation of server-side errors on your forms. 7 | 8 | 9 | ## Installation 10 | 11 | You can install angular-server-form with bower: 12 | 13 | ```shell 14 | bower install angular-server-form 15 | ``` 16 | 17 | Otherwise you can download the files from the `dist` folder and include them in your project. 18 | 19 | Then you must include `angular-server-form` as a module dependency. 20 | 21 | ```javascript 22 | var app = angular.module('app', ['angular-server-form']); 23 | ``` 24 | 25 | ## Examples 26 | 27 | To view examples do the following: 28 | 29 | 1. Clone this repo 30 | 2. Run `npm install` and `bower install` 31 | 3. Run `npm run examples` 32 | 4. Navigate to `127.0.0.1:9999` with your browser 33 | 34 | You can view the source for all examples in the `examples` folder. 35 | 36 | 37 | ## Directive 38 | 39 | `server-form` serializes your form into an object, submits it, and applies errors on the form controller if a 422 response is received. 40 | 41 | ### Details 42 | 43 | * Serializes the form controller on submit by calling `serverForm.serialize` 44 | * Submits the serialized form using `serverForm.submit` 45 | * Does not allow you to submit the form if it is already submitting 46 | 47 | ### Attributes 48 | 49 | * **url** : String (required) - url to which the form must be submitted 50 | * **method** : String - http method to use when submitting the form, default: `POST` 51 | * **on-success** : Function - callback for when form submission is successful 52 | * **on-error** : Function - callback for when form submission is unsuccessful 53 | 54 | ### Example 55 | 56 | **Controller** 57 | 58 | ```javascript 59 | $scope.model = { 60 | favorite: 'banana' 61 | }; 62 | ``` 63 | 64 | **View** 65 | 66 | ```html 67 |
68 | 71 | 72 |
73 | ``` 74 | 75 | ## Service 76 | 77 | `serverForm` is a service to assist in handling forms with server-side validation. 78 | 79 | ### Configuration 80 | 81 | Inject `serverFormProvider` to set the following: 82 | 83 | * logging : Boolean - controls whether or not to log a message when a server error cannot be rendered, default: `true` 84 | * errorsKey : String - object key to check for form errors on the data object of any 422 server response, when set to a falsy value it will use the data object directly, default: `errors` 85 | 86 | 87 | **Example** 88 | 89 | ```javascript 90 | angular.module('app') 91 | .config(function (serverFormProvider) { 92 | serverFormProvider.logging = true; 93 | serverFormProvider.errorsKey = 'errors'; 94 | }); 95 | ``` 96 | 97 | ### Methods 98 | 99 | #### serialize(form) 100 | 101 | * Takes a form controller and returns a form data object. 102 | * If the form controller has a name, the data object's root will be the form name. 103 | * Form controls must not start with `$` 104 | 105 | **Params:** 106 | 107 | * form : Object (required) - an instance of [NgFormController](https://docs.angularjs.org/api/ng/type/form.FormController) 108 | 109 | **Example:** 110 | 111 | ```javascript 112 | /* Nameless form: 113 |
114 | 115 |
116 | */ 117 | serverForm.serialize(namelessFormController); 118 | /* Returns: 119 | { 120 | input: "" 121 | } 122 | */ 123 | 124 | /* Named form: 125 |
126 | 127 |
128 | */ 129 | serverForm.serialize(namedFormController); 130 | /* Returns: 131 | { 132 | name: { 133 | input: "" 134 | } 135 | } 136 | */ 137 | ``` 138 | 139 | #### applyErrors(form, errors) 140 | 141 | * Applies errors on the form controller's fields 142 | * If logging is enabled, it'll log a warning when a server error cannot be rendered on the form 143 | * Sets form controls to pristine 144 | * Sets the server $error value to `true` 145 | * Sets the error message on the `$server` value of the form controls to the 146 | 147 | **Params:** 148 | 149 | * form : Object (required) - an instance of [NgFormController](https://docs.angularjs.org/api/ng/type/form.FormController) 150 | * errors : Object (required) - object that matches the form's structure, values must be strings, arrays of strings (will be concatenated with `, `), or objects (for nested forms) 151 | 152 | **Example:** 153 | 154 | ```javascript 155 | /* Form: 156 |
157 | 158 |
159 | */ 160 | 161 | /* With errors object that has root */ 162 | serverForm.applyErrors(firstFormController, { 163 | name: { 164 | input: "cannot be blank" 165 | } 166 | }); 167 | 168 | firstFormController.input.$server 169 | // "cannot be blank" 170 | 171 | firstFormController.input.$pristine 172 | // true 173 | 174 | /* With errors object that has no root */ 175 | serverForm.applyErrors(secondFormController, { 176 | input: "cannot be blank" 177 | }); 178 | 179 | secondFormController.input.$server 180 | // "cannot be blank" 181 | 182 | secondFormController.input.$pristine 183 | // true 184 | ``` 185 | 186 | #### clearErrors(form) 187 | 188 | * Sets all `$server` errors on form controller's fields to empty string 189 | * Sets form controller's fields to pristine and validity of server to true 190 | 191 | **Params:** 192 | 193 | * form : Object (required) - an instance of [NgFormController](https://docs.angularjs.org/api/ng/type/form.FormController) 194 | 195 | 196 | #### submit(form, config) 197 | 198 | * Calls `clearErrors` on form 199 | * Sets $submitting to true, sets $saved to false 200 | * When it finishes submitting it sets $submitting to false 201 | * If resolved succesfully it sets $saved to true, otherwise it's set to false 202 | * If the status code is `422` it will call `applyErrors` on the form with the errors data response 203 | 204 | **Params:** 205 | 206 | * form : Object (required) - an instance of [NgFormController](https://docs.angularjs.org/api/ng/type/form.FormController) 207 | * config : Object (required) - $http configuration object 208 | 209 | **Returns:** 210 | 211 | * $http promise object 212 | 213 | 214 | ## Tests 215 | 216 | The getting started steps are always the same: 217 | 218 | 1. Clone the repo 219 | 2. Run `npm install` 220 | 3. Run `bower install` 221 | 222 | After you have the dependencies installed: 223 | 224 | * Run unit tests with `npm run unit` 225 | * Run e2e tests with `npm run e2e` 226 | * Run both with `npm test` 227 | 228 | -------------------------------------------------------------------------------- /test/unit/angular-server-form.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('angular-server-form', function () { 4 | var scope, compile, httpBackend, elm, serverForm, form, err, model; 5 | 6 | beforeEach(function () { 7 | module('angular-server-form'); 8 | inject(function ($rootScope, $compile, $httpBackend, _serverForm_) { 9 | scope = $rootScope.$new(); 10 | compile = $compile; 11 | serverForm = _serverForm_; 12 | httpBackend = $httpBackend; 13 | }); 14 | 15 | }); 16 | 17 | describe('service', function () { 18 | 19 | describe('serialize', function () { 20 | 21 | it('works with flat forms containing a root', function () { 22 | 23 | model = { 24 | flat: { 25 | first: 'fistValue', 26 | second: 'secondValue' 27 | } 28 | }; 29 | scope.model = model.flat; 30 | 31 | elm = angular.element( 32 | '
' + 33 | '' + 34 | '' + 35 | '
'); 36 | elm = compile(elm)(scope); 37 | scope.$digest(); 38 | 39 | expect(serverForm.serialize(elm.controller('form'))).toEqual(model); 40 | }); 41 | 42 | it('works with flat forms without a root', function () { 43 | model = { 44 | first: 'fistValue', 45 | second: 'secondValue' 46 | }; 47 | scope.model = model; 48 | 49 | elm = angular.element( 50 | '
' + 51 | '' + 52 | '' + 53 | '
'); 54 | elm = compile(elm)(scope); 55 | scope.$digest(); 56 | 57 | expect(serverForm.serialize(elm.controller('form'))).toEqual(model); 58 | }); 59 | 60 | it('works with nested forms with a root', function () { 61 | model = { 62 | nested: { 63 | first: 'fistValue', 64 | second: 'secondValue', 65 | third: { 66 | fourth: 'fourthValue', 67 | fifth: 'fifthValue' 68 | } 69 | } 70 | }; 71 | scope.model = model.nested; 72 | 73 | elm = angular.element( 74 | '
' + 75 | '' + 76 | '' + 77 | '' + 78 | '' + 79 | '' + 80 | '' + 81 | '
'); 82 | elm = compile(elm)(scope); 83 | scope.$digest(); 84 | 85 | expect(serverForm.serialize(elm.controller('form'))).toEqual(model); 86 | }); 87 | 88 | it('works with nested forms without a root', function () { 89 | model = { 90 | first: 'fistValue', 91 | second: 'secondValue', 92 | third: { 93 | fourth: 'fourthValue', 94 | fifth: 'fifthValue' 95 | } 96 | }; 97 | scope.model = model; 98 | 99 | elm = angular.element( 100 | '
' + 101 | '' + 102 | '' + 103 | '' + 104 | '' + 105 | '' + 106 | '' + 107 | '
'); 108 | elm = compile(elm)(scope); 109 | scope.$digest(); 110 | 111 | expect(serverForm.serialize(elm.controller('form'))).toEqual(model); 112 | }); 113 | 114 | }); 115 | 116 | describe('applyErrors', function () { 117 | 118 | it('sets errors on flat forms containing a root', function () { 119 | 120 | err = { 121 | flat: { 122 | first: 'Error string', 123 | second: ['Error', 'array'] 124 | } 125 | }; 126 | 127 | model = { 128 | flat: { 129 | first: 'fistValue', 130 | second: 'secondValue' 131 | } 132 | }; 133 | scope.model = model.flat; 134 | 135 | elm = angular.element( 136 | '
' + 137 | '' + 138 | '' + 139 | '
'); 140 | elm = compile(elm)(scope); 141 | scope.$digest(); 142 | 143 | form = elm.controller('form'); 144 | serverForm.applyErrors(form, err); 145 | 146 | expect(form.first.$server).toBe(err.flat.first); 147 | expect(form.second.$server).toBe(err.flat.second.join(', ')); 148 | expect(form.first.$error.server).toBe(true); 149 | expect(form.second.$error.server).toBe(true); 150 | expect(form.first.$pristine).toBe(true); 151 | expect(form.second.$pristine).toBe(true); 152 | }); 153 | 154 | it('sets errors on flat forms without a root', function () { 155 | 156 | err = { 157 | first: 'Error string', 158 | second: ['Error', 'array'] 159 | }; 160 | 161 | model = { 162 | first: 'fistValue', 163 | second: 'secondValue' 164 | }; 165 | scope.model = model; 166 | 167 | elm = angular.element( 168 | '
' + 169 | '' + 170 | '' + 171 | '
'); 172 | elm = compile(elm)(scope); 173 | scope.$digest(); 174 | 175 | form = elm.controller('form'); 176 | serverForm.applyErrors(form, err); 177 | 178 | expect(form.first.$server).toBe(err.first); 179 | expect(form.second.$server).toBe(err.second.join(', ')); 180 | expect(form.first.$error.server).toBe(true); 181 | expect(form.second.$error.server).toBe(true); 182 | expect(form.first.$pristine).toBe(true); 183 | expect(form.second.$pristine).toBe(true); 184 | }); 185 | 186 | it('sets errors on nested forms with root', function () { 187 | 188 | err = { 189 | flat: { 190 | first: 'First error string', 191 | nested: { 192 | second: 'Second error string', 193 | third: ['Third', 'error', 'array'] 194 | } 195 | } 196 | }; 197 | 198 | model = { 199 | flat: { 200 | first: 'fistValue', 201 | nested: { 202 | second: 'secondValue', 203 | third: 'thirdValue' 204 | } 205 | } 206 | }; 207 | scope.model = model; 208 | 209 | elm = angular.element( 210 | '
' + 211 | '' + 212 | '' + 213 | '' + 214 | '' + 215 | '' + 216 | '
'); 217 | elm = compile(elm)(scope); 218 | scope.$digest(); 219 | 220 | form = elm.controller('form'); 221 | serverForm.applyErrors(form, err); 222 | 223 | expect(form.first.$server).toBe(err.flat.first); 224 | expect(form.nested.second.$server).toBe(err.flat.nested.second); 225 | expect(form.nested.third.$server).toBe(err.flat.nested.third.join(', ')); 226 | expect(form.first.$error.server).toBe(true); 227 | expect(form.nested.second.$error.server).toBe(true); 228 | expect(form.nested.third.$error.server).toBe(true); 229 | expect(form.first.$pristine).toBe(true); 230 | expect(form.nested.second.$pristine).toBe(true); 231 | expect(form.nested.third.$pristine).toBe(true); 232 | }); 233 | 234 | it('sets errors on nested forms without root', function () { 235 | 236 | err = { 237 | first: 'First error string', 238 | nested: { 239 | second: 'Second error string', 240 | third: ['Third', 'error', 'array'] 241 | } 242 | }; 243 | 244 | model = { 245 | first: 'fistValue', 246 | nested: { 247 | second: 'secondValue', 248 | third: 'thirdValue' 249 | } 250 | }; 251 | scope.model = model; 252 | 253 | elm = angular.element( 254 | '
' + 255 | '' + 256 | '' + 257 | '' + 258 | '' + 259 | '' + 260 | '
'); 261 | elm = compile(elm)(scope); 262 | scope.$digest(); 263 | 264 | form = elm.controller('form'); 265 | serverForm.applyErrors(form, err); 266 | 267 | expect(form.first.$server).toBe(err.first); 268 | expect(form.nested.second.$server).toBe(err.nested.second); 269 | expect(form.nested.third.$server).toBe(err.nested.third.join(', ')); 270 | expect(form.first.$error.server).toBe(true); 271 | expect(form.nested.second.$error.server).toBe(true); 272 | expect(form.nested.third.$error.server).toBe(true); 273 | expect(form.first.$pristine).toBe(true); 274 | expect(form.nested.second.$pristine).toBe(true); 275 | expect(form.nested.third.$pristine).toBe(true); 276 | }); 277 | 278 | }); 279 | 280 | describe('clearErrors', function () { 281 | 282 | it('clears errors on flat forms containing a root', function () { 283 | 284 | err = { 285 | flat: { 286 | first: 'Error string', 287 | second: ['Error', 'array'] 288 | } 289 | }; 290 | 291 | model = { 292 | flat: { 293 | first: 'fistValue', 294 | second: 'secondValue' 295 | } 296 | }; 297 | scope.model = model.flat; 298 | 299 | elm = angular.element( 300 | '
' + 301 | '' + 302 | '' + 303 | '
'); 304 | elm = compile(elm)(scope); 305 | scope.$digest(); 306 | 307 | form = elm.controller('form'); 308 | serverForm.applyErrors(form, err); 309 | 310 | expect(form.first.$server).toBe(err.flat.first); 311 | expect(form.second.$server).toBe(err.flat.second.join(', ')); 312 | expect(form.first.$error.server).toBe(true); 313 | expect(form.second.$error.server).toBe(true); 314 | 315 | serverForm.clearErrors(form); 316 | expect(form.$server).toBe(''); 317 | expect(form.first.$server).toBe(''); 318 | expect(form.second.$server).toBe(''); 319 | expect(form.first.$error.server).toBe(false); 320 | expect(form.second.$error.server).toBe(false); 321 | }); 322 | 323 | it('clears errors on flat forms without a root', function () { 324 | 325 | err = { 326 | first: 'Error string', 327 | second: ['Error', 'array'] 328 | }; 329 | 330 | model = { 331 | first: 'fistValue', 332 | second: 'secondValue' 333 | }; 334 | scope.model = model; 335 | 336 | elm = angular.element( 337 | '
' + 338 | '' + 339 | '' + 340 | '
'); 341 | elm = compile(elm)(scope); 342 | scope.$digest(); 343 | 344 | form = elm.controller('form'); 345 | serverForm.applyErrors(form, err); 346 | 347 | expect(form.first.$server).toBe(err.first); 348 | expect(form.second.$server).toBe(err.second.join(', ')); 349 | expect(form.first.$error.server).toBe(true); 350 | expect(form.second.$error.server).toBe(true); 351 | 352 | serverForm.clearErrors(form); 353 | expect(form.$server).toBe(''); 354 | expect(form.first.$server).toBe(''); 355 | expect(form.second.$server).toBe(''); 356 | expect(form.first.$error.server).toBe(false); 357 | expect(form.second.$error.server).toBe(false); 358 | }); 359 | 360 | it('clears errors on nested forms with root', function () { 361 | 362 | err = { 363 | flat: { 364 | first: 'First error string', 365 | nested: { 366 | second: 'Second error string', 367 | third: ['Third', 'error', 'array'] 368 | } 369 | } 370 | }; 371 | 372 | model = { 373 | flat: { 374 | first: 'fistValue', 375 | nested: { 376 | second: 'secondValue', 377 | third: 'thirdValue' 378 | } 379 | } 380 | }; 381 | scope.model = model; 382 | 383 | elm = angular.element( 384 | '
' + 385 | '' + 386 | '' + 387 | '' + 388 | '' + 389 | '' + 390 | '
'); 391 | elm = compile(elm)(scope); 392 | scope.$digest(); 393 | 394 | form = elm.controller('form'); 395 | serverForm.applyErrors(form, err); 396 | 397 | expect(form.first.$server).toBe(err.flat.first); 398 | expect(form.nested.second.$server).toBe(err.flat.nested.second); 399 | expect(form.nested.third.$server).toBe(err.flat.nested.third.join(', ')); 400 | expect(form.first.$error.server).toBe(true); 401 | expect(form.nested.second.$error.server).toBe(true); 402 | expect(form.nested.third.$error.server).toBe(true); 403 | 404 | serverForm.clearErrors(form); 405 | expect(form.$server).toBe(''); 406 | expect(form.first.$server).toBe(''); 407 | expect(form.nested.$server).toBe(''); 408 | expect(form.nested.second.$server).toBe(''); 409 | expect(form.nested.third.$server).toBe(''); 410 | expect(form.first.$error.server).toBe(false); 411 | expect(form.nested.second.$error.server).toBe(false); 412 | expect(form.nested.third.$error.server).toBe(false); 413 | }); 414 | 415 | it('clears errors on nested forms without root', function () { 416 | 417 | err = { 418 | first: 'First error string', 419 | nested: { 420 | second: 'Second error string', 421 | third: ['Third', 'error', 'array'] 422 | } 423 | }; 424 | 425 | model = { 426 | first: 'fistValue', 427 | nested: { 428 | second: 'secondValue', 429 | third: 'thirdValue' 430 | } 431 | }; 432 | scope.model = model; 433 | 434 | elm = angular.element( 435 | '
' + 436 | '' + 437 | '' + 438 | '' + 439 | '' + 440 | '' + 441 | '
'); 442 | elm = compile(elm)(scope); 443 | scope.$digest(); 444 | 445 | form = elm.controller('form'); 446 | serverForm.applyErrors(form, err); 447 | 448 | expect(form.first.$server).toBe(err.first); 449 | expect(form.nested.second.$server).toBe(err.nested.second); 450 | expect(form.nested.third.$server).toBe(err.nested.third.join(', ')); 451 | expect(form.first.$error.server).toBe(true); 452 | expect(form.nested.second.$error.server).toBe(true); 453 | expect(form.nested.third.$error.server).toBe(true); 454 | 455 | serverForm.clearErrors(form); 456 | expect(form.$server).toBe(''); 457 | expect(form.first.$server).toBe(''); 458 | expect(form.nested.$server).toBe(''); 459 | expect(form.nested.second.$server).toBe(''); 460 | expect(form.nested.third.$server).toBe(''); 461 | expect(form.first.$error.server).toBe(false); 462 | expect(form.nested.second.$error.server).toBe(false); 463 | expect(form.nested.third.$error.server).toBe(false); 464 | }); 465 | 466 | }); 467 | 468 | describe('submit', function () { 469 | 470 | describe('$submitting and $saved', function () { 471 | 472 | beforeEach(function () { 473 | 474 | scope.model = { 475 | first: 'fistValue' 476 | }; 477 | 478 | elm = angular.element( 479 | '
' + 480 | '' + 481 | '
'); 482 | elm = compile(elm)(scope); 483 | scope.$digest(); 484 | 485 | form = elm.controller('form'); 486 | }); 487 | 488 | it('changes $submitting and $saved values on success', function () { 489 | httpBackend 490 | .when('POST', '/endpoint') 491 | .respond(200, {}); 492 | 493 | serverForm.submit(form, { 494 | url: '/endpoint', 495 | method: 'POST' 496 | }); 497 | expect(form.$saved).toEqual(false); 498 | expect(form.$submitting).toEqual(true); 499 | httpBackend.flush(); 500 | expect(form.$submitting).toEqual(false); 501 | expect(form.$saved).toEqual(true); 502 | }); 503 | 504 | it('changes $submitting and $saved values on failure', function () { 505 | httpBackend 506 | .when('POST', '/endpoint') 507 | .respond(400, {}); 508 | 509 | serverForm.submit(form, { 510 | url: '/endpoint', 511 | method: 'POST' 512 | }); 513 | expect(form.$saved).toEqual(false); 514 | expect(form.$submitting).toEqual(true); 515 | httpBackend.flush(); 516 | expect(form.$submitting).toEqual(false); 517 | expect(form.$saved).toEqual(false); 518 | }); 519 | 520 | }); 521 | 522 | 523 | it('calls clearErrors', function () { 524 | 525 | httpBackend 526 | .when('POST', '/endpoint') 527 | .respond(200, {}); 528 | 529 | scope.model = { 530 | first: 'fistValue' 531 | }; 532 | 533 | elm = angular.element( 534 | '
' + 535 | '' + 536 | '
'); 537 | elm = compile(elm)(scope); 538 | scope.$digest(); 539 | 540 | form = elm.controller('form'); 541 | 542 | spyOn(serverForm, 'clearErrors'); 543 | 544 | serverForm.submit(form, { 545 | url: '/endpoint', 546 | method: 'POST' 547 | }); 548 | httpBackend.flush(); 549 | expect(serverForm.clearErrors).toHaveBeenCalledWith(form); 550 | serverForm.clearErrors.reset(); 551 | }); 552 | 553 | }); 554 | 555 | }); 556 | 557 | describe('directive', function () { 558 | 559 | describe('failure', function () { 560 | 561 | beforeEach(function () { 562 | 563 | err = { 564 | errors: { 565 | animals: { 566 | count: 'Must be under 100' 567 | } 568 | } 569 | }; 570 | 571 | scope.count = 100; 572 | scope.url = '/error'; 573 | scope.success = function () {}; 574 | scope.error = function () {}; 575 | 576 | spyOn(serverForm, 'submit').andCallThrough(); 577 | spyOn(serverForm, 'serialize').andCallThrough(); 578 | spyOn(scope, 'success').andCallThrough(); 579 | spyOn(scope, 'error').andCallThrough(); 580 | 581 | httpBackend 582 | .when('POST', '/error') 583 | .respond(422, err); 584 | 585 | elm = angular.element( 586 | '
' + 587 | '' + 588 | '
'); 589 | 590 | elm = compile(elm)(scope); 591 | scope.$digest(); 592 | form = elm.controller('form'); 593 | 594 | elm.triggerHandler('submit'); 595 | httpBackend.flush(); 596 | }); 597 | 598 | afterEach(function () { 599 | 600 | serverForm.submit.reset(); 601 | serverForm.serialize.reset(); 602 | scope.success.reset(); 603 | scope.error.reset(); 604 | }); 605 | 606 | it('adds errors to form', function () { 607 | 608 | expect(form.count.$server).toEqual('Must be under 100'); 609 | expect(form.count.$error.server).toEqual(true); 610 | }); 611 | 612 | it('calls on-error on failure', function () { 613 | 614 | expect(scope.success).not.toHaveBeenCalled(); 615 | expect(scope.error).toHaveBeenCalled(); 616 | }); 617 | 618 | it('calls serialize and submit', function () { 619 | 620 | expect(serverForm.serialize).toHaveBeenCalledWith(form); 621 | expect(serverForm.submit).toHaveBeenCalledWith(form, { 622 | url: '/error', 623 | method: 'POST', 624 | data: { 625 | animals: { 626 | count: 100 627 | } 628 | } 629 | }); 630 | }); 631 | 632 | }); 633 | 634 | describe('success', function () { 635 | 636 | beforeEach(function () { 637 | 638 | scope.count = 100; 639 | scope.url = '/success'; 640 | scope.success = function () {}; 641 | scope.error = function () {}; 642 | 643 | spyOn(serverForm, 'submit').andCallThrough(); 644 | spyOn(serverForm, 'serialize').andCallThrough(); 645 | spyOn(scope, 'success').andCallThrough(); 646 | spyOn(scope, 'error').andCallThrough(); 647 | 648 | httpBackend 649 | .when('POST', '/success') 650 | .respond(200, {}); 651 | 652 | elm = angular.element( 653 | '
' + 654 | '' + 655 | '
'); 656 | 657 | elm = compile(elm)(scope); 658 | scope.$digest(); 659 | form = elm.controller('form'); 660 | 661 | elm.triggerHandler('submit'); 662 | httpBackend.flush(); 663 | }); 664 | 665 | afterEach(function () { 666 | 667 | serverForm.submit.reset(); 668 | serverForm.serialize.reset(); 669 | scope.success.reset(); 670 | scope.error.reset(); 671 | }); 672 | 673 | it('calls on-success on success', function () { 674 | 675 | expect(scope.success).toHaveBeenCalled(); 676 | expect(scope.error).not.toHaveBeenCalled(); 677 | }); 678 | 679 | it('calls serialize and submit', function () { 680 | 681 | expect(serverForm.serialize).toHaveBeenCalledWith(form); 682 | expect(serverForm.submit).toHaveBeenCalledWith(form, { 683 | url: '/success', 684 | method: 'POST', 685 | data: { 686 | animals: { 687 | count: 100 688 | } 689 | } 690 | }); 691 | }); 692 | 693 | }); 694 | 695 | describe('both', function () { 696 | 697 | beforeEach(function () { 698 | 699 | err = { 700 | errors: { 701 | animals: { 702 | count: 'Must be under 100' 703 | } 704 | } 705 | }; 706 | 707 | scope.count = 100; 708 | scope.url = '/error'; 709 | scope.success = function () {}; 710 | scope.error = function () {}; 711 | 712 | spyOn(scope, 'success').andCallThrough(); 713 | spyOn(scope, 'error').andCallThrough(); 714 | 715 | httpBackend 716 | .when('POST', '/error') 717 | .respond(422, err); 718 | 719 | httpBackend 720 | .when('POST', '/success') 721 | .respond(200, {}); 722 | 723 | elm = angular.element( 724 | '
' + 725 | '' + 726 | '
'); 727 | 728 | elm = compile(elm)(scope); 729 | scope.$digest(); 730 | form = elm.controller('form'); 731 | }); 732 | 733 | afterEach(function () { 734 | 735 | scope.success.reset(); 736 | scope.error.reset(); 737 | }); 738 | 739 | it('adds errors to form', function () { 740 | 741 | elm.triggerHandler('submit'); 742 | httpBackend.flush(); 743 | expect(form.count.$server).toEqual('Must be under 100'); 744 | expect(form.count.$error.server).toEqual(true); 745 | expect(scope.error).toHaveBeenCalled(); 746 | 747 | scope.url = '/success'; 748 | scope.$digest(); 749 | 750 | elm.triggerHandler('submit'); 751 | httpBackend.flush(); 752 | expect(form.count.$server).toEqual(''); 753 | expect(form.count.$error.server).toEqual(false); 754 | expect(form.$saved).toEqual(true); 755 | expect(scope.success).toHaveBeenCalled(); 756 | }); 757 | 758 | 759 | }); 760 | 761 | }); 762 | 763 | }); 764 | --------------------------------------------------------------------------------