Call directive event handler emitted from parent controller - angularjs

I have a requirement like i want to emit an event from a controller and the event handler is present in directive controller (i.e child controller).
Please find the code below:
<div ng-app="myApp" ng-controller="appController">
<div test></div>
</div>
/* Fiddle to call views directive event from the view controller */
var app = angular.module('myApp', []);
app.controller('appController', function ($scope, $timeout) {
console.log('Inside Controller');
// If timeout is removed test-event handler will not be called as test controller is not yet executed by the time event was raised
//$timeout(function () {
$scope.$broadcast('test-event');
//});
});
app.directive('test', function () {
return {
restrict: 'A',
controller: function ($scope) {
console.log("Inside test directive controller");
$scope.$on('test-event', function () {
console.log("Test event fired");
});
}
};
});
If I un-comment $timeout in the above code, my code works fine. But i want to know some better solution rather than using above solution.
I have this scenario in my project in a view, where I have a view controller and directive with an event handler and emitting event from the view controller.

Related

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.

AngularJS - Access UI element in template page via directive code

I have the following AngularJS app
template.html
<!-- Complex HTML code which contains other input -->
<input
ng-keyup="enter($event)"
/>
<!-- Complex HTML code which contains other input -->
My directive code is as follow.
angular.module('myApp')
.directive('myDirective', function(appConfiguration) {
return {
templateUrl: 'template.html',
controller: function ($scope) {
$scope.enter = function(e) {
if (e.keyCode == 13) {
// Perform some network operation...
alert('How to make my input lost focus?');
}
};
}
}
I wish to make my input lost focus, when enter is pressed. However, I have no idea, how I can access input, when I'm in directive code.
Any idea?
Accessing Element:
You just need to inject $element in your directive's controller to access element.
Losing Focus on Enter
After accessing, you just need to listen keydown events in your directive's link function. And you should blur(unfocus) the element whenever user press enter.
angular.module('myApp', [])
.directive('loseFocus', function() {
return {
controller: function ($element) {
$element.bind("keydown keypress", function (event) {
if(event.which === 13) {
$element[0].blur();
}
});
}
}
});
And you don't need to pass ng-keyup parameter to directive, as it is directive's job to listen that event.
<input lose-focus>
Here is jsfiddle for it.
UPDATE
It seems that my-directive is used to create multiple inputs. Then, the best approach here would be separating logic. my-directive can continue producing inputs and lose-focus directive can handle blur event on enter key.
Updated jsFiddle is here.
just use following method signature to get hold of the element
controller: function ($scope, $element)
I believe you can use the controller or link function to pass the element and attributes.
Here's an example from AngularJS doc. You can do the same for controller function.
.directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function() {
updateTime(); // update DOM
}, 1000);
}
return {
link: link
};
}]);

angularjs controllerAs register $destroy

Documentation of angular says, to execute code to cleanup when the controller is destroyed, one should register for the $destroy event on the scope.
$scope.$on("$destroy", function() { ... } );
However, when you use the controllerAs syntax, you don't have access to $scope. How would you register the $destroy event ?
Just because you use the controllerAs syntax, it doesn't mean there is no $scope or that you can't use it.
In fact, all the controllerAs does is add the controller's this on the $scope (under the specified name). E.g.:
ng-controller="SomeCtrl as ctrl"
will implicitly do this:
.controller('SomeCtrl', function () {
this.value = 'something';
...
// Angular will implicitly so something equivalent to:
// $scope.ctrl = this;
}
So, there is nothing stopping you from using the $scope (it is actually quite useful for things like $watching stuff and sending/listening for events):
<!-- In the VIEW -->
<div ng-controller="SomeCtrl as ctrl">
Some value: {{ctrl.value}}
<button ng-click="ctrl.doSomething()">Some action</button>
</div>
/* In the CONTROLLER */
.controller('SomeCtrl', function ($scope) {
this.value = 'some value';
this.doSomething = function () { ... };
$scope.$on('$destroy', function () {
// So some clean-up...
});
});
See, also, this short demo.
It is also possible without injecting $scope:
.controller('SomeCtrl', function () {
var vm = this;
vm.$onDestroy = function(){
console.log( 'destroying....' );
}
}
You know there is one dependency $element, if you inject that in controller will give you the DOM where you have ng-controller directive placed. So inject $element inside your controller & then place listener over it like below
$element.on('$destroy', function(){
//write clean up code here
})

$observe within an Angular directive is only firing on the first attribute change

I'm trying to write a custom directive but the attribute the directive is observing only seems to change on the initial scope change. After that, the binding in the view (observed with Firebug) doesn't update any longer. This seems like a scope problem but I'm out of ideas.
JSFiddle Link showing code with problem: http://jsfiddle.net/catalyst156/2gp78/ (contents of fiddle below, but it might be useful to mess around with the fiddle itself).
Controller:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js></script>
<script>
var myApp = angular.module('myApp', []);
myApp.directive('testDirective', function () {
return {
restrict: 'A',
replace: true,
scope: {
toggle: '#'
},
link: function (scope, element, attrs) {
console.log('...linking testDirective...');
attrs.$observe('toggle', function (value) {
console.log('directive: toggle observer');
if ('true' === value) {
console.log('directive: toggle=true');
} else {
console.log('directive: toggle=false');
}
});
}
};
});
myApp.controller('myCtrl', ['$scope', '$log', function ($scope, $log) {
$scope.toggleState = false;
$scope.testToggle = function () {
$scope.toggleState = true;
$log.log('controller: toggleState=' + $scope.toggleState);
setTimeout(turnToggleOff, 2000);
};
function turnToggleOff() {
$scope.toggleState = false;
$log.log('controller: toggleState=' + $scope.toggleState);
}
}]);
HTML:
<div ng-app="myApp">
<div ng-controller="myCtrl">
<div test-directive toggle={{toggleState}}></div>
<button ng-click="testToggle()">Toggle Me</button>
</div>
</div>
Console Output:
1: ...linking testDirective...
2: directive: toggle observer
3: directive: toggle=false
4: controller: toggleState=true
5: directive: toggle observer
6: directive: toggle=true
7: controller: toggleState=false
I can see the link working and the initial state is set to false as expected (lines 1-3). The button is clicked and the observer is notified of the change (lines 4-6). However, when the timer expires and 'toggleState=false', the observer never picks up that change. (multiple button presses beyond only show the console output from the controller and the observer is never fired again).
The issue is that angular is unaware of changes made by a setTimeout(). Said another way- setTimeout() does not trigger a $digest cycle. Angular provides instead $timeout that does the same thing as setTimeout() but it triggers $digest so Angular sees the changes (which in turn will cause your observe to fire).
So just pass in the $timeout dependency:
myApp.controller('myCtrl', ['$scope', '$log', '$timeout', function ($scope, $log, $timeout) {...}
And switch your timeout call:
$timeout(turnToggleOff, 2000);
Here's an updated fiddle

Fail to catch event when call directive method from controller in Angular

I try to call directive method from controller by using $broadcast.
But I catch event only if press on button twice.
See Demo in Plunker
Do I miss something?
Here is snippets of code:
HTML
<div ng-controller="MainCtrl">
<test-dir name="{{test}}"></test-dir>
<select ng-model="broadcastTo" ng-options="x as x for x in ['test1', 'test2', 'test3']"></select>
<button ng-click="broadcastToSelectedChild()">test</button>
</div>
JS
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
// a method that broadcasts to a selected child.
$scope.broadcastToSelectedChild = function (){
$scope.test = $scope.broadcastTo;
console.log('call-' + $scope.test);
$scope.$broadcast('call-' + $scope.test);
};
});
app.directive('testDir', function (){
return {
restrict: 'E',
scope: {
'name': '#'
},
template: '<div>{{name}} called: {{called}}</div>',
link: function(scope, elem, attr) {
scope.called = false;
//set up the name to be used as the listened to event.
var eventOn;
scope.$watch('name', function(v) {
console.log('listen ..','call-' + scope.name);
if(eventOn){
eventOn();
}
eventOn = scope.$on('call-' + scope.name, function (){
alert(scope.name);
});
});
}
};
});
Took example from: HERE
Thanks,
It does not work because you bind your directive scope's name to your controller scope's test, but ng-model on the <select> binds to broadcastTo. When you select a value from the <select>, test is not updated and $watch inside your directive does not fire to attach the event handler.
Try:
<test-dir name="{{broadcastTo}}"></test-dir>
DEMO
In your code, you have to click twice because the first click updates test and causes $watch to fire to attach the event handler:
$scope.test = $scope.broadcastTo;
And the second click will be able to handle the event broadcast from your controller.

Resources