├── .gitignore ├── LICENSE ├── README.md ├── angular-password.js ├── angular-password.min.js ├── bower.json ├── gulpfile.js ├── karma.conf.js ├── package.json └── tests └── angular-password.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | bower_components 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 PatrickJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-password 2 | ================ 3 | 4 | The most performant AngularJS directive for matching two password input fields. I use angular's built in $parsers rather than relying on a $watch 5 | 6 | Installation 7 | 8 | `bower install angular-password` 9 | 10 | or 11 | 12 | `npm install angular-password` 13 | 14 | Inject angular-password into your module 15 | 16 | ```javascript 17 | angular.module('yourmodulename', ['ngPassword']); 18 | ``` 19 | 20 | Simple example. 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | With ngMessages 27 | ```html 28 |
29 |
30 | 31 | 32 |
33 |
34 |
You did not enter a field name
35 |
36 |
37 | 38 | 39 |
40 |
41 |
You did not enter a field name
42 |
Your passwords did not match
43 |
44 |
45 |
46 | 47 |
48 | ``` 49 | 50 | Licensing information can be found [here](LICENSE) 51 | -------------------------------------------------------------------------------- /angular-password.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | function $Password() { 5 | 6 | function link(scope, element, attrs, ctrls) { 7 | var formController = ctrls[1]; 8 | var ngModel = ctrls[0]; 9 | var otherPasswordModel = formController[attrs.matchPassword]; 10 | 11 | var getMatchValue = function() { 12 | return otherPasswordModel.$viewValue; 13 | }; 14 | 15 | scope.$watch(getMatchValue, function() { 16 | ngModel.$$parseAndValidate(); 17 | }); 18 | 19 | // if ng1.3+ 20 | if (ngModel.$validators) { 21 | ngModel.$validators.passwordMatch = function(modelValue) { 22 | return (!modelValue && !otherPasswordModel.$modelValue) || (modelValue === otherPasswordModel.$modelValue); 23 | }; 24 | } else { 25 | ngModel.$parsers.push(function(value) { 26 | ngModel.$setValidity('passwordMatch', (!value && !otherPasswordModel.$viewValue) || value === otherPasswordModel.$viewValue); 27 | return value; 28 | }); 29 | } 30 | 31 | otherPasswordModel.$parsers.push(function(value) { 32 | ngModel.$setValidity('passwordMatch', (!value && !ngModel.$viewValue) || value === ngModel.$viewValue); 33 | return value; 34 | }); 35 | } 36 | 37 | var controllers = ['^ngModel', '^form']; 38 | 39 | return { 40 | restrict: 'A', 41 | require: controllers, 42 | link: link 43 | }; // end return 44 | } 45 | 46 | angular.module('ngPassword', []).directive('matchPassword', $Password); 47 | 48 | angular.module('angular.password', ['ngPassword']); 49 | angular.module('angular-password', ['ngPassword']); 50 | 51 | if (typeof module === 'object' && typeof define !== 'function') { 52 | module.exports = angular.module('ngPassword'); 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /angular-password.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function a(){function a(a,e,r,n){var s=n[1],u=n[0],o=s[r.matchPassword],t=function(){return o.$viewValue};a.$watch(t,function(){u.$$parseAndValidate()}),u.$validators?u.$validators.passwordMatch=function(a){return!a&&!o.$modelValue||a===o.$modelValue}:u.$parsers.push(function(a){return u.$setValidity("passwordMatch",!a&&!o.$viewValue||a===o.$viewValue),a}),o.$parsers.push(function(a){return u.$setValidity("passwordMatch",!a&&!u.$viewValue||a===u.$viewValue),a})}var e=["^ngModel","^form"];return{restrict:"A",require:e,link:a}}angular.module("ngPassword",[]).directive("matchPassword",a),angular.module("angular.password",["ngPassword"]),angular.module("angular-password",["ngPassword"]),"object"==typeof module&&"function"!=typeof define&&(module.exports=angular.module("ngPassword"))}(); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-password", 3 | "main": "angular-password.js", 4 | "version": "1.0.3", 5 | "homepage": "https://github.com/gdi2290/angular-password", 6 | "description": "The most performant AngularJS directive for matching two password input fields ", 7 | "keywords": [ 8 | "angular", 9 | "angularjs", 10 | "password", 11 | "match password", 12 | "confirm password", 13 | "passwords", 14 | "angular-password", 15 | "ngPassword", 16 | "gdi2290", 17 | "PatrickJS" 18 | ], 19 | "authors": [ 20 | "gdi2290 " 21 | ], 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components", 27 | "test", 28 | "tests" 29 | ], 30 | "dependencies" : { 31 | "angular" : "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | jshint = require('gulp-jshint'), 3 | uglify = require('gulp-uglify'), 4 | rename = require('gulp-rename'), 5 | KarmaServer = require('karma').Server; 6 | 7 | 8 | gulp.task('jshint', function() { 9 | return gulp.src('./angular-password.js') 10 | .pipe(jshint()) 11 | .pipe(jshint.reporter('default'), {verbose: true}) 12 | .pipe(jshint.reporter('fail')); 13 | }); 14 | 15 | gulp.task('test', gulp.series('jshint', function(done) { 16 | new KarmaServer({ 17 | configFile: __dirname + '/karma.conf.js', 18 | singleRun: true 19 | }, done).start(); 20 | })); 21 | 22 | gulp.task('dist', gulp.series('test', function() { 23 | return gulp.src('./angular-password.js') 24 | .pipe(uglify()) 25 | .pipe(rename('angular-password.min.js')) 26 | .pipe(gulp.dest('./')); 27 | })); 28 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Mar 27 2016 03:17:28 GMT-0400 (Eastern Daylight Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | './node_modules/angular/angular.js', 19 | './node_modules/angular-mocks/angular-mocks.js', 20 | './angular-password.js', 21 | 'tests/*.js' 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | ], 28 | 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: { 33 | }, 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | reporters: ['progress'], 40 | 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | 50 | // level of logging 51 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: true, 57 | 58 | 59 | // start these browsers 60 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 61 | browsers: ['PhantomJS'], 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, Karma captures browsers, runs the tests and exits 66 | singleRun: false, 67 | 68 | // Concurrency level 69 | // how many browser should be started simultaneous 70 | concurrency: Infinity 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-password", 3 | "version": "1.0.3", 4 | "description": "The most performant AngularJS directive for matching two password input fields ", 5 | "main": "angular-password.js", 6 | "keywords": [ 7 | "angular", 8 | "angularjs", 9 | "password", 10 | "match password", 11 | "confirm password", 12 | "passwords", 13 | "angular-password", 14 | "ngPassword", 15 | "gdi2290", 16 | "PatrickJS" 17 | ], 18 | "scripts": { 19 | "test": "gulp test" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/gdi2290/angular-password.git" 24 | }, 25 | "author": "gdi2290 ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/gdi2290/angular-password/issues" 29 | }, 30 | "homepage": "https://github.com/gdi2290/angular-password", 31 | "dependencies": { 32 | "angular": "^1.5.5" 33 | }, 34 | "devDependencies": { 35 | "angular-mocks": "^1.5.5", 36 | "gulp": "^4.0.0", 37 | "gulp-jshint": "^2.1.0", 38 | "gulp-rename": "^1.3.0", 39 | "gulp-uglify": "^3.0.0", 40 | "jasmine-core": "^2.4.1", 41 | "jshint": "^2.9.5", 42 | "karma": "^0.13.22", 43 | "karma-jasmine": "^0.3.8", 44 | "karma-phantomjs-launcher": "^1.0.0", 45 | "karma-spec-reporter": "0.0.32", 46 | "phantomjs-prebuilt": "^2.1.16" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/angular-password.spec.js: -------------------------------------------------------------------------------- 1 | describe('angular-password', function() { 2 | 3 | var $scope, form; 4 | 5 | beforeEach(module('ngPassword')); 6 | 7 | beforeEach(inject(function($compile, $rootScope) { 8 | $scope = $rootScope; 9 | 10 | var element = angular.element( 11 | '
' + 12 | '' + 13 | '' + 14 | '
' 15 | ); 16 | 17 | $compile(element)($scope); 18 | 19 | $scope.model = {}; 20 | form = $scope.form; 21 | })); 22 | 23 | 24 | describe('match-password', function() { 25 | 26 | // simulates initial page load with nothing in the models 27 | it('should pass with no password or confirm', function() { 28 | $scope.$digest(); 29 | 30 | expect($scope.model.password).toBeUndefined(); 31 | expect($scope.model.confirm).toBeUndefined(); 32 | expect(form.password.$valid).toBe(true); 33 | expect(form.confirm.$valid).toBe(true); 34 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 35 | }); 36 | 37 | // simulates typing something into password then deleting it 38 | it('should pass with empty password and no confirm', function() { 39 | form.password.$setViewValue(''); 40 | $scope.$digest(); 41 | 42 | expect($scope.model.password).toBe(''); 43 | expect($scope.model.confirm).toBeUndefined(); 44 | expect(form.password.$valid).toBe(true); 45 | expect(form.confirm.$valid).toBe(true); 46 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 47 | }); 48 | 49 | // simulates typing something into confirm then deleting it 50 | it('should pass with no password and empty confirm', function() { 51 | form.confirm.$setViewValue(''); 52 | $scope.$digest(); 53 | 54 | expect($scope.model.password).toBeUndefined(); 55 | expect($scope.model.confirm).toBe(''); 56 | expect(form.password.$valid).toBe(true); 57 | expect(form.confirm.$valid).toBe(true); 58 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 59 | }); 60 | 61 | // simulates typing into both fields and deleting them 62 | it('should pass with empty password and confirm', function() { 63 | form.password.$setViewValue(''); 64 | form.confirm.$setViewValue(''); 65 | $scope.$digest(); 66 | 67 | expect($scope.model.password).toBe(''); 68 | expect($scope.model.confirm).toBe(''); 69 | expect(form.password.$valid).toBe(true); 70 | expect(form.confirm.$valid).toBe(true); 71 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 72 | }); 73 | 74 | // simulates typing into password but not confirm 75 | it('should fail with password set and no confirm', function() { 76 | form.password.$setViewValue('abcdef'); 77 | $scope.$digest(); 78 | 79 | expect($scope.model.password).toBe('abcdef'); 80 | expect($scope.model.confirm).toBeUndefined(); 81 | expect(form.password.$valid).toBe(true); 82 | expect(form.confirm.$valid).toBe(false); 83 | expect(form.confirm.$error.passwordMatch).toBe(true); 84 | }); 85 | 86 | // simulates typing into both fields then deleting confirm 87 | it('should fail with password set and empty confirm', function() { 88 | form.password.$setViewValue('abcdef'); 89 | form.confirm.$setViewValue(''); 90 | $scope.$digest(); 91 | 92 | expect($scope.model.password).toBe('abcdef'); 93 | expect($scope.model.confirm).toBeUndefined(); 94 | expect(form.password.$valid).toBe(true); 95 | expect(form.confirm.$valid).toBe(false); 96 | expect(form.confirm.$error.passwordMatch).toBe(true); 97 | }); 98 | 99 | // simulates typing into both fields then deleting password 100 | it('should fail with confirm set and empty password', function() { 101 | form.password.$setViewValue(''); 102 | form.confirm.$setViewValue('abcdef'); 103 | $scope.$digest(); 104 | 105 | expect($scope.model.password).toBe(''); 106 | expect($scope.model.confirm).toBeUndefined(); 107 | expect(form.password.$valid).toBe(true); 108 | expect(form.confirm.$valid).toBe(false); 109 | expect(form.confirm.$error.passwordMatch).toBe(true); 110 | }); 111 | 112 | // simulates typing password, then confirm, not matching 113 | it('should fail with password set and then different confirm', function() { 114 | form.password.$setViewValue('abcdef'); 115 | form.confirm.$setViewValue('fedcba'); 116 | $scope.$digest(); 117 | 118 | expect($scope.model.password).toBe('abcdef'); 119 | expect($scope.model.confirm).toBeUndefined(); 120 | expect(form.password.$valid).toBe(true); 121 | expect(form.confirm.$valid).toBe(false); 122 | expect(form.confirm.$error.passwordMatch).toBe(true); 123 | }); 124 | 125 | // simulates typing confirm, then password, not matching 126 | it('should fail with confirm set and then different password', function() { 127 | form.confirm.$setViewValue('fedcba'); 128 | form.password.$setViewValue('abcdef'); 129 | $scope.$digest(); 130 | 131 | expect($scope.model.password).toBe('abcdef'); 132 | expect($scope.model.confirm).toBeUndefined(); 133 | expect(form.password.$valid).toBe(true); 134 | expect(form.confirm.$valid).toBe(false); 135 | expect(form.confirm.$error.passwordMatch).toBe(true); 136 | }); 137 | 138 | // simulates typing password, then confirm, then changing password 139 | it('should fail with both set same then password changed', function() { 140 | form.password.$setViewValue('abcdef'); 141 | form.confirm.$setViewValue('abcdef'); 142 | $scope.$digest(); 143 | 144 | form.password.$setViewValue('aaaaaa'); 145 | $scope.$digest(); 146 | 147 | expect($scope.model.password).toBe('aaaaaa'); 148 | expect($scope.model.confirm).toBeUndefined(); 149 | expect(form.password.$valid).toBe(true); 150 | expect(form.confirm.$valid).toBe(false); 151 | expect(form.confirm.$error.passwordMatch).toBe(true); 152 | }); 153 | 154 | // simulates typing confirm, then password, then changing confirm 155 | it('should fail with both set same then confirm changed', function() { 156 | form.confirm.$setViewValue('abcdef'); 157 | form.password.$setViewValue('abcdef'); 158 | $scope.$digest(); 159 | 160 | form.confirm.$setViewValue('aaaaaa'); 161 | $scope.$digest(); 162 | 163 | expect($scope.model.password).toBe('abcdef'); 164 | expect($scope.model.confirm).toBeUndefined(); 165 | expect(form.password.$valid).toBe(true); 166 | expect(form.confirm.$valid).toBe(false); 167 | expect(form.confirm.$error.passwordMatch).toBe(true); 168 | }); 169 | 170 | // simulates typing password, then confirm, not matching, then changing password to match 171 | it('should pass with both set differently then password changed to match', function() { 172 | form.password.$setViewValue('aaaaaa'); 173 | form.confirm.$setViewValue('abcdef'); 174 | $scope.$digest(); 175 | 176 | form.password.$setViewValue('abcdef'); 177 | $scope.$digest(); 178 | 179 | expect($scope.model.password).toBe('abcdef'); 180 | expect($scope.model.confirm).toBe('abcdef'); 181 | expect(form.password.$valid).toBe(true); 182 | expect(form.confirm.$valid).toBe(true); 183 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 184 | }); 185 | 186 | // simulates typing confirm, then password, not matching, then changing confirm to match 187 | it('should pass with both set differently then confirm changed to match', function() { 188 | form.confirm.$setViewValue('abcdef'); 189 | form.password.$setViewValue('aaaaaa'); 190 | $scope.$digest(); 191 | 192 | form.confirm.$setViewValue('aaaaaa'); 193 | $scope.$digest(); 194 | 195 | expect($scope.model.password).toBe('aaaaaa'); 196 | expect($scope.model.confirm).toBe('aaaaaa'); 197 | expect(form.password.$valid).toBe(true); 198 | expect(form.confirm.$valid).toBe(true); 199 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 200 | }); 201 | 202 | // simulates typing password, then confirm, matching 203 | it('should pass with password then confirm matching', function() { 204 | form.password.$setViewValue('abcdef'); 205 | form.confirm.$setViewValue('abcdef'); 206 | $scope.$digest(); 207 | 208 | expect($scope.model.password).toBe('abcdef'); 209 | expect($scope.model.confirm).toBe('abcdef'); 210 | expect(form.password.$valid).toBe(true); 211 | expect(form.confirm.$valid).toBe(true); 212 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 213 | }); 214 | 215 | // simulates typing confirm, then password, matching 216 | it('should pass with password then confirm matching', function() { 217 | form.confirm.$setViewValue('abcdef'); 218 | form.password.$setViewValue('abcdef'); 219 | $scope.$digest(); 220 | 221 | expect($scope.model.password).toBe('abcdef'); 222 | expect($scope.model.confirm).toBe('abcdef'); 223 | expect(form.password.$valid).toBe(true); 224 | expect(form.confirm.$valid).toBe(true); 225 | expect(form.confirm.$error.passwordMatch).toBeUndefined(); 226 | }); 227 | 228 | }); 229 | }); 230 | --------------------------------------------------------------------------------