Search code examples
jquerydomangularjsdom-manipulationangularjs-service

AngularJS Informer service


I'm using Twitter Bootstrap for UI in my webapp. Particulary its Alert component. I want to write a simple angular service to wrap Bootstrap's Alert to have a possibility of informing users from any peace of angular code. Like this:

Informer.inform("message", "ERROR"); // will result in alerting with `alert-error` class
Informer.inform("message", "INFO"); // will result in alerting with `alert-info` class

My idea is to to append the template to the end of the <body>:

<div class="alert {{alertClass}} fade in informer" id="informer">
    <button type="button" class="close" data-dismiss="alert">×</button>
    <div class="valignCenterWrapper">
        <div class="valignCenter" id="informerMessage">
            {{message}}
        </div>
    </div>
</div>

Something like this:

grfx.factory("Informer", function() {
    return {
        inform : function(message, type) {
            // Here be dragons. How can I compile/append the template.

            $("#inform").alert();
        }
    };
});

The only thing I want to know: how do I write this with angular, not with jQuery? Is the code above good for start? Folks in the internets say that I should only use directives for DOM manipulation. But I do not understand it: I do not have any existing markup to apply directive on it. Alerts will be appended to the page as a result of some compupations/user interactions. Which services ($compile, $parse, $document) should I use to compile temlate and append it somewhere to the body?

EDIT: Is it also possible to get angularjs service outside of controller. Just in regular JS code so I can write getServiece("Informer").inform("", "")?

EDIT 2: Ok, what I have now:

grfx.factory("Informer", function($compile, $rootScope) {
    return {
        inform : function(message, type) {
            var scope = $rootScope.$new();

            scope.message = message;
            scope.type = type;

            $(document.body).append($compile("<div class='alert {{type}} fade in informer' id='informer'><button type='button' class='close' data-dismiss='alert'>×</button><div class='valignCenterWrapper'><div class='valignCenter' id='informerMessage'>{{message}}</div></div></div>")(scope));
        }
    };
});

With this code I am able to use injected service from controllers. But there is an issue when I try to call service outside angular code:

angular.element(document).injector().get("Informer").inform("Message", "alert-error");

This shows popup with {{message}} e.g. it does not compile template correctly.


Solution

  • In AngularJS we should be focusing on model manipulation and your Informer service is no exception - it should only hold model and shouldn't be concerned with DOM manipulation. The rule of thumb where DOM manipulation = directive is a very good one and if you follow it it will save you a lot of headaches.

    Back to your problem at hand, the solution is to have a service focused on model manipulation and a directive to display this model. Let's start with the service:

    app.factory('Informer', function(){
    
      var messages = [];  
      var Informer = {};
    
      Informer.inform = function(msg, type) {
        messages.push({
          msg: msg,
          type: type
        });
      };
    
      Informer.allInfos = function() {
        return messages;
      };
    
      Informer.remove = function(info) {
        messages.splice(messages.indexOf(info), 1);
      };  
    
      return Informer;
    });
    

    When this service is ready you can easily use it in a controller (or even inside other services!):

    app.controller('MainCtrl', function($scope, Informer) {
    
      Informer.inform("error message", "error");
      Informer.inform("info message", "info");
    
      $scope.allInfos = Informer.allInfos;  
      $scope.remove = Informer.remove;
    });
    

    And finally, to render alerts you can use bootstrap's markup directly, or write a very simple directive that encapsulates it. Here I', using the alert directive from http://angular-ui.github.com/bootstrap/

    <body ng-controller="MainCtrl">
        <alert ng-repeat="alert in allInfos()" type="alert.type" close="remove(alert)">{{alert.msg}}</alert>
      </body>
    

    Of course you don't need to use directives from this repo, you can create your own or use raw markup if needed.

    Here is a plunker demonstrating a working example: http://plnkr.co/edit/VxAcjHFhxXODFB5iAfyX?p=preview

    To sum up:

    • As a rule of thumb don't do DOM manipulation outside of directives
    • Service dealing with the model should be decoupled from model's presentation

    I would also advice removing jQuery from a project while learning AngularJS. This way you will quicker get into AngularJS-zen state!