Variable in directive's view is not always updated - angularjs

When I call notificationService.showNotification("Test from controller") from a controller the message in the directive's view is updated but when I then call notificationService.showNotification("Test from another service") from another service the message in the directive's view doesn't get updated.
How should I do to always update currentNotification.message in the directive's view when currentNotification is updated in the notificationService?
Update:
The problem seems to occur when calling notificationService.showNotification("Test from another service") from a setTimeout function. See plunker: http://plnkr.co/edit/qD6OjamFjzOFbg43vj7R
Clicking on "Update from Other Service needs to be done twice for the message to update.
angular.module('myApp').service('notificationService',function() {
var notificationService = {
currentNotification : {
message: "",
},
showNotification : function(text){
this.currentNotification.message = text;
},
};
return notificationService;
});
angular.module('myApp').directive('notificationDirective', function(notificationService) {
return {
restrict: 'E',
replace: true,
scope: {
},
templateUrl: 'directive/notification-directive/notification-directive.html',
link: function(scope, element, attrs, fn) {
scope.currentNotification = notificationService.currentNotification;
}
};
});
directive/notification-directive/notification-directive.html:
<div>
<div ng-if="currentNotification.message.length > 0" id="notificationWrapper">
<div class="notification">
<div class="message">
{{currentNotification.message}}
</div>
</div>
</div>
</div>

You need to use $timeout which calls $scope.$apply, or you need to call $scope.$apply on your own :
myApp.controller('otherCtrl', function($scope, $timeout, notificationService, otherService) {
$scope.updateFromService = function() {
console.log("updateFromService");
$timeout(function(){
otherService.updateNotification();
}, 200);
};
});

Related

Rebinding this on angular callback from inside link funtion

I have a directive similar to this this:
app.directive('example', function() {
return {
restrict: 'E',
transclude: true,
scope: {
callback: '&'
},
template: '<span ng-click="example.callback()">Click Me</span>',
bindToController: true,
controllerAs: 'example',
controller: function() {
this.counter = 0;
this.incrementCount = function() {
this.counter++;
};
this.getCount = function() {
return this.counter;
};
},
link: function(scope, el, attrs, ctrl) {
var oldCallback = scope.callback;
ctrl.callback = function() {
console.log(ctrl);
return oldCallback.call(ctrl); // I want to be able to use `this` as the controller to access the API from within the callback
};
}
};
});
with a controller
app.controller("ctrl", ["$scope", function(s) {
s.callback = function() {
this.incrementCount();
console.log("Value: " + this.getCount());
};
}]);
And view
<div ng-app="app">
<div class="container" ng-controller="ctrl">
<example callback="callback()"></example>
</div>
</div>
(codepen)
When I log ctrl in within the ctrl.callback in the link function it logs the example controller as I expect but when oldCallback is called, it doesn't get ctrl rebound to this as I want. Is there any way to access the API defined in the directive's controller from within the callback on the scope while still using an isolate scope for the directive?
You could pass the directives controller out through the callback. e.g.
example html
<span ng-click="example.callback({$exampleCtrl:example})">Click Me</span>
index html
<example callback="callback($exampleCtrl)"></example>
controller
$scope.callback = function($exampleCtrl) {
$exampleCtrl.incrementCount();
console.log("Value: " + $exampleCtrl.getCount());
};
http://codepen.io/anon/pen/BzqqzV
Also note that bindToController is only supported in AngularJs 1.3+ and your code pen was using 1.2

Strange behaviour of Angular's $watch

Today I encounter some really strange behaviour in AngularJS using $watch. I simplified my code to the following example:
https://jsfiddle.net/yLeLuard/
This example contains a service which will keep track of a state variable. The directives are used to bind a click event to the DOM changing the state variable through the service.
There are two problems in this example:
The first close button (with the ng-click property on it) only changes the state on the second click
The two buttons without the ng-click are not changing the state at all
main.html
<div ng-controller="main">
State: {{ api.isOpen | json }}
<div ng-click="" open>
<button>Open - Working fine</button>
</div>
<div ng-click="" close>
<button>Close - Works, but only on second click</button>
</div>
<div open>
<button>Open - Not Working</button>
</div>
<div close>
<button>Close - Not Working</button>
</div>
</div>
main.js
var myApp = angular.module('myApp', []);
myApp.controller('main', ['$scope', 'state', function($scope, state) {
$scope.api = state;
$scope.api.isOpen = false;
$scope.$watch('api.isOpen', function() {
console.log('state has changed');
});
}]);
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Open</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = true;
});
}
};
}]);
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Close</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
});
}
};
}]);
myApp.service('state', function() {
return {
isOpen: null
};
});
That's because you are using a native event listener on click. This event is asynchronous and out of the Angular digest cycle, so you need to manually digest the scope.
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link: function(scope, element) {
element.on('click', function() {
scope.$apply(function() {
state.isOpen = true;
});
});
}
};
}]);
Fixed fiddle: https://jsfiddle.net/7h95ct1y/
I would suggest changing the state directly in the ng-click: ng-click="api.isOpen = true"
You should put your link function as a pre-link function, this solves the problem of the second click.
https://jsfiddle.net/2c9pv4xm/
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link:{
pre: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
console.log('click', state);
});
}
}
};
}]);
As for the not working part, you're putting your directive on the div, so when you click on the div it works but not on the button. Your open directive should be on the button.
Edit: other commenters suggested that you change the state directly in the ng-click. I wouldn't recommend it, it might work in this case but if you're bound to have more than an assignation to do it's not viable.

Accessing scope from within element.bind, within link function of directive

I am experiencing some problems within the link function of my directive. I am starting a new timeout within mousedown event bound to the element, then clearing it on the mouseup. The timeout is not clearing and also other variables I call on the scope are not updating within the element.bind functions. When I log to console, both functions are being triggered but the $scope doesn't seem to update until after the timeout has completed?
How can I make this work? JS fiddle here: http://jsfiddle.net/xrh6dhf9/
HTML
<div ng-app="dr" ng-controller="testCtrl">
<holddelete param="myDeletedMessage" update-fn="doCallback(msg)"></test>
JavaScript
var app = angular.module('dr', []);
app.controller("testCtrl", function($scope) {
$scope.myDeletedMessage = "Deleted Succesfully";
$scope.doCallback = function(msg) {
alert(msg);
}
});
app.directive('holddelete', ['$timeout', function( $timeout) {
return {
restrict: 'E',
scope: {
param: '=',
updateFn: '&'
},
template: "<a href> <i class='fa fa-times fa-fw'></i>Delete {{message}}</a>",
replace: true,
link: function($scope, element, attrs) {
$scope.mytimeout = null;
$scope.message = ">";
element.bind('mousedown', function (e) {
console.log("mousedown");
$scope.message = "- Hold 2 Secs";
$scope.mytimeout = $timeout(function(){
$scope.updateFn({msg: $scope.param});
}, 2000)
});
element.bind('mouseup', function (e) {
console.log("mouseup");
$scope.mytimeout = null;
$scope.message = ">";
})
}
}
}]);
Instead of setting timeout as null use
$timeout.cancel($scope.mytimeout);
Also instead of setting event handlers using element.bind pass execute methods in scope with ng-mousedown and ng-mouseup
Fiddle: http://jsfiddle.net/xrh6dhf9/1/

AngularJS directive not being updated with parent scope variable changes

I have a directive and a controller in my AngularJS app as shown below, where I need the directive to be updated with the controller scope variable changes.
Problem I am facing is that any change to the controller scope variable do not update the directive. I've tried using {scope: false}, tried making an isolated scope and one-way binding with the controller scope variable as shown below but none worked, so can someone please check my code and let me know what I am missing / doing wrong here? Thanks
First trial using isolated scope in directive
.directive('loginPanelDir', function() {
return {
restrict: 'A',
scope: {
loginStatus: "&userLoginStatus"
},
link: function(scope, element, attrs) {
console.log(scope.loginStatus()); //will always print 0 despite of changes to the scope var in controller
}
};
});
.controller('LoginController', function ($scope, $location) {
$scope.LoginStatus = "0";
$scope.clickMe = function(){
$scope.LoginStatus = "1";
};
});
<div id="login" login-panel-dir user-login-status="LoginStatus">
<button id="btnLogin" type="submit" ng-click="clickMe()">Login</button>
Second trial using {scope:false} in directive
.directive('loginPanelDir', function() {
return {
restrict: 'A',
scope: false,
link: function(scope, element, attrs) {
console.log(scope.LoginStatus()); //will always print 0 despite of changes to the scope var in controller
scope.$watch(function(){ scope.LoginStatus }, function(){
console.log('Login status : '+scope.LoginStatus); //will always return 0...
});
}
};
});
.controller('LoginController', function ($scope, $location) {
$scope.LoginStatus = "0";
$scope.clickMe = function(){
$scope.LoginStatus = "1";
};
});
<div id="login" login-panel-dir>
<button id="btnLogin" type="submit" ng-click="clickMe()">Login</button>
You don't have to use $timeouts or $intervals to watch changes for certain scope values. Inside your directive you can watch for the changes of your login status via watching the user-login-status attribute.
DEMO
Something like this:
JAVASCRIPT
.controller('LoginController', function($scope) {
$scope.LoginStatus = "0";
$scope.clickMe = function(){
$scope.LoginStatus = "1";
};
})
.directive('loginPanelDir', function() {
return function(scope, elem, attr) {
scope.$watch(attr.userLoginStatus, function(value) {
console.log(value);
});
}
});
HTML
<div id="login" login-panel-dir user-login-status="LoginStatus">
<button id="btnLogin" type="submit" ng-click="clickMe()">Login</button>
</div>
Working plunk.
Use $timeout not setTimeout:
setTimeout(function(){
$scope.LoginStatus = "1";
}, 3000);
should be:
$timeout(function(){
$scope.LoginStatus = "1";
}, 3000);
setTimeout is native Javascript, so it will not run a digest and angular won't be notified of the changes hence no updates to the bindings, $timeout runs a digest when it completes, forcing an update to the binding.
Well its working here, just realized your $watch is also wrong:
it should be:
scope.$watch("LoginStatus", function(){
console.log('Login status : '+scope.LoginStatus); //will always return 0...
});

How to call a method defined in an AngularJS directive?

I have a directive, here is the code :
.directive('map', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function($scope, element, attrs) {
var center = new google.maps.LatLng(50.1, 14.4);
$scope.map_options = {
zoom: 14,
center: center,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
// create map
var map = new google.maps.Map(document.getElementById(attrs.id), $scope.map_options);
var dirService= new google.maps.DirectionsService();
var dirRenderer= new google.maps.DirectionsRenderer()
var showDirections = function(dirResult, dirStatus) {
if (dirStatus != google.maps.DirectionsStatus.OK) {
alert('Directions failed: ' + dirStatus);
return;
}
// Show directions
dirRenderer.setMap(map);
//$scope.dirRenderer.setPanel(Demo.dirContainer);
dirRenderer.setDirections(dirResult);
};
// Watch
var updateMap = function(){
dirService.route($scope.dirRequest, showDirections);
};
$scope.$watch('dirRequest.origin', updateMap);
google.maps.event.addListener(map, 'zoom_changed', function() {
$scope.map_options.zoom = map.getZoom();
});
dirService.route($scope.dirRequest, showDirections);
}
}
})
I would like to call updateMap() on a user action. The action button is not on the directive.
What is the best way to call updateMap() from a controller?
If you want to use isolated scopes you can pass a control object using bi-directional binding = of a variable from the controller scope. You can also control also several instances of the same directive on a page with the same control object.
angular.module('directiveControlDemo', [])
.controller('MainCtrl', function($scope) {
$scope.focusinControl = {};
})
.directive('focusin', function factory() {
return {
restrict: 'E',
replace: true,
template: '<div>A:{{internalControl}}</div>',
scope: {
control: '='
},
link: function(scope, element, attrs) {
scope.internalControl = scope.control || {};
scope.internalControl.takenTablets = 0;
scope.internalControl.takeTablet = function() {
scope.internalControl.takenTablets += 1;
}
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="directiveControlDemo">
<div ng-controller="MainCtrl">
<button ng-click="focusinControl.takeTablet()">Call directive function</button>
<p>
<b>In controller scope:</b>
{{focusinControl}}
</p>
<p>
<b>In directive scope:</b>
<focusin control="focusinControl"></focusin>
</p>
<p>
<b>Without control object:</b>
<focusin></focusin>
</p>
</div>
</div>
Assuming that the action button uses the same controller $scope as the directive, just define function updateMap on $scope inside the link function. Your controller can then call that function when the action button is clicked.
<div ng-controller="MyCtrl">
<map></map>
<button ng-click="updateMap()">call updateMap()</button>
</div>
app.directive('map', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function($scope, element, attrs) {
$scope.updateMap = function() {
alert('inside updateMap()');
}
}
}
});
fiddle
As per #FlorianF's comment, if the directive uses an isolated scope, things are more complicated. Here's one way to make it work: add a set-fn attribute to the map directive which will register the directive function with the controller:
<map set-fn="setDirectiveFn(theDirFn)"></map>
<button ng-click="directiveFn()">call directive function</button>
scope: { setFn: '&' },
link: function(scope, element, attrs) {
scope.updateMap = function() {
alert('inside updateMap()');
}
scope.setFn({theDirFn: scope.updateMap});
}
function MyCtrl($scope) {
$scope.setDirectiveFn = function(directiveFn) {
$scope.directiveFn = directiveFn;
};
}
fiddle
Although it might be tempting to expose an object on the isolated scope of a directive to facilitate communicating with it, doing can lead to confusing "spaghetti" code, especially if you need to chain this communication through a couple levels (controller, to directive, to nested directive, etc.)
We originally went down this path but after some more research found that it made more sense and resulted in both more maintainable and readable code to expose events and properties that a directive will use for communication via a service then using $watch on that service's properties in the directive or any other controls that would need to react to those changes for communication.
This abstraction works very nicely with AngularJS's dependency injection framework as you can inject the service into any items that need to react to those events. If you look at the Angular.js file, you'll see that the directives in there also use services and $watch in this manner, they don't expose events over the isolated scope.
Lastly, in the case that you need to communicate between directives that are dependent on one another, I would recommend sharing a controller between those directives as the means of communication.
AngularJS's Wiki for Best Practices also mentions this:
Only use .$broadcast(), .$emit() and .$on() for atomic events
Events that are relevant globally across the entire app (such as a user authenticating or the app closing). If you want events specific to modules, services or widgets you should consider Services, Directive Controllers, or 3rd Party Libs
$scope.$watch() should replace the need for events
Injecting services and calling methods directly is also useful for direct communication
Directives are able to directly communicate with each other through directive-controllers
Building on Oliver's answer - you might not always need to access a directive's inner methods, and in those cases you probably don't want to have to create a blank object and add a control attr to the directive just to prevent it from throwing an error (cannot set property 'takeTablet' of undefined).
You also might want to use the method in other places within the directive.
I would add a check to make sure scope.control exists, and set methods to it in a similar fashion to the revealing module pattern
app.directive('focusin', function factory() {
return {
restrict: 'E',
replace: true,
template: '<div>A:{{control}}</div>',
scope: {
control: '='
},
link : function (scope, element, attrs) {
var takenTablets = 0;
var takeTablet = function() {
takenTablets += 1;
}
if (scope.control) {
scope.control = {
takeTablet: takeTablet
};
}
}
};
});
To be honest, I was not really convinced with any of the answers in this thread. So, here's are my solutions:
Directive Handler(Manager) Approach
This method is agnostic to whether the directive's $scope is a shared one or isolated one
A factory to register the directive instances
angular.module('myModule').factory('MyDirectiveHandler', function() {
var instance_map = {};
var service = {
registerDirective: registerDirective,
getDirective: getDirective,
deregisterDirective: deregisterDirective
};
return service;
function registerDirective(name, ctrl) {
instance_map[name] = ctrl;
}
function getDirective(name) {
return instance_map[name];
}
function deregisterDirective(name) {
instance_map[name] = null;
}
});
The directive code, I usually put all the logic that doesn't deal with DOM inside directive controller. And registering the controller instance inside our handler
angular.module('myModule').directive('myDirective', function(MyDirectiveHandler) {
var directive = {
link: link,
controller: controller
};
return directive;
function link() {
//link fn code
}
function controller($scope, $attrs) {
var name = $attrs.name;
this.updateMap = function() {
//some code
};
MyDirectiveHandler.registerDirective(name, this);
$scope.$on('destroy', function() {
MyDirectiveHandler.deregisterDirective(name);
});
}
})
template code
<div my-directive name="foo"></div>
Access the controller instance using the factory & run the publicly exposed methods
angular.module('myModule').controller('MyController', function(MyDirectiveHandler, $scope) {
$scope.someFn = function() {
MyDirectiveHandler.get('foo').updateMap();
};
});
Angular's approach
Taking a leaf out of angular's book on how they deal with
<form name="my_form"></form>
using $parse and registering controller on $parent scope. This technique doesn't work on isolated $scope directives.
angular.module('myModule').directive('myDirective', function($parse) {
var directive = {
link: link,
controller: controller,
scope: true
};
return directive;
function link() {
//link fn code
}
function controller($scope, $attrs) {
$parse($attrs.name).assign($scope.$parent, this);
this.updateMap = function() {
//some code
};
}
})
Access it inside controller using $scope.foo
angular.module('myModule').controller('MyController', function($scope) {
$scope.someFn = function() {
$scope.foo.updateMap();
};
});
A bit late, but this is a solution with the isolated scope and "events" to call a function in the directive. This solution is inspired by this SO post by satchmorun and adds a module and an API.
//Create module
var MapModule = angular.module('MapModule', []);
//Load dependency dynamically
angular.module('app').requires.push('MapModule');
Create an API to communicate with the directive. The addUpdateEvent adds an event to the event array and updateMap calls every event function.
MapModule.factory('MapApi', function () {
return {
events: [],
addUpdateEvent: function (func) {
this.events.push(func);
},
updateMap: function () {
this.events.forEach(function (func) {
func.call();
});
}
}
});
(Maybe you have to add functionality to remove event.)
In the directive set a reference to the MapAPI and add $scope.updateMap as an event when MapApi.updateMap is called.
app.directive('map', function () {
return {
restrict: 'E',
scope: {},
templateUrl: '....',
controller: function ($scope, $http, $attrs, MapApi) {
$scope.api = MapApi;
$scope.updateMap = function () {
//Update the map
};
//Add event
$scope.api.addUpdateEvent($scope.updateMap);
}
}
});
In the "main" controller add a reference to the MapApi and just call MapApi.updateMap() to update the map.
app.controller('mainController', function ($scope, MapApi) {
$scope.updateMapButtonClick = function() {
MapApi.updateMap();
};
}
You can specify a DOM attribute that can be used to allow the directive to define a function on the parent scope. The parent scope can then call this method like any other. Here's a plunker. And below is the relevant code.
clearfn is an attribute on the directive element into which the parent scope can pass a scope property which the directive can then set to a function that accomplish's the desired behavior.
<!DOCTYPE html>
<html ng-app="myapp">
<head>
<script data-require="angular.js#*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<style>
my-box{
display:block;
border:solid 1px #aaa;
min-width:50px;
min-height:50px;
padding:.5em;
margin:1em;
outline:0px;
box-shadow:inset 0px 0px .4em #aaa;
}
</style>
</head>
<body ng-controller="mycontroller">
<h1>Call method on directive</h1>
<button ng-click="clear()">Clear</button>
<my-box clearfn="clear" contentEditable=true></my-box>
<script>
var app = angular.module('myapp', []);
app.controller('mycontroller', function($scope){
});
app.directive('myBox', function(){
return {
restrict: 'E',
scope: {
clearFn: '=clearfn'
},
template: '',
link: function(scope, element, attrs){
element.html('Hello World!');
scope.clearFn = function(){
element.html('');
};
}
}
});
</script>
</body>
</html>
Just use scope.$parent to associate function called to directive function
angular.module('myApp', [])
.controller('MyCtrl',['$scope',function($scope) {
}])
.directive('mydirective',function(){
function link(scope, el, attr){
//use scope.$parent to associate the function called to directive function
scope.$parent.myfunction = function directivefunction(parameter){
//do something
}
}
return {
link: link,
restrict: 'E'
};
});
in HTML
<div ng-controller="MyCtrl">
<mydirective></mydirective>
<button ng-click="myfunction(parameter)">call()</button>
</div>
You can tell the method name to directive to define which you want to call from controller but without isolate scope,
angular.module("app", [])
.directive("palyer", [
function() {
return {
restrict: "A",
template:'<div class="player"><span ng-bind="text"></span></div>',
link: function($scope, element, attr) {
if (attr.toPlay) {
$scope[attr.toPlay] = function(name) {
$scope.text = name + " playing...";
}
}
}
};
}
])
.controller("playerController", ["$scope",
function($scope) {
$scope.clickPlay = function() {
$scope.play('AR Song');
};
}
]);
.player{
border:1px solid;
padding: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="playerController">
<p>Click play button to play
<p>
<p palyer="" to-play="play"></p>
<button ng-click="clickPlay()">Play</button>
</div>
</div>
TESTED
Hope this helps someone.
My simple approach (Think tags as your original code)
<html>
<div ng-click="myfuncion">
<my-dir callfunction="myfunction">
</html>
<directive "my-dir">
callfunction:"=callfunction"
link : function(scope,element,attr) {
scope.callfunction = function() {
/// your code
}
}
</directive>
Maybe this is not the best choice, but you can do angular.element("#element").isolateScope() or $("#element").isolateScope() to access the scope and/or the controller of your directive.
How to get a directive's controller in a page controller:
write a custom directive to get the reference to the directive controller from the DOM element:
angular.module('myApp')
.directive('controller', controller);
controller.$inject = ['$parse'];
function controller($parse) {
var directive = {
restrict: 'A',
link: linkFunction
};
return directive;
function linkFunction(scope, el, attrs) {
var directiveName = attrs.$normalize(el.prop("tagName").toLowerCase());
var directiveController = el.controller(directiveName);
var model = $parse(attrs.controller);
model.assign(scope, directiveController);
}
}
use it in the page controller's html:
<my-directive controller="vm.myDirectiveController"></my-directive>
Use the directive controller in the page controller:
vm.myDirectiveController.callSomeMethod();
Note: the given solution works only for element directives' controllers (tag name is used to get the name of the wanted directive).
Below solution will be useful when, you are having controllers (both parent and directive (isolated)) in 'controller As' format
someone might find this useful,
directive :
var directive = {
link: link,
restrict: 'E',
replace: true,
scope: {
clearFilters: '='
},
templateUrl: "/temp.html",
bindToController: true,
controller: ProjectCustomAttributesController,
controllerAs: 'vmd'
};
return directive;
function link(scope, element, attrs) {
scope.vmd.clearFilters = scope.vmd.SetFitlersToDefaultValue;
}
}
directive Controller :
function DirectiveController($location, dbConnection, uiUtility) {
vmd.SetFitlersToDefaultValue = SetFitlersToDefaultValue;
function SetFitlersToDefaultValue() {
//your logic
}
}
html code :
<Test-directive clear-filters="vm.ClearFilters"></Test-directive>
<a class="pull-right" style="cursor: pointer" ng-click="vm.ClearFilters()"><u>Clear</u></a>
//this button is from parent controller which will call directive controller function

Resources