├── 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 |
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 | [](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 |
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 |
116 | */
117 | serverForm.serialize(namelessFormController);
118 | /* Returns:
119 | {
120 | input: ""
121 | }
122 | */
123 |
124 | /* Named form:
125 |
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 |
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 | '');
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 |
--------------------------------------------------------------------------------