Creating an "App-Wide" Message System in Angular - angularjs

So I want to create a directive that outputs a global message.
Requirements of directive...
This directive message can be updated from any controller.
When the message is updated from any controller so is the directive consequently the view.
Clear the Message after the view is destoryed
So far I am doing this by creating a directive and service that work together.
Problem is Im not able to update the view when the message is update from inside other controllers
If someone could direct me on how to proceed that would be swell.
What about using $rootScope and broadcasting?
app.directive("alertMsg", ['MsgService', function(MsgService) {
return {
restrict: "E",
scope: true,
template: '{{msg}}', // this string is the html that will be placed inside the <alert-msg></alert-msg> tags.
link: function (scope, $element, attrs) {
scope.msg = MsgService.getAlertMsg(); //set msg to be available to the template above <alert-msg>{{msg}}</alert-msg>
scope.$on("$destroy", function(){ //when the <alert-msg> view is destroyed clear the alert message
MsgService.clearAlertMsg();
});
}
};
}]);
app.service('MsgService', function() {
this.alertMsg = '';
this.getAlertMsg = function(){
return this.alertMsg;
};
this.setAlertMsg = function(string) {
this.alertMsg = string;
};
this.clearAlertMsg = function(){
this.alertMsg = '';
};
});
app.controller('NewPlateController', ['urlConfig', '$scope', '$http', '$location', 'MsgService', '$routeParams', function(urlConfig, $scope, $http, $location, MsgService, $routeParams) {
$scope.plate = {license_plate: $routeParams.plate, state: 'default-state'};
// create new plate via json request
$scope.createPlate = function(){
$http.post(urlConfig.rootUrl+"/plates.js", $scope.plate).success(function(data) {
$scope.plateInfo = data;
MsgService.setAlertMsg('Plate Sucessfully Created'); //Need to update the directive to actual show this update
$location.path('/plate/'+$scope.plateInfo.plate_id);
// http error: display error messages
}).error(function(data,status,headers,config) {
$scope.errors = data;
$('#new-plate-errors').slideDown('fast');
});
};
}]);

Use $rootscope.$emit to send messages from your controllers (and even services) and use $rootScope.$on to receive them in your directive.
You must remove the listener on the directive's scope destruction or you will have a memory leak.
app.directive("alertMsg", ['$rootScope', function($rootScope) {
return {
restrict: "E",
scope: true,
template: '{{msg}}', // this string is the html that will be placed inside the <alert-msg></alert-msg> tags.
link: function (scope, $element, attrs) {
var _unregister; // store a reference to the message event listener so it can be destroyed.
_unregister = $rootScope.$on('message-event', function (event, message) {
scope.msg = message; // message can be value, an object, or an accessor function; whatever meets your needs.
});
scope.$on("$destroy", _unregister) //when the <alert-msg> view is destroyed remove the $rootScope event listener.
}
};
}]);
app.controller('NewPlateController', ['urlConfig', '$scope', '$http', '$location', '$rootScope', '$routeParams', function(urlConfig, $scope, $http, $location, $rootScope, $routeParams) {
$scope.plate = {license_plate: $routeParams.plate, state: 'default-state'};
// create new plate via json request
$scope.createPlate = function(){
$http.post(urlConfig.rootUrl+"/plates.js", $scope.plate).success(function(data) {
$scope.plateInfo = data;
$rootScope.$emit('message-event', 'Plate Sucessfully Created'); //use $emit, not $broadcast. Only $rootscope listeners are called.
scope.$on("$destroy", function() { // remove the message when the view is destroyed.
$rootScope.$emit('message-event', "");
});
$location.path('/plate/'+$scope.plateInfo.plate_id);
// http error: display error messages
}).error(function(data,status,headers,config) {
$scope.errors = data;
$('#new-plate-errors').slideDown('fast');
});
};
}]);
The message is not persisted outside of the directive so it will be removed when its scope is destroyed.
Edit: Adding a JSFiddle showing a working example: http://jsfiddle.net/kadm3zah/
Edit 2: I missed the requirement to remove the remove the message added when the view is destroyed. With this method you could add a second emit to the message on the NewPlateController scope's destruction with an empty string message.
This does not cover dynamically adding or removing the directive to the DOM. For that you could use a service to add and later remove the directive tag. This is how module's like ngToast and ui.boostrap's modal service work. Using one of them may be more appropriate for what you want to accomplish.

Related

AngularJS update value of scope from global variable

I have a global variable which is used to get information from server each 3 minutes if there are new messages. The variable name is HasNewMessage. Now what I want to do is to get the value of that variable in one of my AngularJS apps.
My directive is this
commonApp.directive('osMessageList', ['getMessages', '$location', '$anchorScroll', 'searchMessages', 'userfactory', '_', '$rootScope', '$window',
function (getMessages, $location, $anchorScroll, searchMessages, userfactory, _, $rootScope, $window) {
return {
restrict: 'A',
scope: {
messages: "="
},
controller: function ($scope) {
$scope.hasNewMessages = false;
$scope.$watch(function () {
return $scope.hasNewMessages;
}, function (newValue, oldValue) {
if (newValue) {
}
}, true);
},
function startInterval() {
// do something
// get scope of that directive and update the value
},SOME_TIME)
How can I acheive this?
If the startInterval function is outside the angularjs space (ie. global) it's a bit trickier. I assume there's a reason you can't simply use the $interval service and keep it all in the angularjs framework.
You could try using a native js event, and add a listener in your angularjs controller with reference to the services/scopes/etc in the callback of the listener.
https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
Create the event globally on page load or something. Dispatch it in your startInterval callback when the HasNewMessage state changes.
Add the listener in your directive controller, similar to the watch.

Dynamically change custom directive attribute value which should change directive content

Basically I have made custom directive to which i pass Url as string then inside the directive controller i'm calling http.get() method which creates the content inside this directive. What i want is to be able to change the value of annotation-url attribute which in return will change the content inside the directive because the new Url will return different JSON object. But it seems the directive is not getting refreshed when i change the annotationData from the controller.
HTML
<span ng-click="annotatonData = 'data/annotations1.json'">John</span>
<span ng-click="annotatonData = 'data/annotations2.json'">Marry</span>
<ng-annotate user-options="user" annotation-url="{{annotatonData}}"></ng-annotate>
Controller:
app.controller('ngAnnotationsController', ['$scope', '$http', function ($scope, $http) {
//InIt default http get
$scope.annotatonData = 'data/annotations1.json';
}]);
Directive:
app.directive('ngAnnotate', function () {
return {
templateUrl: 'templates/...',
restrict: 'E',
scope: {
annotationUrl: "#"
},
controller: ['$scope', '$http', function ($scope, $http) {
$http.get($scope.annotationUrl).then(function (response) {
$scope.data = response.data;
...
...
First, I suggest you change to
scope: {
annotationUrl: "="
}
the you can add a watcher to invoke $http whenever value is changed
$scope.$watch('annotationUrl', function(newVal, oldVal) {
if (newVal != oldVal) {
$http.get(newVal).then(function (response) {
$scope.data = response.data;
...
...
}
}
However, if you want to keep annotationUrl as it is, you need to use
$attr.$observe('annotationUrl', fn) to catch value changes.

AngularJS - Passing Scope between directives

In the interest of abstraction, I'm trying to pass a scope between directives with little success... Basically, this is a modal type scenario:
Directive A - handles click function of on screen element:
.directive('myElement', ['pane', function(pane){
return {
restrict: 'A',
scope: {},
link: function(scope,elem,attrs){
//im going to try and call the form.cancel function from a template compiled in another directive
scope.form = {
cancel: function(){
pane.close();
}
};
scope.$watch(function(){
var w = elem.parent()[0].clientWidth;
elem.css('height',(w*5)/4+'px');
});
elem.on('click', function(){
//this calls my service which communicates with my other directive to 1) display the pane, and 2) pass a template compiled with this directive's scope
pane.open({
templateUrl: 'views/forms/edit.html',
scope: scope //I pass the scope to the service API here
});
});
}
}
}])
I have a service called 'Pane' to handle the API:
.service('pane',['$rootScope', function($rootScope){
var open = function(data){
$rootScope.$broadcast('openPane',data); //this broadcasts my call to open the pane with the template url and the scope object
};
var close = function(){
$rootScope.$broadcast('closePane');
};
return {
open: open,
close: close
}
}]);
Finally, directive B is lying in wait for the 'openPane' broadcast which includes the template url and the scope:
.directive('pane',['$compile','$templateRequest','$rootScope', function($compile,$templateRequest,$rootScope){
return {
restrict: 'A',
link: function(scope,elem,attrs){
var t;
scope.$on('openPane', function(e,data){ //broadcast is received and pane is displayed with template that gets retrieved
if(data.templateUrl){
$templateRequest(data.templateUrl).then(function(template){
//this is where the problem seems to be. it works just fine, and the data.scope object does include my form object, but calling it from the template that opens does nothing
t = $compile(template)(data.scope);
elem.addClass('open');
elem.append(t);
}, function(err){
console.log(JSON.stringify(err));
});
}
else if(data.template){
t = $compile(angular.element(data.template))(data.scope);
elem.addClass('open');
elem.append(t);
}
else console.log("Can't open pane. No templateUrl or template was specified.")
});
scope.$on('closePane', function(e,data){
elem.removeClass('open');
t.remove();
});
}
}
}])
The problem is that when the last directive, 'pane', receives the 'openPane' broadcast, it opens and appends the template just fine, but when i call the function 'form.cancel()' defined in my original directive like so:
<button type="button" ng-click="form.cancel()">Cancel</button>
... nothing happens. Truth is, I'm not sure what I'm doing is legit at all, but i want to understand why it isn't working. The ultimate goal here is to be able to pass the scope of one directive, along with a form template, to the Pane directive, so all my forms (which are controlled by their own directives) can be 'injected' into the pane.
Without a running example I'm suspecting the likely cause to be the scope of your scope when passed to your pane template. The scope itself does get passed and used when you compile your pane template, but its closure is lost along the way, so you likely can't see pane service which is part of the directive factory closure and form.cancel uses.
I've written a simplified example that does work and doesn't rely on closures bt rather on local variables. You could accomplish a similar thing if you called .bind(pane) on your scope.form.cancel function and within replace pane by this.
So here's a working example and this is its code:
/* ************ */
/* Pane service */
class PaneService {
constructor($rootScope) {
console.log('pane service instantiated.', this);
this.$rootScope = $rootScope;
}
open(template, scope) {
this.$rootScope.$emit('OpenPane', template, scope);
}
close(message) {
this.$rootScope.$emit('ClosePane', message);
}
}
PaneService.$inject = ['$rootScope'];
/* ************************* */
/* Pane directive controller */
class PaneController {
constructor($rootScope, $compile, $element) {
console.log('pane directive instantiated.', this);
this.$compile = $compile;
this.$element = $element;
$rootScope.$on('OpenPane', this.open.bind(this));
$rootScope.$on('ClosePane', this.close.bind(this));
}
open(event, template, scope) {
console.log('pane directive opening', template, scope);
var t = this.$compile(template)(scope);
this.$element.empty().append(t);
}
close(evet, message) {
console.log('pane directive closing', message);
this.$element.empty().append('<strong>' + message + '</strong>');
}
}
PaneController.$inject = ['$rootScope', '$compile', '$element'];
var PaneDirective = {
restrict: 'A',
controller: PaneController,
controllerAs: 'pane',
bindToController: true
}
/* *************** */
/* Page controller */
class PageController {
constructor(paneService, $scope) {
console.log('page controller instantiated.', this);
this.paneService = paneService;
this.$scope = $scope;
}
open() {
console.log('page controller open', this);
this.paneService.open('<button ng-click="page.close(\'Closed from pane\')">Close from pane</button>', this.$scope);
}
close(message) {
console.log('page controller close');
this.paneService.close(message);
}
}
PageController.$inject = ['paneService', '$scope'];
angular
.module('App', [])
.service('paneService', PaneService)
.directive('pane', () => PaneDirective)
.controller('PageController', PageController);
And page template is very simple:
<body ng-app="App">
<h1>Hello Plunker!</h1>
<div ng-controller="PageController as page">
<button ng-click="page.open()">Open pane</button>
<button ng-click="page.close('Closed from page')">Close pane</button>
</div>
<div pane></div>
</body>

Unable to call Angular directive method

I've got an Angular view thusly:
<div ng-include="'components/navbar/navbar.html'" class="ui centered grid" id="navbar" onload="setDropdown()"></div>
<div class="sixteen wide centered column full-height ui grid" style="margin-top:160px">
<!-- other stuff -->
<import-elements></import-elements>
</div>
This is controlled by UI-Router, which is assigning the controller, just FYI.
The controller for this view looks like this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
$scope.loadOfficialFrameworks();
// other stuff here
});
The <import-elements> directive, looks like this:
angular.module('pcfApp').directive('importElements', function($state, $stateParams, $timeout, $window, Framework, OfficialFramework) {
var link = function(scope, el, attrs) {
scope.loadOfficialFrameworks = function() {
OfficialFramework.query(function(data) {
scope.officialFrameworks = data;
$(".ui.dropdown").dropdown({
onChange: function(value, text, $item) {
loadSections($item.attr("data-id"));
}
});
window.setTimeout(function() {
$(".ui.dropdown").dropdown('set selected', data[0]._id);
}, 0);
});
}
return {
link: link,
replace: true,
templateUrl: "app/importElements/components/import_elements_component.html"
}
});
I was under the impression that I'd be able to call the directive's loadOfficialFrameworks() method from my controller in this way (since I'm not specifying isolate scope), but I'm getting a method undefined error on the controller. What am I missing here?
The problem is that your controller function runs before your link function runs, so loadOfficialFrameworks is not available yet when you try to call it.
Try this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
//this will fail because loadOfficialFrameworks doesn't exist yet.
//$scope.loadOfficialFrameworks();
//wait until the directive's link function adds loadOfficialFrameworks to $scope
var disconnectWatch = $scope.$watch('loadOfficialFrameworks', function (loadOfficialFrameworks) {
if (loadOfficialFrameworks !== undefined) {
disconnectWatch();
//execute the function now that we know it has finally been added to scope
$scope.loadOfficialFrameworks();
}
});
});
Here's a fiddle with this example in action: http://jsfiddle.net/81bcofgy/
The directive scope and controller scope are two differents object
you should use in CTRL
$scope.$broadcast('loadOfficialFrameworks_event');
//And in the directive
scope.$on('loadOfficialFrameworks_event', function(){
scope.loadOfficialFrameworks();
})

How to broadcast events from directive to other controllers in AngularJS

I need somehow to emit event from part of the page (scrolling, clicking) that is served by one directive to other parts of the page, served by other controller so that it could be updated accordingly. Use case - for example Word document with annotations that are moving along with the page in the viewport.
SO in my design I have directive with link method in it and I need to broadcast events from it to other controllers in my app. What I have inside my link function:
element.bind('click', function (e) {
var eventObj = element.scrollTop();
scope.$broadcast('app.scrollOnDocument', eventObj);
});
This event cannot I cannot be see in other controllers directly - so code like this in other controller doesn't work:
$scope.$on('app.scrollOnDocument', function (e, params) {
console.log(e, params);
});
So what I have to do is to intercept those events in the same directive's controller and broadcast them to the higher scope like:
$scope.$on('app.scrollOnDocument', function(event, params){
//go further only if some_condition
if( some_condition ){
$rootScope.$broadcast('app.scrollOnDocumentOuter', params);
}
});
I am not sure this is the correct way of doing this. Maybe I am missing some directive property or setting to make it possible?
Non standard services can be passed to a directive like
.directive('notify', ['$rootScope', '$interval', function(rootScope, interval){
return {
restrict : 'E',
link : function(){
interval(function(){
rootScope.$broadcast('custom.event', new Date());
}, 1500);
}
};
}]);
The example below broadcasts an event every 1500ms.
If using the rootScope for communication cannot be avoided,you should always try unregistering the listener.
angular.module('app', [])
.controller('indexCtrl', ['$rootScope', '$scope',
function(rootScope, scope) {
scope.title = 'hello';
scope.captured = [];
var unregister = rootScope.$on('custom.event', function(evt, data) {
scope.captured.push(data);
});
scope.$on('$destroy', function() {
unregister();
});
}
])
.directive('notify', ['$rootScope', '$interval',
function(rootScope, interval) {
return {
restrict: 'E',
link: function() {
interval(function() {
rootScope.$broadcast('custom.event', new Date());
}, 1500);
}
};
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="indexCtrl">
<h1>{{title}}</h1>
<notify></notify>
<ul>
<li ng-repeat="event in captured">{{event|date:'medium'}}</li>
</ul>
</div>
For broadcasting in AngularJS, you always have to use $rootScope. You are listening always on $scope instead of $rootScope.

Resources