├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── Makefile ├── app ├── components ├── index.html ├── scripts │ ├── app.js │ └── controllers │ │ └── main-ctrl.js ├── styles │ └── main.css └── views │ └── main.html ├── bower.json ├── dist ├── angular-flash.js └── angular-flash.min.js ├── karma.conf.js ├── package.json ├── readme.md ├── src ├── .jshintrc ├── directives │ └── flash-alert-directive.js └── services │ └── flash-service.js └── test ├── .jshintrc └── unit ├── directives └── flash-alert-directive-spec.js └── services └── flash-service-spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.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 = 4 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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.js text 7 | *.html text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | bower_components 4 | libpeerconnection.log 5 | reports 6 | 7 | *.swp 8 | *.swo 9 | 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "jquery": false, 5 | "es5": true, 6 | "bitwise": true, 7 | "camelcase": false, 8 | "latedef": true, 9 | "boss": false, 10 | "curly": true, 11 | "debug": false, 12 | "devel": false, 13 | "eqeqeq": true, 14 | "evil": true, 15 | "forin": true, 16 | "immed": true, 17 | "laxbreak": false, 18 | "newcap": true, 19 | "noarg": true, 20 | "noempty": true, 21 | "nonew": true, 22 | "nomen": false, 23 | "onevar": false, 24 | "plusplus": true, 25 | "regexp": false, 26 | "undef": true, 27 | "sub": true, 28 | "strict": true, 29 | "white": false, 30 | "unused": true, 31 | "smarttabs": false, 32 | "indent": 4, 33 | "quotmark": "single" 34 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install grunt-cli -g 6 | - npm install bower -g 7 | - bower install 8 | after_script: 9 | - npm run coveralls 10 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var mountFolder = function (connect, dir) { 3 | return connect.static(require('path').resolve(dir)); 4 | }; 5 | 6 | // configurable paths 7 | var yeomanConfig = { 8 | app: 'app', 9 | src: 'src', 10 | dist: 'dist' 11 | }; 12 | 13 | try { 14 | yeomanConfig.app = require('./bower.json').appPath || yeomanConfig.app; 15 | } catch (e) { 16 | } 17 | 18 | module.exports = function (grunt) { 19 | 20 | // Project configuration. 21 | grunt.initConfig({ 22 | yeoman: yeomanConfig, 23 | pkg: grunt.file.readJSON('package.json'), 24 | lifecycle: { 25 | validate: [ 26 | 'jshint' 27 | ], 28 | compile: [], 29 | test: [ 30 | 'karma:phantom' 31 | ], 32 | 'package': [ 33 | 'concat', 34 | 'uglify' 35 | ], 36 | 'integration-test': [], 37 | verify: [], 38 | install: [], 39 | deploy: [] 40 | }, 41 | jshint: { 42 | src: { 43 | options: { 44 | jshintrc: 'src/.jshintrc' 45 | }, 46 | src: ['src/**/*.js'] 47 | }, 48 | test: { 49 | options: { 50 | jshintrc: 'test/.jshintrc' 51 | }, 52 | src: ['test/unit/*.js'] 53 | }, 54 | grunt: { 55 | options: { 56 | jshintrc: '.jshintrc' 57 | }, 58 | src: ['Gruntfile.js'] 59 | } 60 | }, 61 | karma: { 62 | options: { 63 | configFile: 'karma.conf.js' 64 | }, 65 | unit: { 66 | singleRun: true 67 | }, 68 | phantom: { 69 | singleRun: true, 70 | browsers: ['PhantomJS'] 71 | }, 72 | debug: { 73 | singleRun: false, 74 | reporters: ['progress', 'junit'] 75 | } 76 | }, 77 | concat: { 78 | options: { 79 | banner: ['/**! ', 80 | ' * @license <%= pkg.name %> v<%= pkg.version %>', 81 | ' * Copyright (c) 2013 <%= pkg.author.name %>. <%= pkg.homepage %>', 82 | ' * License: MIT', 83 | ' */\n'].join('\n') 84 | }, 85 | main: { 86 | src: [ 87 | 'src/services/flash-service.js', 88 | 'src/directives/flash-alert-directive.js' 89 | ], 90 | dest: 'dist/<%= pkg.name %>.js' 91 | } 92 | }, 93 | uglify: { 94 | options: { 95 | banner: ['/**! ', 96 | ' * @license <%= pkg.name %> v<%= pkg.version %>', 97 | ' * Copyright (c) 2013 <%= pkg.author.name %>. <%= pkg.homepage %>', 98 | ' * License: MIT', 99 | ' */\n'].join('\n') 100 | }, 101 | main: { 102 | files: { 103 | 'dist/<%= pkg.name %>.min.js': [ 104 | '<%= concat.main.dest %>' 105 | ] 106 | } 107 | } 108 | }, 109 | watch: { 110 | scripts: { 111 | files: ['src/**/*.js'], 112 | tasks: ['phase-package'] 113 | }, 114 | livereload: { 115 | options: { 116 | livereload: true 117 | }, 118 | files: [ 119 | '<%= yeoman.app %>/*.html', 120 | '<%= yeoman.app %>/scrips/*.js', 121 | '<%= yeoman.app %>/scrips/**/*.js', 122 | '<%= yeoman.dist %>/*.js' 123 | ] 124 | 125 | } 126 | }, 127 | bumpup: ['package.json', 'bower.json'], 128 | connect: { 129 | options: { 130 | port: 9000, 131 | // Change this to '0.0.0.0' to access the server from outside. 132 | hostname: 'localhost' 133 | }, 134 | livereload: { 135 | options: { 136 | middleware: function (connect) { 137 | return [ 138 | require('connect-livereload')(), 139 | mountFolder(connect, yeomanConfig.dist), 140 | mountFolder(connect, yeomanConfig.app), 141 | mountFolder(connect, yeomanConfig.src), 142 | mountFolder(connect, 'test') 143 | ]; 144 | } 145 | } 146 | } 147 | }, 148 | open: { 149 | server: { 150 | url: 'http://localhost:<%= connect.options.port %>' 151 | } 152 | } 153 | }); 154 | 155 | // load all grunt tasks 156 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 157 | 158 | grunt.registerTask('bump', function (type) { 159 | type = type ? type : 'patch'; 160 | grunt.task.run('bumpup:' + type); 161 | }); 162 | 163 | grunt.registerTask('server', [ 164 | 'package', 165 | 'connect:livereload', 166 | 'open', 167 | 'watch' 168 | ]); 169 | 170 | grunt.registerTask('test-phantom', ['karma:phantom']); 171 | grunt.registerTask('test-start', ['karma:debug:start']); 172 | grunt.registerTask('test-run', ['karma:debug:run']); 173 | grunt.registerTask('build', ['install']); 174 | grunt.registerTask('default', ['install']); 175 | 176 | }; 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 William L. Bunselmeyer. https://github.com/wmluke/angular-blocks 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | install: 3 | npm install # Install node modules 4 | bower install # Install bower components 5 | grunt install # Build & test client app -------------------------------------------------------------------------------- /app/components: -------------------------------------------------------------------------------- 1 | ../bower_components -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | {{flash.type}} 41 | {{flash.message}} 42 | 43 |

This alert:

44 |
    45 |
  • Shows all alerts
  • 46 |
  • Does not fade away
  • 47 |
  • Has a close button
  • 48 |
  • Works outside of controller scope
  • 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
60 | 61 | 62 | {{flash.type}} 63 | {{flash.message}} 64 |

Alert #1

65 |
66 | 67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular.module('App', ['ngRoute', 'angular-flash.flash-alert-directive', 'App.main-ctrl']) 5 | .config(function ($routeProvider, $locationProvider) { 6 | $routeProvider 7 | .when('/', { 8 | templateUrl: 'views/main.html', 9 | controller: 'MainCtrl' 10 | }) 11 | .otherwise({ 12 | redirectTo: '/' 13 | }); 14 | $locationProvider.html5Mode(true); 15 | }) 16 | .config(function (flashProvider) { 17 | // Support bootstrap 3.0 "alert-danger" class with error flash types 18 | flashProvider.errorClassnames.push('alert-danger'); 19 | 20 | /** 21 | * Also have... 22 | * 23 | * flashProvider.warnClassnames 24 | * flashProvider.infoClassnames 25 | * flashProvider.successClassnames 26 | */ 27 | 28 | }) 29 | .run(); 30 | }()); 31 | -------------------------------------------------------------------------------- /app/scripts/controllers/main-ctrl.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var MainCtrl = function ($scope, flash) { 5 | 6 | 7 | $scope.all = function () { 8 | $scope.info(); 9 | $scope.warn(); 10 | $scope.success(); 11 | $scope.error(); 12 | }; 13 | 14 | $scope.info = function () { 15 | flash.info = 'info message'; 16 | }; 17 | 18 | $scope.info1 = function () { 19 | flash.to('alert-1').info = 'info message'; 20 | }; 21 | 22 | $scope.warn = function () { 23 | flash.warn = 'warn message'; 24 | }; 25 | 26 | $scope.warn1 = function () { 27 | flash.to('alert-1').warn = 'warn message'; 28 | }; 29 | 30 | $scope.success = function () { 31 | flash.success = 'success message'; 32 | }; 33 | 34 | $scope.success1 = function () { 35 | flash.to('alert-1').success = 'success message'; 36 | }; 37 | 38 | $scope.error = function () { 39 | flash.error = 'error message'; 40 | }; 41 | 42 | $scope.error1 = function () { 43 | flash.to('alert-1').error = 'error message'; 44 | }; 45 | 46 | $scope.dismissAlert1 = function () { 47 | flash.to('alert-1').error = false; 48 | }; 49 | 50 | $scope.all(); 51 | }; 52 | 53 | angular.module('App.main-ctrl', []) 54 | .controller('MainCtrl', ['$scope', 'flash', MainCtrl]); 55 | }()); 56 | -------------------------------------------------------------------------------- /app/styles/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding-top: 60px; 4 | } 5 | 6 | .alert-flash { 7 | padding: 8px 35px 8px 14px; 8 | margin-bottom: 20px; 9 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 10 | border: 1px solid transparent; 11 | -webkit-border-radius: 4px; 12 | -moz-border-radius: 4px; 13 | border-radius: 4px; 14 | 15 | -webkit-transition-property: all; 16 | transition-property: all; 17 | 18 | } 19 | 20 | .alert-flash h4 { 21 | margin: 0; 22 | } 23 | 24 | .alert-flash .close { 25 | position: relative; 26 | top: -2px; 27 | right: -21px; 28 | line-height: 20px; 29 | } 30 | 31 | .alert-warn { 32 | background-color: #fcf8e3; 33 | border: 1px solid #fbeed5; 34 | } 35 | 36 | .alert-warn, 37 | .alert-warn h4 { 38 | color: #c09853; 39 | } 40 | -------------------------------------------------------------------------------- /app/views/main.html: -------------------------------------------------------------------------------- 1 | 36 | 37 |
38 |
39 |
40 | 41 | Ahem... 42 | {{flash.message}} 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | 51 | Not so good. 52 | {{flash.message}} 53 |
54 |
55 |
56 | 57 |
58 |
59 |
60 | 61 | Nice work! 62 | {{flash.message}} 63 |
64 |
65 |
66 | 67 |
68 |
69 |
70 | 71 | Yikes! 72 | {{flash.message}} 73 |
74 |
75 |
76 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-flash", 3 | "description": "Flash messages for Angular Js", 4 | "version": "0.1.14", 5 | "main": [ 6 | "dist/angular-flash.js" 7 | ], 8 | "ignore": [ 9 | "app", 10 | "bower_components", 11 | "test", 12 | ".jshintrc", 13 | "src/.jshintrc", 14 | ".travis.yml", 15 | "karma.conf.js", 16 | "Gruntfile.js" 17 | ], 18 | "dependencies": { 19 | "angular": "1.0 - 1.3" 20 | }, 21 | "devDependencies": { 22 | "jquery": "~2.0.0", 23 | "angular-mocks": "1.3.2", 24 | "angular-scenario": "1.3.2", 25 | "angular-route": "1.3.2", 26 | "json3": "~3.2.4", 27 | "es5-shim": "~2.0.8", 28 | "bootstrap": "~2.3.2", 29 | "font-awesome": "~3.2.1" 30 | } 31 | } -------------------------------------------------------------------------------- /dist/angular-flash.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * @license angular-flash v0.1.14 3 | * Copyright (c) 2013 William L. Bunselmeyer. https://github.com/wmluke/angular-flash 4 | * License: MIT 5 | */ 6 | /* global angular */ 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | var subscriberCount = 0; 12 | 13 | var Flash = function (options) { 14 | var _options = angular.extend({ 15 | id: null, 16 | subscribers: {}, 17 | classnames: { 18 | error: [], 19 | warn: [], 20 | info: [], 21 | success: [] 22 | } 23 | }, options); 24 | 25 | var _self = this; 26 | var _success; 27 | var _info; 28 | var _warn; 29 | var _error; 30 | var _type; 31 | 32 | function _notify(type, message) { 33 | angular.forEach(_options.subscribers, function (subscriber) { 34 | var matchesType = !subscriber.type || subscriber.type === type; 35 | var matchesId = (!_options.id && !subscriber.id) || subscriber.id === _options.id; 36 | if (matchesType && matchesId) { 37 | subscriber.cb(message, type); 38 | } 39 | }); 40 | } 41 | 42 | this.clean = function () { 43 | _success = null; 44 | _info = null; 45 | _warn = null; 46 | _error = null; 47 | _type = null; 48 | }; 49 | 50 | this.subscribe = function (subscriber, type, id) { 51 | subscriberCount += 1; 52 | _options.subscribers[subscriberCount] = { 53 | cb: subscriber, 54 | type: type, 55 | id: id 56 | }; 57 | return subscriberCount; 58 | }; 59 | 60 | this.unsubscribe = function (handle) { 61 | delete _options.subscribers[handle]; 62 | }; 63 | 64 | this.to = function (id) { 65 | var options = angular.copy(_options); 66 | options.id = id; 67 | return new Flash(options); 68 | }; 69 | 70 | Object.defineProperty(this, 'success', { 71 | get: function () { 72 | return _success; 73 | }, 74 | set: function (message) { 75 | _success = message; 76 | _type = 'success'; 77 | _notify(_type, message); 78 | } 79 | }); 80 | 81 | Object.defineProperty(this, 'info', { 82 | get: function () { 83 | return _info; 84 | }, 85 | set: function (message) { 86 | _info = message; 87 | _type = 'info'; 88 | _notify(_type, message); 89 | } 90 | }); 91 | 92 | Object.defineProperty(this, 'warn', { 93 | get: function () { 94 | return _warn; 95 | }, 96 | set: function (message) { 97 | _warn = message; 98 | _type = 'warn'; 99 | _notify(_type, message); 100 | } 101 | }); 102 | 103 | Object.defineProperty(this, 'error', { 104 | get: function () { 105 | return _error; 106 | }, 107 | set: function (message) { 108 | _error = message; 109 | _type = 'error'; 110 | _notify(_type, message); 111 | } 112 | }); 113 | 114 | Object.defineProperty(this, 'type', { 115 | get: function () { 116 | return _type; 117 | } 118 | }); 119 | 120 | Object.defineProperty(this, 'message', { 121 | get: function () { 122 | return _type ? _self[_type] : null; 123 | } 124 | }); 125 | 126 | Object.defineProperty(this, 'classnames', { 127 | get: function () { 128 | return _options.classnames; 129 | } 130 | }); 131 | 132 | Object.defineProperty(this, 'id', { 133 | get: function () { 134 | return _options.id; 135 | } 136 | }); 137 | }; 138 | 139 | angular.module('angular-flash.service', []) 140 | .provider('flash', function () { 141 | var _self = this; 142 | this.errorClassnames = ['alert-error']; 143 | this.warnClassnames = ['alert-warn']; 144 | this.infoClassnames = ['alert-info']; 145 | this.successClassnames = ['alert-success']; 146 | 147 | this.$get = function () { 148 | return new Flash({ 149 | classnames: { 150 | error: _self.errorClassnames, 151 | warn: _self.warnClassnames, 152 | info: _self.infoClassnames, 153 | success: _self.successClassnames 154 | } 155 | }); 156 | }; 157 | }); 158 | 159 | }()); 160 | 161 | /* global angular */ 162 | 163 | (function () { 164 | 'use strict'; 165 | 166 | function isBlank(str) { 167 | if (str === null || str === undefined) { 168 | str = ''; 169 | } 170 | return (/^\s*$/).test(str); 171 | } 172 | 173 | function flashAlertDirective(flash, $timeout) { 174 | return { 175 | scope: true, 176 | link: function ($scope, element, attr) { 177 | var timeoutHandle, subscribeHandle; 178 | 179 | $scope.flash = {}; 180 | 181 | $scope.hide = function () { 182 | removeAlertClasses(); 183 | if (!isBlank(attr.activeClass)) { 184 | element.removeClass(attr.activeClass); 185 | } 186 | }; 187 | 188 | $scope.$on('$destroy', function () { 189 | flash.clean(); 190 | flash.unsubscribe(subscribeHandle); 191 | }); 192 | 193 | function removeAlertClasses() { 194 | var classnames = [].concat(flash.classnames.error, flash.classnames.warn, flash.classnames.info, flash.classnames.success); 195 | angular.forEach(classnames, function (clazz) { 196 | element.removeClass(clazz); 197 | }); 198 | } 199 | 200 | function show(message, type) { 201 | if (timeoutHandle) { 202 | $timeout.cancel(timeoutHandle); 203 | } 204 | 205 | $scope.flash.type = type; 206 | $scope.flash.message = message; 207 | removeAlertClasses(); 208 | angular.forEach(flash.classnames[type], function (clazz) { 209 | element.addClass(clazz); 210 | }); 211 | 212 | if (!isBlank(attr.activeClass)) { 213 | element.addClass(attr.activeClass); 214 | } 215 | 216 | if (!message) { 217 | $scope.hide(); 218 | return; 219 | } 220 | 221 | var delay = Number(attr.duration || 5000); 222 | if (delay > 0) { 223 | timeoutHandle = $timeout($scope.hide, delay); 224 | } 225 | } 226 | 227 | subscribeHandle = flash.subscribe(show, attr.flashAlert, attr.id); 228 | 229 | /** 230 | * Fixes timing issues: display the last flash message sent before this directive subscribed. 231 | */ 232 | 233 | if (attr.flashAlert && flash[attr.flashAlert]) { 234 | show(flash[attr.flashAlert], attr.flashAlert); 235 | } 236 | 237 | if (!attr.flashAlert && flash.message) { 238 | show(flash.message, flash.type); 239 | } 240 | 241 | } 242 | }; 243 | } 244 | 245 | angular.module('angular-flash.flash-alert-directive', ['angular-flash.service']) 246 | .directive('flashAlert', ['flash', '$timeout', flashAlertDirective]); 247 | 248 | }()); 249 | -------------------------------------------------------------------------------- /dist/angular-flash.min.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * @license angular-flash v0.1.14 3 | * Copyright (c) 2013 William L. Bunselmeyer. https://github.com/wmluke/angular-flash 4 | * License: MIT 5 | */ 6 | !function(){"use strict";var a=0,b=function(c){function d(a,b){angular.forEach(j.subscribers,function(c){var d=!c.type||c.type===a,e=!j.id&&!c.id||c.id===j.id;d&&e&&c.cb(b,a)})}var e,f,g,h,i,j=angular.extend({id:null,subscribers:{},classnames:{error:[],warn:[],info:[],success:[]}},c),k=this;this.clean=function(){e=null,f=null,g=null,h=null,i=null},this.subscribe=function(b,c,d){return a+=1,j.subscribers[a]={cb:b,type:c,id:d},a},this.unsubscribe=function(a){delete j.subscribers[a]},this.to=function(a){var c=angular.copy(j);return c.id=a,new b(c)},Object.defineProperty(this,"success",{get:function(){return e},set:function(a){e=a,i="success",d(i,a)}}),Object.defineProperty(this,"info",{get:function(){return f},set:function(a){f=a,i="info",d(i,a)}}),Object.defineProperty(this,"warn",{get:function(){return g},set:function(a){g=a,i="warn",d(i,a)}}),Object.defineProperty(this,"error",{get:function(){return h},set:function(a){h=a,i="error",d(i,a)}}),Object.defineProperty(this,"type",{get:function(){return i}}),Object.defineProperty(this,"message",{get:function(){return i?k[i]:null}}),Object.defineProperty(this,"classnames",{get:function(){return j.classnames}}),Object.defineProperty(this,"id",{get:function(){return j.id}})};angular.module("angular-flash.service",[]).provider("flash",function(){var a=this;this.errorClassnames=["alert-error"],this.warnClassnames=["alert-warn"],this.infoClassnames=["alert-info"],this.successClassnames=["alert-success"],this.$get=function(){return new b({classnames:{error:a.errorClassnames,warn:a.warnClassnames,info:a.infoClassnames,success:a.successClassnames}})}})}(),function(){"use strict";function a(a){return(null===a||void 0===a)&&(a=""),/^\s*$/.test(a)}function b(b,c){return{scope:!0,link:function(d,e,f){function g(){var a=[].concat(b.classnames.error,b.classnames.warn,b.classnames.info,b.classnames.success);angular.forEach(a,function(a){e.removeClass(a)})}function h(h,j){if(i&&c.cancel(i),d.flash.type=j,d.flash.message=h,g(),angular.forEach(b.classnames[j],function(a){e.addClass(a)}),a(f.activeClass)||e.addClass(f.activeClass),!h)return void d.hide();var k=Number(f.duration||5e3);k>0&&(i=c(d.hide,k))}var i,j;d.flash={},d.hide=function(){g(),a(f.activeClass)||e.removeClass(f.activeClass)},d.$on("$destroy",function(){b.clean(),b.unsubscribe(j)}),j=b.subscribe(h,f.flashAlert,f.id),f.flashAlert&&b[f.flashAlert]&&h(b[f.flashAlert],f.flashAlert),!f.flashAlert&&b.message&&h(b.message,b.type)}}}angular.module("angular-flash.flash-alert-directive",["angular-flash.service"]).directive("flashAlert",["flash","$timeout",b])}(); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Aug 09 2013 09:44:05 GMT-0700 (PDT) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | // frameworks to use 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'bower_components/jquery/jquery.js', 16 | 'bower_components/angular/angular.js', 17 | 'bower_components/angular-mocks/angular-mocks.js', 18 | 'src/**/*.js', 19 | 'test/unit/**/*-spec.js' 20 | ], 21 | 22 | // list of files to exclude 23 | exclude: [], 24 | 25 | // test results reporter to use 26 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 27 | reporters: ['progress', 'junit', 'coverage'], 28 | 29 | preprocessors: { 30 | // source files, that you wanna generate coverage for 31 | // do not include tests or libraries 32 | // (these files will be instrumented by Istanbul) 33 | 'src/**/*.js': ['coverage'] 34 | }, 35 | 36 | coverageReporter: { 37 | type: 'lcov', // lcov format supported by Coveralls 38 | dir: 'reports/coverage' 39 | }, 40 | 41 | junitReporter: { 42 | outputFile: "reports/test/unit-test-results.xml" 43 | }, 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | runnerPort: 9100, 49 | 50 | // enable / disable colors in the output (reporters and logs) 51 | colors: true, 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: false, 59 | 60 | // Start these browsers, currently available: 61 | // - Chrome 62 | // - ChromeCanary 63 | // - Firefox 64 | // - Opera 65 | // - Safari (only Mac) 66 | // - PhantomJS 67 | // - IE (only Windows) 68 | browsers: ['Chrome'], 69 | 70 | // If browser does not capture in given timeout [ms], kill it 71 | captureTimeout: 60000, 72 | 73 | // Continuous Integration mode 74 | // if true, it capture browsers, run tests and exit 75 | singleRun: false 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-flash", 3 | "description": "Flash messages for Angular JS", 4 | "private": false, 5 | "version": "0.1.14", 6 | "homepage": "https://github.com/wmluke/angular-flash", 7 | "author": { 8 | "name": "William L. Bunselmeyer", 9 | "email": "wmlukeb@gmail.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/wmluke/angular-flash.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/wmluke/angular-flash/issues" 17 | }, 18 | "engines": { 19 | "node": ">= 0.8.0", 20 | "npm": "1.1.x" 21 | }, 22 | "scripts": { 23 | "test": "grunt test-phantom", 24 | "start": "node app", 25 | "coveralls": "cat ./reports/coverage/*/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 26 | }, 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "connect-livereload": "~0.4.0", 30 | "coveralls": "~2.10.0", 31 | "underscore.string": "~2.3.1", 32 | "grunt": "~0.4.1", 33 | "chai": "~1.5.0", 34 | "istanbul": "~0.1.34", 35 | "sinon-chai": "~2.3.1", 36 | "sinon": "~1.6.0", 37 | "jshint": "~2.1.4", 38 | "matchdep": "~0.1.2", 39 | "grunt-contrib-jshint": "~0.1.1", 40 | "grunt-contrib-uglify": "~0.2.0", 41 | "grunt-bumpup": "~0.2.0", 42 | "grunt-contrib-watch": "~0.5.1", 43 | "grunt-exec": "~0.4.5", 44 | "grunt-contrib-concat": "~0.3.0", 45 | "grunt-build-lifecycle": "~0.1.1", 46 | "grunt-open": "~0.2.0", 47 | "grunt-contrib-connect": "~0.7.1", 48 | "grunt-karma": "0.8.2", 49 | "karma": "~0.12.9", 50 | "karma-ng-scenario": "~0.1.0", 51 | "karma-junit-reporter": "~0.2.2", 52 | "karma-coverage": "~0.2.1", 53 | "karma-jasmine": "~0.1.5", 54 | "karma-phantomjs-launcher": "~0.1.4", 55 | "karma-chrome-launcher": "~0.1.2" 56 | }, 57 | "keywords": [ 58 | "angular", 59 | "flash" 60 | ] 61 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # angular-flash 2 | 3 | [![Build Status](https://travis-ci.org/wmluke/angular-flash.png?branch=master)](https://travis-ci.org/wmluke/angular-flash) 4 | [![Coverage Status](https://coveralls.io/repos/wmluke/angular-flash/badge.png?branch=master)](https://coveralls.io/r/wmluke/angular-flash?branch=master) 5 | 6 | A flash service and directive for setting and displaying flash messages in [Angular JS](http://angularjs.org). Specifically, the flash service is a publisher of flash messages and the flash directive is a subscriber to flash messages. The flash directive leverages the Twitter Bootstrap Alert component. 7 | 8 | ## Installation 9 | 10 | Download [angular-flash.min.js](https://github.com/wmluke/angular-flash/blob/master/dist/angular-flash.min.js) or install with bower. 11 | 12 | ```bash 13 | $ bower install angular-flash --save 14 | ``` 15 | 16 | Load the `angular-flash.service` and the `angular-flash.flash-alert-directive` modules in your app. 17 | 18 | ```javascript 19 | angular.module('app', ['angular-flash.service', 'angular-flash.flash-alert-directive']); 20 | ``` 21 | 22 | ## Configure 23 | 24 | ```javascript 25 | angular.module('app', ['angular-flash.service', 'angular-flash.flash-alert-directive']) 26 | .config(function (flashProvider) { 27 | 28 | // Support bootstrap 3.0 "alert-danger" class with error flash types 29 | flashProvider.errorClassnames.push('alert-danger'); 30 | 31 | /** 32 | * Also have... 33 | * 34 | * flashProvider.warnClassnames 35 | * flashProvider.infoClassnames 36 | * flashProvider.successClassnames 37 | */ 38 | 39 | }) 40 | ``` 41 | 42 | ## Usage 43 | 44 | Use the `flash` service to publish a flash messages... 45 | 46 | ```javascript 47 | 48 | var FooController = function(flash){ 49 | // Publish a success flash 50 | flash.success = 'Do it live!'; 51 | 52 | // Publish a error flash 53 | flash.error = 'Fail!'; 54 | 55 | // Publish an info flash to the `alert-1` subscriber 56 | flash.to('alert-1').info = 'Only for alert 1'; 57 | 58 | // The `flash-alert` directive hides itself when if receives falsey flash messages of any type 59 | flash.error = ''; 60 | 61 | }; 62 | 63 | FooController.$inject = ['flash']; 64 | 65 | ``` 66 | 67 | Use the `flash-alert` directive to subscribe to flash messages... 68 | 69 | ```html 70 | 71 |
72 | Congrats! 73 | {{flash.message}} 74 |
75 | 76 | 77 |
78 | Boo! 79 | {{flash.message}} 80 |
81 | 82 | 83 |
84 | Boo! 85 | {{flash.message}} 86 |
87 | 88 | 89 |
90 | Boo! 91 | {{flash.message}} 92 |
93 | 94 | 95 |
96 | 97 | 98 | Boo! 99 | {{flash.message}} 100 |
101 | ``` 102 | 103 | When a flash message is published, the `flash-alert` directive will add a class of the form `alert-` and also add classes specified in `active-class`. Then after 5 seconds it will remove them. 104 | 105 | The example above leverages Twitter Bootstrap CSS3 transitions: `fade` and `in`. 106 | 107 | ### Styling Considerations 108 | 109 | Bootstrap 2 has a few styling quirks with the `.alert` and `.fade` classes. 110 | 111 | #### Visible or not 112 | 113 | Some folks may want hidden alerts to take up visible space others may not. Fortunately, each case is easy to achieve by declaring `.alert` as indicated below... 114 | 115 | Takes up no visible space when hidden 116 | ```html 117 |
118 | ... 119 |
120 | ``` 121 | 122 | Takes up visible space when hidden 123 | ```html 124 |
125 | ... 126 |
127 | ``` 128 | 129 | #### CSS Transition Quirks 130 | 131 | The `.fade` class only transitions opacity and the base `.alert` class has a background color and background border used for alert warnings. Together these styling attributes can make it challenging to achieve smooth transitions. 132 | 133 | Fortunately, its easy to replace the `.alert` class and move the warning colors to `.alert-warn` as illustrated below... 134 | 135 | Styling 136 | ```css 137 | /* Remove colors and add transition property */ 138 | .alert-flash { 139 | padding: 8px 35px 8px 14px; 140 | margin-bottom: 20px; 141 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 142 | border: 1px solid transparent; 143 | -webkit-border-radius: 4px; 144 | -moz-border-radius: 4px; 145 | border-radius: 4px; 146 | 147 | /* change transition property to all */ 148 | -webkit-transition-property: all; 149 | transition-property: all; 150 | } 151 | 152 | .alert-flash h4 { 153 | margin: 0; 154 | } 155 | 156 | .alert-flash .close { 157 | position: relative; 158 | top: -2px; 159 | right: -21px; 160 | line-height: 20px; 161 | } 162 | 163 | /* add warning colors to warn class */ 164 | .alert-warn { 165 | background-color: #fcf8e3; 166 | border: 1px solid #fbeed5; 167 | } 168 | 169 | .alert-warn, 170 | .alert-warn h4 { 171 | color: #c09853; 172 | } 173 | ``` 174 | 175 | Template: 176 | ```html 177 |
178 | 179 | Ahem... 180 | {{flash.message}} 181 |
182 | ``` 183 | 184 | ### FlashProvider API 185 | 186 | ```javascript 187 | flashProvider.errorClassnames 188 | flashProvider.warnClassnames 189 | flashProvider.infoClassnames 190 | flashProvider.successClassnames 191 | ``` 192 | 193 | ### Flash Service API 194 | 195 | #### Properties 196 | Set and get flash messages with the following flash properties... 197 | 198 | * success 199 | * info 200 | * warn 201 | * error 202 | 203 | #### Methods 204 | 205 | ##### subscribe(listener, [type]) 206 | Register a subscriber callback function to be notified of flash messages. The subscriber function has two arguments: `message` and `type`. 207 | 208 | ##### clean() 209 | Clear all subscribers and flash messages. 210 | 211 | ## Contributing 212 | 213 | ### Prerequisites 214 | 215 | The project requires [Bower](http://bower.io), [Grunt](http://gruntjs.com), and [PhantomJS](http://phantomjs.org). Once you have installed them, you can build, test, and run the project. 216 | 217 | ### Build & Test 218 | 219 | To build and run tests, run either... 220 | 221 | ```bash 222 | $ make install 223 | ``` 224 | 225 | or 226 | 227 | ```bash 228 | $ npm install 229 | $ bower install 230 | $ grunt install 231 | ``` 232 | 233 | ### Demo & Develop 234 | 235 | To run a live demo or do some hackery, run... 236 | 237 | ```bash 238 | $ grunt server 239 | ``` 240 | -------------------------------------------------------------------------------- /src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": false, 3 | "browser": true, 4 | "jquery": true, 5 | "es5": true, 6 | "bitwise": true, 7 | "camelcase": false, 8 | "latedef": true, 9 | "boss": false, 10 | "curly": true, 11 | "debug": false, 12 | "devel": false, 13 | "eqeqeq": true, 14 | "evil": true, 15 | "forin": true, 16 | "immed": true, 17 | "laxbreak": false, 18 | "newcap": true, 19 | "noarg": true, 20 | "noempty": true, 21 | "nonew": true, 22 | "nomen": false, 23 | "onevar": false, 24 | "plusplus": true, 25 | "regexp": false, 26 | "undef": true, 27 | "sub": true, 28 | "strict": true, 29 | "white": false, 30 | "unused": true, 31 | "smarttabs": false, 32 | "indent": 4, 33 | "quotmark": "single", 34 | "globals": { 35 | "angular": true 36 | } 37 | } -------------------------------------------------------------------------------- /src/directives/flash-alert-directive.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | function isBlank(str) { 7 | if (str === null || str === undefined) { 8 | str = ''; 9 | } 10 | return (/^\s*$/).test(str); 11 | } 12 | 13 | function flashAlertDirective(flash, $timeout) { 14 | return { 15 | scope: true, 16 | link: function ($scope, element, attr) { 17 | var timeoutHandle, subscribeHandle; 18 | 19 | $scope.flash = {}; 20 | 21 | $scope.hide = function () { 22 | removeAlertClasses(); 23 | if (!isBlank(attr.activeClass)) { 24 | element.removeClass(attr.activeClass); 25 | } 26 | }; 27 | 28 | $scope.$on('$destroy', function () { 29 | flash.clean(); 30 | flash.unsubscribe(subscribeHandle); 31 | }); 32 | 33 | function removeAlertClasses() { 34 | var classnames = [].concat(flash.classnames.error, flash.classnames.warn, flash.classnames.info, flash.classnames.success); 35 | angular.forEach(classnames, function (clazz) { 36 | element.removeClass(clazz); 37 | }); 38 | } 39 | 40 | function show(message, type) { 41 | if (timeoutHandle) { 42 | $timeout.cancel(timeoutHandle); 43 | } 44 | 45 | $scope.flash.type = type; 46 | $scope.flash.message = message; 47 | removeAlertClasses(); 48 | angular.forEach(flash.classnames[type], function (clazz) { 49 | element.addClass(clazz); 50 | }); 51 | 52 | if (!isBlank(attr.activeClass)) { 53 | element.addClass(attr.activeClass); 54 | } 55 | 56 | if (!message) { 57 | $scope.hide(); 58 | return; 59 | } 60 | 61 | var delay = Number(attr.duration || 5000); 62 | if (delay > 0) { 63 | timeoutHandle = $timeout($scope.hide, delay); 64 | } 65 | } 66 | 67 | subscribeHandle = flash.subscribe(show, attr.flashAlert, attr.id); 68 | 69 | /** 70 | * Fixes timing issues: display the last flash message sent before this directive subscribed. 71 | */ 72 | 73 | if (attr.flashAlert && flash[attr.flashAlert]) { 74 | show(flash[attr.flashAlert], attr.flashAlert); 75 | } 76 | 77 | if (!attr.flashAlert && flash.message) { 78 | show(flash.message, flash.type); 79 | } 80 | 81 | } 82 | }; 83 | } 84 | 85 | angular.module('angular-flash.flash-alert-directive', ['angular-flash.service']) 86 | .directive('flashAlert', ['flash', '$timeout', flashAlertDirective]); 87 | 88 | }()); 89 | -------------------------------------------------------------------------------- /src/services/flash-service.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | var subscriberCount = 0; 7 | 8 | var Flash = function (options) { 9 | var _options = angular.extend({ 10 | id: null, 11 | subscribers: {}, 12 | classnames: { 13 | error: [], 14 | warn: [], 15 | info: [], 16 | success: [] 17 | } 18 | }, options); 19 | 20 | var _self = this; 21 | var _success; 22 | var _info; 23 | var _warn; 24 | var _error; 25 | var _type; 26 | 27 | function _notify(type, message) { 28 | angular.forEach(_options.subscribers, function (subscriber) { 29 | var matchesType = !subscriber.type || subscriber.type === type; 30 | var matchesId = (!_options.id && !subscriber.id) || subscriber.id === _options.id; 31 | if (matchesType && matchesId) { 32 | subscriber.cb(message, type); 33 | } 34 | }); 35 | } 36 | 37 | this.clean = function () { 38 | _success = null; 39 | _info = null; 40 | _warn = null; 41 | _error = null; 42 | _type = null; 43 | }; 44 | 45 | this.subscribe = function (subscriber, type, id) { 46 | subscriberCount += 1; 47 | _options.subscribers[subscriberCount] = { 48 | cb: subscriber, 49 | type: type, 50 | id: id 51 | }; 52 | return subscriberCount; 53 | }; 54 | 55 | this.unsubscribe = function (handle) { 56 | delete _options.subscribers[handle]; 57 | }; 58 | 59 | this.to = function (id) { 60 | var options = angular.copy(_options); 61 | options.id = id; 62 | return new Flash(options); 63 | }; 64 | 65 | Object.defineProperty(this, 'success', { 66 | get: function () { 67 | return _success; 68 | }, 69 | set: function (message) { 70 | _success = message; 71 | _type = 'success'; 72 | _notify(_type, message); 73 | } 74 | }); 75 | 76 | Object.defineProperty(this, 'info', { 77 | get: function () { 78 | return _info; 79 | }, 80 | set: function (message) { 81 | _info = message; 82 | _type = 'info'; 83 | _notify(_type, message); 84 | } 85 | }); 86 | 87 | Object.defineProperty(this, 'warn', { 88 | get: function () { 89 | return _warn; 90 | }, 91 | set: function (message) { 92 | _warn = message; 93 | _type = 'warn'; 94 | _notify(_type, message); 95 | } 96 | }); 97 | 98 | Object.defineProperty(this, 'error', { 99 | get: function () { 100 | return _error; 101 | }, 102 | set: function (message) { 103 | _error = message; 104 | _type = 'error'; 105 | _notify(_type, message); 106 | } 107 | }); 108 | 109 | Object.defineProperty(this, 'type', { 110 | get: function () { 111 | return _type; 112 | } 113 | }); 114 | 115 | Object.defineProperty(this, 'message', { 116 | get: function () { 117 | return _type ? _self[_type] : null; 118 | } 119 | }); 120 | 121 | Object.defineProperty(this, 'classnames', { 122 | get: function () { 123 | return _options.classnames; 124 | } 125 | }); 126 | 127 | Object.defineProperty(this, 'id', { 128 | get: function () { 129 | return _options.id; 130 | } 131 | }); 132 | }; 133 | 134 | angular.module('angular-flash.service', []) 135 | .provider('flash', function () { 136 | var _self = this; 137 | this.errorClassnames = ['alert-error']; 138 | this.warnClassnames = ['alert-warn']; 139 | this.infoClassnames = ['alert-info']; 140 | this.successClassnames = ['alert-success']; 141 | 142 | this.$get = function () { 143 | return new Flash({ 144 | classnames: { 145 | error: _self.errorClassnames, 146 | warn: _self.warnClassnames, 147 | info: _self.infoClassnames, 148 | success: _self.successClassnames 149 | } 150 | }); 151 | }; 152 | }); 153 | 154 | }()); 155 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": false, 3 | "browser": true, 4 | "jquery": true, 5 | "es5": true, 6 | "bitwise": true, 7 | "camelcase": false, 8 | "latedef": true, 9 | "boss": false, 10 | "curly": true, 11 | "debug": false, 12 | "devel": false, 13 | "eqeqeq": true, 14 | "evil": true, 15 | "forin": true, 16 | "immed": true, 17 | "laxbreak": false, 18 | "newcap": true, 19 | "noarg": true, 20 | "noempty": true, 21 | "nonew": true, 22 | "nomen": false, 23 | "onevar": false, 24 | "plusplus": true, 25 | "regexp": false, 26 | "undef": true, 27 | "sub": true, 28 | "strict": true, 29 | "white": false, 30 | "unused": true, 31 | "smarttabs": false, 32 | "indent": 4, 33 | "quotmark": "single", 34 | "globals": { 35 | "angular": true, 36 | "module": true, 37 | "describe": true, 38 | "beforeEach": true, 39 | "afterEach": true, 40 | "it": true, 41 | "inject": true, 42 | "expect": true, 43 | "jasmine": true 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /test/unit/directives/flash-alert-directive-spec.js: -------------------------------------------------------------------------------- 1 | describe('flash-alert-directive', function () { 2 | 'use strict'; 3 | 4 | beforeEach(function () { 5 | module('angular-flash.service', 'angular-flash.flash-alert-directive'); 6 | 7 | module(function ($provide, flashProvider) { 8 | 9 | flashProvider.errorClassnames.push('alert-danger'); 10 | 11 | $provide.decorator('$timeout', function ($delegate, $browser) { 12 | var spy = jasmine.createSpy('$timeout').andCallFake($delegate); 13 | spy.flush = function () { 14 | $browser.defer.flush(); 15 | }; 16 | spy.cancel = $delegate.cancel; 17 | return spy; 18 | }); 19 | }); 20 | 21 | }); 22 | 23 | 24 | it('should display all flash messages', inject(function ($rootScope, $compile, $timeout, flash) { 25 | 26 | var template = [ 27 | '
', 28 | '{{flash.type}}', 29 | '{{flash.message}}', 30 | '
' 31 | ]; 32 | 33 | var element = angular.element(template.join('\n')); 34 | $compile(element)($rootScope); 35 | $rootScope.$digest(); 36 | 37 | expect(element.find('.alert-heading').text()).toBe(''); 38 | expect(element.find('.alert-message').text()).toBe(''); 39 | expect(element.hasClass('alert-error')).toBe(false); 40 | expect(element.hasClass('alert-danger')).toBe(false); 41 | expect(element.hasClass('in')).toBe(false); 42 | 43 | flash.error = ':error-message'; 44 | $rootScope.$digest(); 45 | 46 | expect(element.find('.alert-heading').text()).toBe('error'); 47 | expect(element.find('.alert-message').text()).toBe(':error-message'); 48 | expect(element.hasClass('alert-error')).toBe(true); 49 | expect(element.hasClass('alert-danger')).toBe(true); 50 | expect(element.hasClass('in')).toBe(true); 51 | 52 | $timeout.flush(); 53 | 54 | expect(element.find('.alert-heading').text()).toBe('error'); 55 | expect(element.find('.alert-message').text()).toBe(':error-message'); 56 | expect(element.hasClass('alert-error')).toBe(false); 57 | expect(element.hasClass('in')).toBe(false); 58 | 59 | flash.success = ':success-message'; 60 | $rootScope.$digest(); 61 | 62 | 63 | expect(element.find('.alert-heading').text()).toBe('success'); 64 | expect(element.find('.alert-message').text()).toBe(':success-message'); 65 | expect(element.hasClass('alert-success')).toBe(true); 66 | expect(element.hasClass('in')).toBe(true); 67 | 68 | $timeout.flush(); 69 | 70 | expect(element.find('.alert-heading').text()).toBe('success'); 71 | expect(element.find('.alert-message').text()).toBe(':success-message'); 72 | expect(element.hasClass('alert-success')).toBe(false); 73 | expect(element.hasClass('in')).toBe(false); 74 | 75 | })); 76 | 77 | it('should display only error flash messages', inject(function ($rootScope, $compile, $timeout, flash) { 78 | 79 | var template = [ 80 | '
', 81 | '{{flash.type}}', 82 | '{{flash.message}}', 83 | '
' 84 | ]; 85 | 86 | var element = angular.element(template.join('\n')); 87 | $compile(element)($rootScope); 88 | $rootScope.$digest(); 89 | 90 | expect(element.find('.alert-heading').text()).toBe(''); 91 | expect(element.find('.alert-message').text()).toBe(''); 92 | expect(element.hasClass('alert-error')).toBe(false); 93 | expect(element.hasClass('alert-danger')).toBe(false); 94 | expect(element.hasClass('in')).toBe(false); 95 | 96 | flash.error = ':error-message'; 97 | $rootScope.$digest(); 98 | 99 | expect(element.find('.alert-heading').text()).toBe('error'); 100 | expect(element.find('.alert-message').text()).toBe(':error-message'); 101 | expect(element.hasClass('alert-error')).toBe(true); 102 | expect(element.hasClass('alert-danger')).toBe(true); 103 | expect(element.hasClass('in')).toBe(true); 104 | 105 | $timeout.flush(); 106 | 107 | expect(element.find('.alert-heading').text()).toBe('error'); 108 | expect(element.find('.alert-message').text()).toBe(':error-message'); 109 | expect(element.hasClass('alert-error')).toBe(false); 110 | expect(element.hasClass('alert-danger')).toBe(false); 111 | expect(element.hasClass('in')).toBe(false); 112 | 113 | flash.success = ':success-message'; 114 | $rootScope.$digest(); 115 | 116 | expect(element.find('.alert-heading').text()).toBe('error'); 117 | expect(element.find('.alert-message').text()).toBe(':error-message'); 118 | expect(element.hasClass('alert-success')).toBe(false); 119 | expect(element.hasClass('in')).toBe(false); 120 | 121 | })); 122 | 123 | it('should only display the most recent flash message', inject(function ($rootScope, $compile, $timeout, flash) { 124 | var template = [ 125 | '
', 126 | '{{flash.type}}', 127 | '{{flash.message}}', 128 | '
' 129 | ]; 130 | 131 | var element = angular.element(template.join('\n')); 132 | $compile(element)($rootScope); 133 | $rootScope.$digest(); 134 | 135 | expect(element.find('.alert-heading').text()).toBe(''); 136 | expect(element.find('.alert-message').text()).toBe(''); 137 | expect(element.hasClass('alert-error')).toBe(false); 138 | expect(element.hasClass('alert-danger')).toBe(false); 139 | expect(element.hasClass('alert-success')).toBe(false); 140 | expect(element.hasClass('alert-info')).toBe(false); 141 | expect(element.hasClass('alert-warning')).toBe(false); 142 | expect(element.hasClass('in')).toBe(false); 143 | 144 | flash.info = ':info-message'; 145 | flash.success = ':success-message'; 146 | $rootScope.$digest(); 147 | 148 | expect(element.find('.alert-heading').text()).toBe('success'); 149 | expect(element.find('.alert-message').text()).toBe(':success-message'); 150 | expect(element.hasClass('alert-error')).toBe(false); 151 | expect(element.hasClass('alert-danger')).toBe(false); 152 | expect(element.hasClass('alert-success')).toBe(true); 153 | expect(element.hasClass('alert-info')).toBe(false); 154 | expect(element.hasClass('alert-warning')).toBe(false); 155 | expect(element.hasClass('in')).toBe(true); 156 | 157 | $timeout.flush(); 158 | 159 | expect(element.find('.alert-heading').text()).toBe('success'); 160 | expect(element.find('.alert-message').text()).toBe(':success-message'); 161 | expect(element.hasClass('alert-error')).toBe(false); 162 | expect(element.hasClass('alert-danger')).toBe(false); 163 | expect(element.hasClass('alert-success')).toBe(false); 164 | expect(element.hasClass('alert-info')).toBe(false); 165 | expect(element.hasClass('alert-warning')).toBe(false); 166 | expect(element.hasClass('in')).toBe(false); 167 | 168 | })); 169 | 170 | it('should appear for 5 seconds with no duration attribute', inject(function ($rootScope, $compile, $timeout, flash) { 171 | var template = [ 172 | '
', 173 | '{{flash.type}}', 174 | '{{flash.message}}', 175 | '
' 176 | ]; 177 | 178 | var element = angular.element(template.join('\n')); 179 | $compile(element)($rootScope); 180 | 181 | flash.success = ':success-message'; 182 | $rootScope.$digest(); 183 | 184 | expect($timeout).toHaveBeenCalledWith(jasmine.any(Function), 5000); 185 | })); 186 | 187 | it('should appear for the number of msec specified by the duration attribute', inject(function ($rootScope, $compile, $timeout, flash) { 188 | var template = [ 189 | '
', 190 | '{{flash.type}}', 191 | '{{flash.message}}', 192 | '
' 193 | ]; 194 | 195 | var element = angular.element(template.join('\n')); 196 | $compile(element)($rootScope); 197 | 198 | flash.success = ':success-message'; 199 | $rootScope.$digest(); 200 | 201 | expect($timeout).toHaveBeenCalledWith(jasmine.any(Function), 3000); 202 | })); 203 | 204 | 205 | it('should not fade away with duration set to 0', inject(function ($rootScope, $compile, $timeout, flash) { 206 | var template = [ 207 | '
', 208 | '{{flash.type}}', 209 | '{{flash.message}}', 210 | '
' 211 | ]; 212 | 213 | var element = angular.element(template.join('\n')); 214 | $compile(element)($rootScope); 215 | 216 | flash.success = ':success-message'; 217 | $rootScope.$digest(); 218 | 219 | expect($timeout.wasCalled).toBe(false); 220 | 221 | })); 222 | 223 | it('should should display the error message even if the message was set before the directive subscribed', inject(function ($rootScope, $compile, flash) { 224 | var template = [ 225 | '
', 226 | '{{flash.type}}', 227 | '{{flash.message}}', 228 | '
' 229 | ]; 230 | 231 | flash.error = ':error-message'; 232 | 233 | var element = angular.element(template.join('\n')); 234 | 235 | $compile(element)($rootScope); 236 | $rootScope.$digest(); 237 | 238 | expect(element.find('.alert-heading').text()).toBe('error'); 239 | expect(element.find('.alert-message').text()).toBe(':error-message'); 240 | expect(element.hasClass('alert-error')).toBe(true); 241 | expect(element.hasClass('alert-danger')).toBe(true); 242 | expect(element.hasClass('in')).toBe(true); 243 | })); 244 | 245 | it('should should display the any message even if the message was set before the directive subscribed', inject(function ($rootScope, $compile, flash) { 246 | var template = [ 247 | '
', 248 | '{{flash.type}}', 249 | '{{flash.message}}', 250 | '
' 251 | ]; 252 | 253 | flash.error = ':error-message'; 254 | 255 | var element = angular.element(template.join('\n')); 256 | 257 | $compile(element)($rootScope); 258 | $rootScope.$digest(); 259 | 260 | expect(element.find('.alert-heading').text()).toBe('error'); 261 | expect(element.find('.alert-message').text()).toBe(':error-message'); 262 | expect(element.hasClass('alert-error')).toBe(true); 263 | expect(element.hasClass('alert-danger')).toBe(true); 264 | expect(element.hasClass('in')).toBe(true); 265 | })); 266 | 267 | it('should display the error message even if the error message was not the last message', inject(function ($rootScope, $compile, flash) { 268 | var template = [ 269 | '
', 270 | '{{flash.type}}', 271 | '{{flash.message}}', 272 | '
' 273 | ]; 274 | 275 | flash.error = ':error-message'; 276 | flash.success = ':success-message'; 277 | 278 | var element = angular.element(template.join('\n')); 279 | 280 | $compile(element)($rootScope); 281 | $rootScope.$digest(); 282 | 283 | expect(element.find('.alert-heading').text()).toBe('error'); 284 | expect(element.find('.alert-message').text()).toBe(':error-message'); 285 | expect(element.hasClass('alert-error')).toBe(true); 286 | expect(element.hasClass('alert-danger')).toBe(true); 287 | expect(element.hasClass('in')).toBe(true); 288 | })); 289 | 290 | it('should hide the alert if the flash message is falsey', inject(function ($rootScope, $compile, flash) { 291 | var template = [ 292 | '
', 293 | '{{flash.type}}', 294 | '{{flash.message}}', 295 | '
' 296 | ]; 297 | 298 | var element = angular.element(template.join('\n')); 299 | $compile(element)($rootScope); 300 | $rootScope.$digest(); 301 | 302 | expect(element.find('.alert-heading').text()).toBe(''); 303 | expect(element.find('.alert-message').text()).toBe(''); 304 | expect(element.hasClass('alert-error')).toBe(false); 305 | expect(element.hasClass('alert-danger')).toBe(false); 306 | expect(element.hasClass('in')).toBe(false); 307 | 308 | flash.error = ':error-message'; 309 | $rootScope.$digest(); 310 | 311 | expect(element.find('.alert-heading').text()).toBe('error'); 312 | expect(element.find('.alert-message').text()).toBe(':error-message'); 313 | expect(element.hasClass('alert-error')).toBe(true); 314 | expect(element.hasClass('alert-danger')).toBe(true); 315 | expect(element.hasClass('in')).toBe(true); 316 | 317 | flash.error = ''; 318 | $rootScope.$digest(); 319 | 320 | expect(element.find('.alert-heading').text()).toBe('error'); 321 | expect(element.find('.alert-message').text()).toBe(''); 322 | expect(element.hasClass('alert-error')).toBe(false); 323 | expect(element.hasClass('alert-danger')).toBe(false); 324 | expect(element.hasClass('in')).toBe(false); 325 | 326 | })); 327 | 328 | describe('scope destroy', function () { 329 | 330 | it('should clean the flash service when the directive scope is destroyed', inject(function ($rootScope, $compile, flash) { 331 | var template = [ 332 | '
', 333 | '{{flash.type}}', 334 | '{{flash.message}}', 335 | '
' 336 | ]; 337 | 338 | flash.error = ':error-message'; 339 | flash.success = ':success-message'; 340 | 341 | var element = angular.element(template.join('\n')); 342 | 343 | var $scope = $rootScope.$new(); 344 | 345 | spyOn(flash, 'clean').andCallThrough(); 346 | spyOn(flash, 'unsubscribe').andCallThrough(); 347 | 348 | $compile(element)($scope); 349 | $scope.$digest(); 350 | 351 | expect(element.find('.alert-heading').text()).toBe('error'); 352 | expect(element.find('.alert-message').text()).toBe(':error-message'); 353 | expect(element.hasClass('alert-error')).toBe(true); 354 | expect(element.hasClass('alert-danger')).toBe(true); 355 | expect(element.hasClass('in')).toBe(true); 356 | 357 | $scope.$destroy(); 358 | 359 | expect(flash.clean).toHaveBeenCalled(); 360 | expect(flash.unsubscribe).toHaveBeenCalledWith(jasmine.any(Number)); 361 | expect(flash.message).toBeNull(); 362 | 363 | })); 364 | 365 | it('should not unsubscribe subscribers when the directive scope is destroyed', inject(function ($rootScope, $compile, flash) { 366 | var template = [ 367 | '
', 368 | '{{flash.type}}', 369 | '{{flash.message}}', 370 | '
' 371 | ]; 372 | 373 | var element1 = angular.element(template.join('\n')); 374 | var element2 = angular.element(template.join('\n')); 375 | 376 | var $scope1 = $rootScope.$new(); 377 | var $scope2 = $rootScope.$new(); 378 | 379 | spyOn(flash, 'clean').andCallThrough(); 380 | spyOn(flash, 'unsubscribe').andCallThrough(); 381 | 382 | $compile(element1)($scope1); 383 | $compile(element2)($scope2); 384 | 385 | flash.success = ':success-message'; 386 | 387 | $scope1.$digest(); 388 | $scope2.$digest(); 389 | 390 | $scope1.$destroy(); 391 | 392 | flash.error = ':error-message'; 393 | 394 | $scope1.$digest(); 395 | $scope2.$digest(); 396 | 397 | expect(flash.clean.calls.length).toEqual(1); 398 | expect(flash.unsubscribe.calls.length).toEqual(1); 399 | 400 | expect(element1.find('.alert-heading').text()).toBe('success'); 401 | expect(element1.find('.alert-message').text()).toBe(':success-message'); 402 | expect(element1.hasClass('alert-success')).toBe(true); 403 | expect(element1.hasClass('in')).toBe(true); 404 | 405 | expect(element2.find('.alert-heading').text()).toBe('error'); 406 | expect(element2.find('.alert-message').text()).toBe(':error-message'); 407 | expect(element2.hasClass('alert-error')).toBe(true); 408 | expect(element2.hasClass('alert-danger')).toBe(true); 409 | expect(element2.hasClass('in')).toBe(true); 410 | })); 411 | }); 412 | 413 | 414 | }); 415 | -------------------------------------------------------------------------------- /test/unit/services/flash-service-spec.js: -------------------------------------------------------------------------------- 1 | describe('FlashService', function () { 2 | 'use strict'; 3 | 4 | var _flash; 5 | 6 | beforeEach(module('angular-flash.service')); 7 | 8 | beforeEach(inject(function (flash) { 9 | _flash = flash; 10 | })); 11 | 12 | it('it should send flash messages to subscribers', function () { 13 | var subscriber1 = jasmine.createSpy('subscriber1'); 14 | var subscriber2 = jasmine.createSpy('subscriber2'); 15 | 16 | _flash.subscribe(subscriber1); 17 | _flash.subscribe(subscriber2); 18 | 19 | _flash.error = ':error-message'; 20 | _flash.warn = ':warn-message'; 21 | _flash.info = ':info-message'; 22 | _flash.success = ':success-message'; 23 | 24 | expect(subscriber1).toHaveBeenCalledWith(':error-message', 'error'); 25 | expect(subscriber1).toHaveBeenCalledWith(':warn-message', 'warn'); 26 | expect(subscriber1).toHaveBeenCalledWith(':info-message', 'info'); 27 | expect(subscriber1).toHaveBeenCalledWith(':success-message', 'success'); 28 | 29 | expect(subscriber2).toHaveBeenCalledWith(':error-message', 'error'); 30 | expect(subscriber2).toHaveBeenCalledWith(':warn-message', 'warn'); 31 | expect(subscriber2).toHaveBeenCalledWith(':info-message', 'info'); 32 | expect(subscriber2).toHaveBeenCalledWith(':success-message', 'success'); 33 | 34 | }); 35 | 36 | it('it should send flash messages to subscribers of the right type of flash', function () { 37 | var errorSubscriber = jasmine.createSpy('errorSubscriber'); 38 | var warnSubscriber = jasmine.createSpy('warnSubscriber'); 39 | var infoSubscriber = jasmine.createSpy('infoSubscriber'); 40 | var successSubscriber = jasmine.createSpy('successSubscriber'); 41 | 42 | _flash.subscribe(errorSubscriber, 'error'); 43 | _flash.subscribe(warnSubscriber, 'warn'); 44 | _flash.subscribe(infoSubscriber, 'info'); 45 | _flash.subscribe(successSubscriber, 'success'); 46 | 47 | _flash.error = ':error-message'; 48 | _flash.warn = ':warn-message'; 49 | _flash.info = ':info-message'; 50 | _flash.success = ':success-message'; 51 | 52 | expect(errorSubscriber).toHaveBeenCalledWith(':error-message', 'error'); 53 | expect(errorSubscriber.calls.length).toEqual(1); 54 | 55 | expect(warnSubscriber).toHaveBeenCalledWith(':warn-message', 'warn'); 56 | expect(warnSubscriber.calls.length).toEqual(1); 57 | 58 | expect(infoSubscriber).toHaveBeenCalledWith(':info-message', 'info'); 59 | expect(infoSubscriber.calls.length).toEqual(1); 60 | 61 | expect(successSubscriber).toHaveBeenCalledWith(':success-message', 'success'); 62 | expect(successSubscriber.calls.length).toEqual(1); 63 | }); 64 | 65 | it('it should send flash messages to the right subscribers', function () { 66 | var subscriber1 = jasmine.createSpy('subscriber1'); 67 | var subscriber2 = jasmine.createSpy('subscriber2'); 68 | 69 | _flash.subscribe(subscriber1, null, 'foo'); 70 | _flash.subscribe(subscriber2, null, 'bar'); 71 | 72 | _flash.to('foo').error = 'error 1'; 73 | 74 | _flash.to('bar').error = 'error 2'; 75 | 76 | _flash.error = ':error-message'; 77 | 78 | expect(_flash.id).toBeNull(); 79 | expect(_flash.to('foo').id).toEqual('foo'); 80 | expect(_flash.to('bar').id).toEqual('bar'); 81 | 82 | expect(subscriber1).toHaveBeenCalledWith('error 1', 'error'); 83 | expect(subscriber1.calls.length).toEqual(1); 84 | 85 | expect(subscriber2).toHaveBeenCalledWith('error 2', 'error'); 86 | expect(subscriber2.calls.length).toEqual(1); 87 | }); 88 | 89 | it('the flash getters should return the right message', function () { 90 | _flash.error = ':error-message'; 91 | _flash.warn = ':warn-message'; 92 | _flash.info = ':info-message'; 93 | _flash.success = ':success-message'; 94 | 95 | expect(_flash.error).toBe(':error-message'); 96 | expect(_flash.warn).toBe(':warn-message'); 97 | expect(_flash.info).toBe(':info-message'); 98 | expect(_flash.success).toBe(':success-message'); 99 | }); 100 | 101 | it('flash.type and flash.message should return the last flash', function () { 102 | _flash.error = ':error-message'; 103 | _flash.warn = ':warn-message'; 104 | 105 | expect(_flash.type).toBe('warn'); 106 | expect(_flash.message).toBe(':warn-message'); 107 | }); 108 | 109 | }); 110 | --------------------------------------------------------------------------------