var app = angular.module('plunker', ['simpleAlert']);

app.controller('MainCtrl', function($scope,simpleAlertFactory) { 
  
  simpleAlertFactory.setDefault({'closeIconClasses':'glyphicon glyphicon-heart'});
  
  //for testing callbacks:
  var saySomething = function(params){console.log(params)}; 
  
  // simplest use case:
  $scope.basicTest = function(){
     simpleAlertFactory.show({message:'simple test.',clickToClose:'true'});
     // no callbacks, no title, just a simple closable message
  };
  
  // demoing fully defined cancel and continue options.
  $scope.sendWarning = function(){ 
    simpleAlertFactory.show({ 
      /* NOTE: no id is required for default alert target 
         (provided a simple-alert directive exists in HTML with no id) */
      /* NOTE: no type is required for warning style alert */
      title:"Warning:",
      message:'Holy Moly Spiccoli, a link---&gt; <a class="alert-link link-warning" href="http://google.com">LOOK OUT!</a>',
      classes:'pink force-top',
      cancelLabel:"Cancel",
      cancelCallback:saySomething,
      cancelCallbackParamsArray:["default warning alert was canceled!"],
      
      okLabel:"Continue",
      callback:saySomething,
      callbackParamsArray:["default warning alert was closed!"]}
      );
  };
  
  // sending 'warning' style alert with title to a specific ('custom') alert target
  // with success callback defined
  $scope.sendWarningToCustom = function(){
    simpleAlertFactory.show({
      id:"custom",  
      /* notice no type is required for alert-warning */
      title:"No close icon!",
      closeIcon:false,
      clickToClose:true,
      message:'Click anywhere on alert', 
      callback:saySomething, 
      callbackParamsArray:["custom warning alert was closed!"]});
  };
  
  // sending 'info' style alert to default alert target
  // with success callback defined
  $scope.sendInfo = function(){
    simpleAlertFactory.show({ 
      /* notice no id is required for default */
      type:"info",
      title:"Here's a tip...",
      message:'Remember to tip your waiter.',
      callback:saySomething,
      callbackParamsArray:["default info alert was closed!"]});
  };
  
  // sending 'error' style alert to custom alert target
  // with success callback defined
  $scope.sendErrorToCustom = function(){
    simpleAlertFactory.show({ 
      id:"custom",
      type:'danger',
      title:"Oh Yes",
      message:'You are in danger.', 
      callback:saySomething,
      callbackParamsArray:["custom error alert was closed!"]});
  }; 
  
  // sendAutoCloseToCustom
  $scope.sendAutoCloseToCustom = function(){
    simpleAlertFactory.show({ 
      id:"custom",
      type:'info',
      message:"3... 2... 1...",
      title:'Closing in...',
      timeout:3000,
      clickToClose:true,
      callback:saySomething,
      callbackParamsArray:["custom error alert was closed!"]
      
    });
  };
  
  // proxy function for clearing all simple alerts.
  // NOTE: No callbacks are triggered.
  $scope.clearAll = function(){
    console.log('clearing all alerts.');
    simpleAlertFactory.clearAll();
  };
  
});

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS test</title>
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <link data-require="jasmine" data-semver="2.0.0" rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
    <script data-require="json2" data-semver="0.0.2012100-8" src="//cdnjs.cloudflare.com/ajax/libs/json2/20121008/json2.js"></script>
    <script data-require="jasmine" data-semver="2.0.0" src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
    <script data-require="jasmine" data-semver="2.0.0" src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
    <script data-require="jasmine@*" data-semver="2.0.0" src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
    <script data-require="angular.js" data-semver="1.4.0-beta.2" src="https://code.angularjs.org/1.4.0-beta.2/angular.js"></script>
    <script data-require="angular-mocks" data-semver="1.4.0-beta.2" src="https://code.angularjs.org/1.4.0-beta.2/angular-mocks.js"></script>
    
    <link rel="stylesheet" href="style.css" /> 
    <script src="simpleAlert.js"></script>
    <script src="app.js"></script> 
    
    <script src="simpleAlertSpec.js"></script> 
    <script src="jasmineBootstrap.js"></script> 
    <!-- bootstraps Jasmine -->
  </head>

  <body>
    <div id="container" ng-controller="MainCtrl">
    <h3>Alert Examples:</h3>
    <h6>-- global commands --</h6>
    <ul>
       <li class="btn-link" ng-click="clearAll()">Clear all currently open alerts (no callbacks)</li>
       <li class="btn-link" ng-click="removeCustom=!removeCustom">Toggle rendering of custom directive container (no callbacks)</li>
     </ul>
     <h6>-- default target --</h6>
      <ul>
        <li class="btn-link" ng-click="basicTest()">Send simple warning to default test area</li>
        <li class="btn-link" ng-click="sendWarning()">Send more complex warning message to default test area with custom class</li> 
        <li class="btn-link" ng-click="sendInfo()">Send info message to default test area</li>
      </ul>
      <h6>-- custom target --</h6>
      <ul>
        <li class="btn-link" ng-click="sendWarningToCustom()">Send warning to custom test area with no close button</li>
        <li class="btn-link" ng-click="sendErrorToCustom()">Send error alert to custom test area</li>
        <li class="btn-link" ng-click="sendAutoCloseToCustom()">Send Auto Closing alert to custom test area</li>
      </ul>
     <data-simple-alert id="hack-solely-for-hiding-jasmine-cruft-in-plnkr"></data-simple-alert>
      <div class="well area col-xs-6">
      
        <p>This is the <strong>default</strong> alert test area:</p>
        <!-- put at least the following directive on your page -->
        <data-simple-alert></data-simple-alert>
      
      </div>  
      <!-- Here we add the ng-if for re-rendering to test if the model stays clean, and to check if commands throws errors -->
      <div class="well area col-xs-6" ng-if="removeCustom!==true">
      
        <p>This is <strong>custom</strong> alert test area:</p>
        
        <data-simple-alert id="custom"></data-simple-alert>
        
      </div>
 
    </div>
   <hr/>
    <div id="HTMLReporter" class="jasmine_reporter"></div>
  </body>

</html>
(function() {
  var jasmineEnv = jasmine.getEnv();
  jasmineEnv.updateInterval = 250;

  /**
   Create the `HTMLReporter`, which Jasmine calls to provide results of each spec and each suite. The Reporter is responsible for presenting results to the user.
   */
  var htmlReporter = new jasmine.HtmlReporter();
  jasmineEnv.addReporter(htmlReporter);

  /**
   Delegate filtering of specs to the reporter. Allows for clicking on single suites or specs in the results to only run a subset of the suite.
   */
  jasmineEnv.specFilter = function(spec) {
    return htmlReporter.specFilter(spec);
  };

  /**
   Run all of the tests when the page finishes loading - and make sure to run any previous `onload` handler

   ### Test Results

   Scroll down to see the results of all of these specs.
   */
  var currentWindowOnload = window.onload;
  window.onload = function() {
    if (currentWindowOnload) {
      currentWindowOnload();
    }

    //document.querySelector('.version').innerHTML = jasmineEnv.versionString();
    execJasmine();
  };

  function execJasmine() {
    jasmineEnv.execute();
  }
})();
/* restore "body" styling that were changes by "jasmine.css"... */
body { background-color: white; padding: 0; margin: 8px; }
/* ... but remain the "jasmine.css" styling for the Jasmine reporting */
.jasmine_reporter { background-color: #eeeeee; padding: 0; margin: 0; }
.area{
  border:1px dashed #44F; 
}

simple-alert .btn{
	margin-top:10px;
	margin-left:10px;
} 
.pink {
  border:3px dashed pink;
}

.force-top{
  position:fixed;
  top:0;
  left:0;
  margin:0;
  width:100%;
  z-index:9000;
}
angular.module('simpleAlert', [])
  .factory('simpleAlertFactory', function($interval,$sce) {
    var timeoutVar;
    var internal = {
      messages: {},
      default: {
        id: undefined,
        type: 'warning', 
        message: '',
        defaultClasses: '', // use this to set site-wide customizations
        // custom
        classes: '',

        //OK is optional
        okLabel: '',
        callback: angular.noop,
        callbackParamsArray: [undefined],

        // cancel is optional 
        cancelLabel: '',
        cancelCallback: angular.noop,
        cancelCallbackParamsArray: [undefined],
        title: undefined,
        timeout: 0,
        closeIcon: true,
        closeIconClasses: 'glyphicon glyphicon-remove-sign',
        clickToClose: false
      }
    };
    
    // setDefault() has two signatures:
    // if passed a key value pair, then it will update that element
    // otherwise it will expect key to be a new default object, 
    // and extend the existing default object with that
    var setDefault = function(keyOrObject, newDefault) {
      if(newDefault){
      internal.default[keyOrObject] = newDefault;
      } else if(keyOrObject){
        angular.extend(internal.default, keyOrObject)
      }
    };
    
    var show = function(opts) {
      var msg = angular.extend({}, internal.default, opts);
      internal.messages[msg.id] = msg;
    };

    var clearById = function(id) {
      $interval.cancel(timeoutVar);
      internal.messages[id].callback.apply(this, internal.messages[id].callbackParamsArray);
      internal.messages[id] = {};
      timeoutVar = $interval(function() {
        delete internal.messages[id];
      }, 0,1);
    };

    var removeById = function(id) {
      delete internal.messages[id];
    };

    var clearAll = function() {
      angular.forEach(internal.messages, function(value, key) {
        internal.messages[key] = {};
      });

      $interval(function() {
        angular.forEach(internal.messages, function(value, key) {
          delete internal.messages[key];
        });

      }, 0,1);


    };

    var cancelById = function(id) {
       $interval.cancel(timeoutVar);
      internal.messages[id].cancelCallback.apply(this, internal.messages[id].cancelCallbackParamsArray);
      internal.messages[id] = {};
      // delayed delete so everything has a chance to execute.
      timeoutVar = $interval(function() {
        delete internal.messages[id];
      }, 0,1);

    };

    return {
      setDefault: setDefault,
      show: show,
      cancelById: cancelById,
      clearById: clearById,
      removeById: removeById,
      clearAll: clearAll,
      messages: internal.messages
    };

  }).directive('simpleAlert', function($interval, simpleAlertFactory,$sce) {
    return {
      restrict: 'E',
      scope: {
        id: '@' 
      },
      template: '<div data-ng-click="clickToClose()" class="alert alert-{{msg.type}} alert-dismissable {{msg.defaultClasses}} {{msg.classes}}" ng-show="msg.message.length">' +
        '<button class="close" data-dismiss="alert" data-ng-click="clear()" ng-show="msg.closeIcon">' +
        '<span class="{{msg.closeIconClasses}}"></span>' +
        '</button>' +
        '<h3 ng-show="msg.title.length" ng-bind="msg.title"></h3>' +
        '<p ng-bind-html="makeHTML(msg.message)"></p>' +
        '<button ng-show="msg.cancelLabel.length" class="btn btn-default" data-ng-click="cancel()">{{msg.cancelLabel}}</button>' +
        '<button ng-show="msg.okLabel.length" class="btn btn-{{msg.type}}" data-ng-click="clear()">{{msg.okLabel}}</button>' +
        '</div>',
      link: function($scope) {
        var timeoutVar;
        $scope.msgObj = simpleAlertFactory.messages;

        // deep watch - can we avoid?  contains all named alert instances currently in play.
        $scope.$watch('msgObj', function(newVal, oldVal) {
          $interval.cancel(timeoutVar);
          // The id here is the key so newVal[id] is an alert config object
          if (newVal[$scope.id] && !angular.equals(newVal[$scope.id], oldVal[$scope.id])) {
            $scope.msg = newVal[$scope.id];

            // add a timeout if 'timeout exists on msgObj'
            if (newVal[$scope.id].timeout) {
              timeoutVar = $interval(function() {
                $scope.clear();
              }, +newVal[$scope.id].timeout,1);
            }
          }
        }, true);

        $scope.clickToClose = function() {
          if ($scope.msg.clickToClose) {
            $scope.clear();
          }
        };
        $scope.makeHTML = function(text){
          return $sce.trustAsHtml(text);
        }
        
        $scope.clear = function() {
          $interval.cancel(timeoutVar);
          //removes the  message  and trigger the success handler
          simpleAlertFactory.clearById($scope.id);

        };

        $scope.cancel = function() {
          //removes the  message and trigger the cancel handler
          simpleAlertFactory.cancelById($scope.id);
        };

        $scope.$on('$destroy', function() {
          $interval.cancel(timeoutVar);
          simpleAlertFactory.removeById($scope.id);
        });

      }
    };
  });
Angular-Simple-Message

This Directive/Service is an attempt at creating a minimalistic in-page (non-modal) notification system using Bootstrap.
The goal of this project is to minimize excess HTML and code where possible, 
while providing options for a rich interactive notification system.

Features:
- Simple
- Fully customizable template.
- Uses plain HTML ID's to determine alert target directive.
- Can handle triggers callbacks and parameters
- Can be used anywhere in an angular app, regardless of scope, just import the service.
- no external dependencies.
- fully tested (coming soon)
describe('simpleAlert: Testing', function() {
  var defaultElement, customElement, scope, simpleAlertFactory,
    complexTestData, simpleTestData,
    simpleTitle, simpleText,simpleFunction,
    successParams,failParams;
  
  // create a default app (container)
  beforeEach(module('simpleAlert'));
 
  beforeEach(inject(function($rootScope, $compile, _simpleAlertFactory_) {
    scope = $rootScope.$new();
    simpleAlertFactory = _simpleAlertFactory_;
    defaultElement =
        ' <data-simple-alert></data-simple-alert>';
        
    customElement =
        ' <data-simple-alert id="test"></data-simple-alert>';

    defaultElement = $compile(defaultElement)(scope);  
    customElement = $compile(customElement)(scope);
    scope.$digest();
    
    // --- test data values ---
    simpleText = 'message text';
    simpleTitle = 'title text';
    simpleFunction = function(params){/*no-op*/};
    successParams = [1,2,3];
    failParams = [-1,-2,-3];
    
    // --- test data objects ---
    simpleTestData = {message:simpleText};
    
    complexTestData = { 
      id : 'test', 
      type : 'info', 
      message : 'Complex example demo text',
      okLabel : 'Continue', 
      callback : Function, 
      callbackParamsArray : [ 1, 2, 3 ], 
      cancelLabel : 'Cancel', 
      cancelCallback : Function, cancelCallbackParamsArray : [ -1, -2, -3 ], 
      title : undefined, 
      timeout:0,
      defaultClasses: "",
      classes: "",
      closeIcon : true, 
      closeIconClasses:'glyphicon glyphicon-remove-sign',
      clickToClose : false };
  }));
  
  describe('an empty default simple alert', function() {
    
    it("should have a message Object defined", function() {
      var isolated = defaultElement.isolateScope();  
     expect(isolated.msgObj).toBeDefined(); 
    });
    
    it("should not have a message Object id defined", function() {
      var isolated = defaultElement.isolateScope(); 
     expect(isolated.msgObj.id).toBeUndefined();
    });
  });

  describe('a populated default simple alert with minimal configuration', function() {
    
    it("should not have a message value defined before factory call", function() {
      var isolated = defaultElement.isolateScope();  
     expect(isolated.msgObj[undefined]).toBeUndefined(); 
    });
    
    it("should have a message value defined after factory call", function() { 
      simpleAlertFactory.show(simpleTestData);
      var isolated = defaultElement.isolateScope();  
      scope.$digest();
      expect(isolated.msgObj[undefined].message).toBe(simpleText); 
    });
    
    it("should not bleed data into a custom alert scope", function() { 
      simpleAlertFactory.show(simpleTestData);
      var isolated = defaultElement.isolateScope();  
      scope.$digest();
      expect(isolated.msgObj['test']).toBeUndefined(); 
    });
    
    it("should not have a message value defined after clear", function() {
        simpleAlertFactory.show(simpleTestData);
        var isolated = defaultElement.isolateScope(); 
  
        scope.$digest();
        expect(isolated.msgObj).toBeDefined(); 
        simpleAlertFactory.clearAll();
        scope.$digest(); 
        expect(isolated.msgObj[undefined]).toEqual({}); //.toBeUndefined(); 
    });
    
  });
  
  describe('a populated default simple alert with complex configuration', function() {
    
    it("should not have a message value defined before factory call", function() {
      var isolated = customElement.isolateScope();  
     expect(isolated.msgObj['test']).toBeUndefined(); 
    });
    
    it("should have all custom values defined after factory call", function() { 
      simpleAlertFactory.show(complexTestData);
      var isolated = customElement.isolateScope();  
      scope.$digest();
      expect(isolated.msgObj['test']).toEqual(complexTestData); 
    });
    
    it("should not bleed data into the default alert scope", function() { 
      simpleAlertFactory.show(complexTestData);
      var isolated = defaultElement.isolateScope();  
      scope.$digest();
      expect(isolated.msgObj[undefined]).toBeUndefined(); 
    });
     // spyOn(complexTestData, 'callback'); expect to have been called with...
     // 
  });
  describe('callback handling', function() {
    //spyOn(complexTestData,'callback');
  })
  describe('clearing a simple alert', function() {})
  describe('canceling a simple alert', function() {})
});