From 801547602b7de8ddbc3c51345d114b245e3cb561 Mon Sep 17 00:00:00 2001 From: Ryan Hutchison Date: Wed, 5 Aug 2015 00:40:54 -0400 Subject: [PATCH] client-side form validation with ng-messages. remove data prefix from attributes. fix tests --- bower.json | 3 +- config/assets/default.js | 1 + config/assets/production.js | 1 + .../controllers/articles.client.controller.js | 20 ++++- .../views/create-article.client.view.html | 17 ++--- .../views/edit-article.client.view.html | 17 ++--- .../articles.client.controller.tests.js | 10 +-- modules/core/client/app/config.js | 2 +- modules/core/client/css/core.css | 15 ++-- .../show-errors.client.directives.js | 74 +++++++++++++++++++ .../admin/user.client.controller.js | 8 +- .../authentication.client.controller.js | 20 ++++- .../change-password.client.controller.js | 9 ++- .../edit-profile.client.controller.js | 27 ++++--- .../views/admin/user-edit.client.view.html | 24 +++--- .../authentication/signin.client.view.html | 20 +++-- .../authentication/signup.client.view.html | 46 +++++++----- .../settings/change-password.client.view.html | 23 ++++-- .../settings/edit-profile.client.view.html | 31 +++++--- .../authentication.client.controller.tests.js | 12 +-- 20 files changed, 274 insertions(+), 106 deletions(-) create mode 100644 modules/core/client/directives/show-errors.client.directives.js diff --git a/bower.json b/bower.json index 5e87d4d990..117ca44a2f 100644 --- a/bower.json +++ b/bower.json @@ -11,7 +11,8 @@ "angular-bootstrap": "~0.13", "angular-ui-utils": "bower", "angular-ui-router": "~0.2", - "angular-file-upload": "1.1.5" + "angular-file-upload": "1.1.5", + "angular-messages": "1.3.17" }, "resolutions": { "angular": "~1.3" diff --git a/config/assets/default.js b/config/assets/default.js index ce8d70a65d..e7d4f0bd23 100644 --- a/config/assets/default.js +++ b/config/assets/default.js @@ -11,6 +11,7 @@ module.exports = { 'public/lib/angular/angular.js', 'public/lib/angular-resource/angular-resource.js', 'public/lib/angular-animate/angular-animate.js', + 'public/lib/angular-messages/angular-messages.js', 'public/lib/angular-ui-router/release/angular-ui-router.js', 'public/lib/angular-ui-utils/ui-utils.js', 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js', diff --git a/config/assets/production.js b/config/assets/production.js index ed4f296173..71d755d5b2 100644 --- a/config/assets/production.js +++ b/config/assets/production.js @@ -11,6 +11,7 @@ module.exports = { 'public/lib/angular/angular.min.js', 'public/lib/angular-resource/angular-resource.min.js', 'public/lib/angular-animate/angular-animate.min.js', + 'public/lib/angular-messages/angular-messages.min.js', 'public/lib/angular-ui-router/release/angular-ui-router.min.js', 'public/lib/angular-ui-utils/ui-utils.min.js', 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js', diff --git a/modules/articles/client/controllers/articles.client.controller.js b/modules/articles/client/controllers/articles.client.controller.js index 4a47d5a27b..f36ea4f328 100644 --- a/modules/articles/client/controllers/articles.client.controller.js +++ b/modules/articles/client/controllers/articles.client.controller.js @@ -6,7 +6,15 @@ angular.module('articles').controller('ArticlesController', ['$scope', '$statePa $scope.authentication = Authentication; // Create new Article - $scope.create = function () { + $scope.create = function (isValid) { + $scope.error = null; + + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'articleForm'); + + return false; + } + // Create new Article object var article = new Articles({ title: this.title, @@ -43,7 +51,15 @@ angular.module('articles').controller('ArticlesController', ['$scope', '$statePa }; // Update existing Article - $scope.update = function () { + $scope.update = function (isValid) { + $scope.error = null; + + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'articleForm'); + + return false; + } + var article = $scope.article; article.$update(function () { diff --git a/modules/articles/client/views/create-article.client.view.html b/modules/articles/client/views/create-article.client.view.html index 235e6b37af..fd61646138 100644 --- a/modules/articles/client/views/create-article.client.view.html +++ b/modules/articles/client/views/create-article.client.view.html @@ -3,19 +3,18 @@

New Article

-
+
-
- -
- +
+ + +
+

Article title is required.

- -
- -
+ +
diff --git a/modules/articles/client/views/edit-article.client.view.html b/modules/articles/client/views/edit-article.client.view.html index 69eb621b2e..138a74d4a6 100644 --- a/modules/articles/client/views/edit-article.client.view.html +++ b/modules/articles/client/views/edit-article.client.view.html @@ -3,19 +3,18 @@

Edit Article

- +
-
- -
- +
+ + +
+

Article title is required.

- -
- -
+ +
diff --git a/modules/articles/tests/client/articles.client.controller.tests.js b/modules/articles/tests/client/articles.client.controller.tests.js index 2e6f317f34..934acdf03c 100644 --- a/modules/articles/tests/client/articles.client.controller.tests.js +++ b/modules/articles/tests/client/articles.client.controller.tests.js @@ -97,7 +97,7 @@ expect(scope.article).toEqualData(mockArticle); })); - describe('$scope.craete()', function () { + describe('$scope.create()', function () { var sampleArticlePostData; beforeEach(function () { @@ -119,7 +119,7 @@ $httpBackend.expectPOST('api/articles', sampleArticlePostData).respond(mockArticle); // Run controller functionality - scope.create(); + scope.create(true); $httpBackend.flush(); // Test form inputs are reset @@ -136,7 +136,7 @@ message: errorMessage }); - scope.create(); + scope.create(true); $httpBackend.flush(); expect(scope.error).toBe(errorMessage); @@ -154,7 +154,7 @@ $httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond(); // Run controller functionality - scope.update(); + scope.update(true); $httpBackend.flush(); // Test URL location to new object @@ -167,7 +167,7 @@ message: errorMessage }); - scope.update(); + scope.update(true); $httpBackend.flush(); expect(scope.error).toBe(errorMessage); diff --git a/modules/core/client/app/config.js b/modules/core/client/app/config.js index 98b48bffff..10e0588cb7 100644 --- a/modules/core/client/app/config.js +++ b/modules/core/client/app/config.js @@ -4,7 +4,7 @@ var ApplicationConfiguration = (function () { // Init module configuration options var applicationModuleName = 'mean'; - var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ui.router', 'ui.bootstrap', 'ui.utils', 'angularFileUpload']; + var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ngMessages', 'ui.router', 'ui.bootstrap', 'ui.utils', 'angularFileUpload']; // Add a new vertical module var registerModule = function (moduleName, dependencies) { diff --git a/modules/core/client/css/core.css b/modules/core/client/css/core.css index 861e085d37..2cbf0af6cf 100644 --- a/modules/core/client/css/core.css +++ b/modules/core/client/css/core.css @@ -12,12 +12,6 @@ .x-ng-cloak { display: none !important; } -.ng-invalid.ng-dirty { - border-color: #FA787E; -} -.ng-valid.ng-dirty { - border-color: #78FA89; -} .header-profile-image { opacity: 0.8; height: 28px; @@ -33,3 +27,12 @@ a:hover .header-profile-image { padding-top: 11px !important; padding-bottom: 11px !important; } +.error-text { + display: none; +} +.has-error .help-block.error-text { + display: block; +} +.has-error .help-inline.error-text { + display: inline; +} diff --git a/modules/core/client/directives/show-errors.client.directives.js b/modules/core/client/directives/show-errors.client.directives.js new file mode 100644 index 0000000000..acfbea23d4 --- /dev/null +++ b/modules/core/client/directives/show-errors.client.directives.js @@ -0,0 +1,74 @@ +'use strict'; + +/** + * Edits by Ryan Hutchison + * Credit: https://github.com/paulyoder/angular-bootstrap-show-errors */ + +angular.module('core') + .directive('showErrors', ['$timeout', '$interpolate', function ($timeout, $interpolate) { + var linkFn = function (scope, el, attrs, formCtrl) { + var inputEl, inputName, inputNgEl, options, showSuccess, toggleClasses, + initCheck = false, + showValidationMessages = false, + blurred = false; + + options = scope.$eval(attrs.showErrors) || {}; + showSuccess = options.showSuccess || false; + inputEl = el[0].querySelector('.form-control[name]') || el[0].querySelector('[name]'); + inputNgEl = angular.element(inputEl); + inputName = $interpolate(inputNgEl.attr('name') || '')(scope); + + if (!inputName) { + throw 'show-errors element has no child input elements with a \'name\' attribute class'; + } + + var reset = function () { + return $timeout(function () { + el.removeClass('has-error'); + el.removeClass('has-success'); + showValidationMessages = false; + }, 0, false); + }; + + scope.$watch(function () { + return formCtrl[inputName] && formCtrl[inputName].$invalid; + }, function (invalid) { + return toggleClasses(invalid); + }); + + scope.$on('show-errors-check-validity', function (event, name) { + if (angular.isUndefined(name) || formCtrl.$name === name) { + initCheck = true; + showValidationMessages = true; + + return toggleClasses(formCtrl[inputName].$invalid); + } + }); + + scope.$on('show-errors-reset', function (event, name) { + if (angular.isUndefined(name) || formCtrl.$name === name) { + return reset(); + } + }); + + toggleClasses = function (invalid) { + el.toggleClass('has-error', showValidationMessages && invalid); + if (showSuccess) { + return el.toggleClass('has-success', showValidationMessages && !invalid); + } + }; + }; + + return { + restrict: 'A', + require: '^form', + compile: function (elem, attrs) { + if (attrs.showErrors.indexOf('skipFormGroupCheck') === -1) { + if (!(elem.hasClass('form-group') || elem.hasClass('input-group'))) { + throw 'show-errors element does not have the \'form-group\' or \'input-group\' class'; + } + } + return linkFn; + } + }; +}]); diff --git a/modules/users/client/controllers/admin/user.client.controller.js b/modules/users/client/controllers/admin/user.client.controller.js index 22a1dd4de4..7a017598d5 100644 --- a/modules/users/client/controllers/admin/user.client.controller.js +++ b/modules/users/client/controllers/admin/user.client.controller.js @@ -19,7 +19,13 @@ angular.module('users.admin').controller('UserController', ['$scope', '$state', } }; - $scope.update = function () { + $scope.update = function (isValid) { + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'userForm'); + + return false; + } + var user = $scope.user; user.$update(function () { diff --git a/modules/users/client/controllers/authentication.client.controller.js b/modules/users/client/controllers/authentication.client.controller.js index 2bcd6d0fe9..f5f256e6c6 100644 --- a/modules/users/client/controllers/authentication.client.controller.js +++ b/modules/users/client/controllers/authentication.client.controller.js @@ -12,7 +12,15 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$stat $location.path('/'); } - $scope.signup = function () { + $scope.signup = function (isValid) { + $scope.error = null; + + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'userForm'); + + return false; + } + $http.post('/api/auth/signup', $scope.credentials).success(function (response) { // If successful we assign the response to the global user model $scope.authentication.user = response; @@ -24,7 +32,15 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$stat }); }; - $scope.signin = function () { + $scope.signin = function (isValid) { + $scope.error = null; + + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'userForm'); + + return false; + } + $http.post('/api/auth/signin', $scope.credentials).success(function (response) { // If successful we assign the response to the global user model $scope.authentication.user = response; diff --git a/modules/users/client/controllers/settings/change-password.client.controller.js b/modules/users/client/controllers/settings/change-password.client.controller.js index d5ac3b2186..5e32e11d9b 100644 --- a/modules/users/client/controllers/settings/change-password.client.controller.js +++ b/modules/users/client/controllers/settings/change-password.client.controller.js @@ -5,11 +5,18 @@ angular.module('users').controller('ChangePasswordController', ['$scope', '$http $scope.user = Authentication.user; // Change user password - $scope.changeUserPassword = function () { + $scope.changeUserPassword = function (isValid) { $scope.success = $scope.error = null; + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'passwordForm'); + + return false; + } + $http.post('/api/users/password', $scope.passwordDetails).success(function (response) { // If successful show success message and clear form + $scope.$broadcast('show-errors-reset', 'passwordForm'); $scope.success = true; $scope.passwordDetails = null; }).error(function (response) { diff --git a/modules/users/client/controllers/settings/edit-profile.client.controller.js b/modules/users/client/controllers/settings/edit-profile.client.controller.js index cb985ad51b..edade73f11 100644 --- a/modules/users/client/controllers/settings/edit-profile.client.controller.js +++ b/modules/users/client/controllers/settings/edit-profile.client.controller.js @@ -6,19 +6,24 @@ angular.module('users').controller('EditProfileController', ['$scope', '$http', // Update a user profile $scope.updateUserProfile = function (isValid) { - if (isValid) { - $scope.success = $scope.error = null; - var user = new Users($scope.user); + $scope.success = $scope.error = null; - user.$update(function (response) { - $scope.success = true; - Authentication.user = response; - }, function (response) { - $scope.error = response.data.message; - }); - } else { - $scope.submitted = true; + if (!isValid) { + $scope.$broadcast('show-errors-check-validity', 'userForm'); + + return false; } + + var user = new Users($scope.user); + + user.$update(function (response) { + $scope.$broadcast('show-errors-reset', 'userForm'); + + $scope.success = true; + Authentication.user = response; + }, function (response) { + $scope.error = response.data.message; + }); }; } ]); diff --git a/modules/users/client/views/admin/user-edit.client.view.html b/modules/users/client/views/admin/user-edit.client.view.html index 46c8e349f2..d09286c50b 100644 --- a/modules/users/client/views/admin/user-edit.client.view.html +++ b/modules/users/client/views/admin/user-edit.client.view.html @@ -1,26 +1,28 @@
- +
-
- -
- +
+ + +
+

First name is required.

-
- -
- +
+ + +
+

Last name is required.

- +
diff --git a/modules/users/client/views/authentication/signin.client.view.html b/modules/users/client/views/authentication/signin.client.view.html index cb2d908b3c..02d836fb9f 100644 --- a/modules/users/client/views/authentication/signin.client.view.html +++ b/modules/users/client/views/authentication/signin.client.view.html @@ -1,17 +1,21 @@

Or with your account

-
- +
+
-
+
- + +
+

Username is required.

+
-
+
- + +
+

Password is required.

+
diff --git a/modules/users/client/views/authentication/signup.client.view.html b/modules/users/client/views/authentication/signup.client.view.html index 2227fab432..12c4a7e3e7 100644 --- a/modules/users/client/views/authentication/signup.client.view.html +++ b/modules/users/client/views/authentication/signup.client.view.html @@ -1,32 +1,44 @@

Or sign up using your email

-
- +
+
-
+
- + +
+

First name is required.

+
-
+
- + +
+

Last name is required.

+
-
+
- + +
+

Email address is required.

+

Email address is invalid.

+
-
+
- + +
+

Username is required.

+
-
+
- + +
+

Password is required.

+

Password is too short.

+
diff --git a/modules/users/client/views/settings/change-password.client.view.html b/modules/users/client/views/settings/change-password.client.view.html index 28e72dddce..3ea89b03ef 100644 --- a/modules/users/client/views/settings/change-password.client.view.html +++ b/modules/users/client/views/settings/change-password.client.view.html @@ -1,18 +1,27 @@
- +
-
+
- + +
+

Your current password is required.

+
-
+
- + +
+

Enter a new password.

+
-
+
- + +
+

Verify your new password.

+
diff --git a/modules/users/client/views/settings/edit-profile.client.view.html b/modules/users/client/views/settings/edit-profile.client.view.html index 2f461127ad..d47a4b692e 100644 --- a/modules/users/client/views/settings/edit-profile.client.view.html +++ b/modules/users/client/views/settings/edit-profile.client.view.html @@ -1,22 +1,35 @@
- +
-
+
- + +
+

First name is required.

+
-
+
- + +
+

Last name is required.

+
-
+
- + +
+

Email address is required.

+

Email address is invalid.

+
-
+
- + +
+

Username is required.

+
diff --git a/modules/users/tests/client/authentication.client.controller.tests.js b/modules/users/tests/client/authentication.client.controller.tests.js index 9322d23a8a..62022529c7 100644 --- a/modules/users/tests/client/authentication.client.controller.tests.js +++ b/modules/users/tests/client/authentication.client.controller.tests.js @@ -51,7 +51,7 @@ // Test expected GET request $httpBackend.when('POST', '/api/auth/signin').respond(200, 'Fred'); - scope.signin(); + scope.signin(true); $httpBackend.flush(); // Test scope value @@ -65,7 +65,7 @@ 'message': 'Missing credentials' }); - scope.signin(); + scope.signin(true); $httpBackend.flush(); // Test scope value @@ -82,7 +82,7 @@ 'message': 'Unknown user' }); - scope.signin(); + scope.signin(true); $httpBackend.flush(); // Test scope value @@ -96,12 +96,12 @@ scope.authentication.user = 'Fred'; $httpBackend.when('POST', '/api/auth/signup').respond(200, 'Fred'); - scope.signup(); + scope.signup(true); $httpBackend.flush(); // test scope value expect(scope.authentication.user).toBe('Fred'); - expect(scope.error).toEqual(undefined); + expect(scope.error).toEqual(null); expect($location.url()).toBe('/'); }); @@ -111,7 +111,7 @@ 'message': 'Username already exists' }); - scope.signup(); + scope.signup(true); $httpBackend.flush(); // Test scope value