My controller is not catching the event sent from a directive - javascript

I have a directive that broadcasts an event when a table row gets clicked. Here is the directive:
angular.module('app').directive('encounterItemTable', function () {
return {
restrict: 'A',
replace: true,
templateUrl: 'views/encounter.item.table.html',
scope: {
encounters : '='
},
link: function(scope) {
scope.getSelectedRow = function(index) {
scope.$broadcast('selectedRow', { rowIndex: index });
};
}
};
});
Here is the markup that calls the getSelectedRow
<tr ng-class="{selected: $index==selectedIndex}" ng-repeat="encounter in encounters | filter:search" data-id="{{encounter.id}}" ng-click="getSelectedRow($index)">
My getSelectedRow() function gets called when the row gets clicked. The index is correct. The controller on the other hand never hears anything. Here is the code in the controller.
$scope.$on('selectedRow', function(event, data) {
$scope.selectedIndex = data.rowIndex;
$scope.selectedEncounter = $scope.encounters[data.rowIndex];
});
Why would the controller not hear the event? What am I missing?

I use $rootScope.$broadcast(event, data). I use events to decouple components i.e. components emitting events and listeners for events don't need to know about each other.
In your case where the event could reasonably contained to the component (directive) then you have to care about where in the DOM the relative positions of the listener/emitter are. I haven't run into this myself so generally use $rootScope.$broadcast() another benefit being any component in the app can listen to these events so something in a sidebar could update in relation to the events from the table (which probably not be in the same DOM hierarchy)

It is $rootScope.$broadcast. If it were to broadcast only to the current scope, your controller wouldn't see it.

$broadcast sends events down to children. directive is nested within controller so need to send event up to a parent. Use scope.$emit to push events up through parents. Read the section in scope docs titled Scope Events Propagation

Related

What is the difference between $rootScope vs $rootScope.$emit/$broadcast in AngularJS?

This is my page's structure.
// app.html
<wrapper ng-if="initialized && $root.user.type!='guest'">
<header-component></header-component>
<div class="app-body">
<sidebar-component></sidebar-component>
<main class="main-content" style="height:{{$root.pageHeight}}px; overflow-y: scroll">
<ng-view></ng-view>
</main>
<aside-component></aside-component>
</div>
</wrapper>
Now in ng-view directive I have a controller which needs to pass data to the header-component.
As you can see, ng-view is not associated to header-component in some way.
Let's say that ng-view controll now screen is:
// some-screen.js
$scope.foo = "bar";
And I want to display bar in the header.
I can do this both with $rootScope (without any event) or using the $broadcast event.
First method - using the $rootScope - as it is - without just anything:
// some-screen.js
$rootScope.foo = "bar";
// header.js
app.directive("headerComponent", ($rootScope) => {
return {
templateUrl: "/structure/header/header.html",
scope: {},
link: function($scope, element, attrs) {
console.log($rootScope.foo) // "bar"
}
}
});
Second method - using the $broadcast event
// some-screen.js
$rootScope.$emit("SomeNameOfTheEvent", $scope.foo);
// header.js
app.directive("headerComponent", ($rootScope) => {
return {
templateUrl: "/structure/header/header.html",
scope: {},
link: function($scope, element, attrs) {
$rootScope.$on("SomeNameOfTheEvent", function(event, info) {
console.log(info.foo) // "bar"
});
}
}
});
Now notice two things while using the $broadcast event:
You need to specify name for this event - in big app this can be tricky - since
you probably ain't going to remember the names you throw while coding.
And sitting and think of good names is a waste of time.
You will probably need to make a documentation in order to re-use the event name from other places
in the app - otherwise you will mistakely try to use the same event but with wrong names.
They are both doing the same - $broadcast just takes more code to function.
What am I missing, AngularJS probably created the $broadcast event for something.
$emit dispatches an event upwards ... $broadcast dispatches an event
downwards
Detailed explanation
$rootScope.$emit only lets other $rootScope listeners catch it. This is good when you don't want every $scope to get it. Mostly a high level communication. Think of it as adults talking to each other in a room so the kids can't hear them.
$rootScope.$broadcast is a method that lets pretty much everything hear it. This would be the equivalent of parents yelling that dinner is ready so everyone in the house hears it.
$scope.$emit is when you want that $scope and all its parents and $rootScope to hear the event. This is a child whining to their parents at home (but not at a grocery store where other kids can hear).
$scope.$broadcast is for the $scope itself and its children. This is a child whispering to its stuffed animals so their parents can't hear.

AngularJS ng-init $broadcast called before listeners registered

I am trying to broadcast initial values to an indeterminate number of listeners with $broadcast
I am calling $rootScope.$broadcast with ng-init. The broadcast works correctly on page load but the listeners are not registered. Subsequent calls are picked up correctly, however.
The html;
<div class="panel panel-body" ng-controller="Location" ng-init="setActiveLocation({{ defaultLocation }})">
The method;
$scope.setActiveLocation = function(location) {
console.log('broadcast:', location);
$rootScope.$broadcast('setActiveLocation', location);
};
A listener;
$scope.$on('setActiveLocation', function(event, location)
{
console.log('recieved: ', event)
$scope.clearHistory();
$scope.refreshHistory(location);
});
My question is this; ng-init should be called when the page is ready, so why is it called prior to listeners being ready? What should I do to prevent this?
"ng-init should be called when the page is ready". This assumption is wrong. Look at the ngInitDirective (currently angular 1.4.7).
var ngInitDirective = ngDirective({
priority: 450,
compile: function() {
return {
pre: function(scope, element, attrs) {
scope.$eval(attrs.ngInit);
}
};
}
});
It is executed during the pre-link phase and has othing to do with the state of state of the page.
So i guess your set up is like this:
<a ng-init="someFunctionInA()">
<b ng-init="someFunctionInB()"></b>
</a>
If the controller attached to a does the $broadcast and the listener is registered in the controller attached to b, then your broadcast is fired before the listener is even registered.
Solution:
Do not use ng-init. Call the init function from your directive. Make the call of your broadcast function from the post-link function of directive a. While pre-link gets executed from outer to inner directive, post-link does vice-versa and so your inner controllers (with the listeners) will be ready before the outer ones do the broadcast.

ngRepeat track by: How to add event hook on model change?

I have a simple ngRepeat like the following:
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
arrayOfRecords is updated from a server and may contain new data.
ngRepeat's track by feature can figure out when a new element is added to the array and automatically updates the DOM without changing the existing elements. I would like to hook into that code and execute a callback function when there's new data coming in or old data is removed. Is it possible to easily do this via Angular?
From what I understand, there's a $$watchers which triggers callbacks whenever there's changes to certain variables, but I don't know how to go about hacking that. Is this the right direction?
NOTE: I know I can manually save the arrayOfRecords and compare it with the new values when I fetch them to see what changed. However, since Angular already offers a track by feature which has this logic, it would be nice if I can have Angular automatically trigger an event callback when an element is added or removed from the array. It doesn't make sense to duplicate this logic which already exists in Angular.
Probably you could create a directive and add it along with ng-repeat, so the directive when created(when item is added by ng-repeat) will emit an event and similarly when the item is destroyed it will emit another event.
A simple implementation here:
.directive('tracker', function(){
return{
restrict:'A',
link:function(scope, el, attr){
scope.$emit('ITEM_ADDED', scope.$eval(attr.tracker))
scope.$on('$destroy', function(){
scope.$emit('ITEM_REMOVED', scope.$eval(attr.tracker))
});
}
}
});
and use it as:
<some-element
ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id"
tracker="item">
and listen for these events at the parent controller for example.
Demo
Or using function binding but in a different way, without using isolate scope for that.
.directive('tracker', function() {
return {
restrict: 'A',
link: function(scope, el, attr) {
var setter = scope.$eval(attr.tracker);
if(!angular.isFunction(setter)) return;
setter({status:'ADDED', item:scope.$eval(attr.trackerItem)});
scope.$on('$destroy', function() {
setter({status:'REMOVED', item:scope.$eval(attr.trackerItem)});
})
}
}
});
Demo
The one above was specific to your question since there is no other built in way, Note that if you were to really find out the items added/removed, you could as well do it in your controller by diffing the 2 lists. You could try use lodash api like _.unique or even simple loop comparisons to find the results.
function findDif(oldList,newList){
return {added:_.uniq(newList, oldList), removed:_.uniq(oldList, newList)};
}
Demo
You can change it to:
<div ng-model="arrayOfRecords">
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
</div>
The model will change as soon as arrayOfRecords will change.

Using .on in directive VS in controller

What would be considered as best practice, attaching directive to element or binding event inside the controller?
Directive
<openread-more what-to-expand="teds-bets-readmore" />
myApp.directive('openreadMore', function () {
return {
restrict: 'AE',
replace: false,
template: '<a class="attach-event" what-to-expand="readmore1">Event</a></span>',
link: function (scope, elem, attrs) {
elem.on('click', function () {
// attached code on click
});
}
}
});
Just attaching it inside the controller
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
angular.element(document.querySelectorAll('.attach-event')).on('click', function () {
// attached code on click
});
});
The second option seems shorter and much cleaner, but i don't know if it's considered as best practice or not.
Just use the ng-click directive.
<openread-more what-to-expand="teds-bets-readmore" ng-click="doSomeAction()" />
And on the controller:
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
$scope.doSomeAction = function() {
// onClick logic here...
};
});
Edit
In case you are binding other kind of events, just make this question to yourself: "Will I have different behaviours for this event depending on the current view or application state?".
If the answer is yes then you should register the event handlers on the controllers. If the answer is no (which means you will have always the same behaviour) then register and handle the events on the directive.
Nevertheless, you should not access UI elements on the controllers (e.g. don't use selectors or anything similar). The controllers are supposed to be reusable, which means you should be able to use them on different UIs, with different UI elements. The best approach is to define a directive that allows you to bind specific events, like Angular UI Event Binder.

Angular Directive to Directive call

If you have a directive that you're using multiple times on a page how can 1 directive communicate with another?
I'm trying to chain directives together in a parent child relationship. When directive A is clicked i want to filter Directive B to only have the children of the selected item in Directive A. In this case there may be infinite number of directives and relationships on the page.
Normally i would have Directive A call a filter method on each of it's children, and each child calls it's child to continue filtering down the hierarchy.
But i can't figure out if calling methods from 1 directive to another is possibe.
Thanks
It sounds like you are looking for a directive controller. You can use the require: parameter of a directive to pull in another directive's controller. It looks like this:
app.directive('foo', function() {
return {
restrict: 'A',
controller: function() {
this.qux = function() {
console.log("I'm from foo!");
};
},
link: function(scope, element, attrs) {
}
};
});
app.directive('bar', function() {
return {
restrict: 'A',
require: '^foo',
link: function(scope, element, attrs, foo) {
foo.qux();
}
};
});
From the angular docs, here are the symbols you can use with require and what they do.
(no prefix) - Locate the required controller on the current element.
? - Attempt to locate the required controller, or return null if not found.
^ - Locate the required controller by searching the element's parents.
?^ - Attempt to locate the required controller by searching the element's parents, or return null if not found.
Here's a jsbin of my example. http://jsbin.com/aLikEF/1/edit
Another option that may work for what you need is to have a service that each directive sets up a watch on and can manipulate. For example, directive1 may watch a property in the service and respond to changes and also setup a button that can change that property. Then, directive2 can also watch and change the service, and they will respond to one another however you set that up. If you need a jsbin of that also, just let me know.
I hope this helps!
You could try putting all of the data into a service that the directives can each reference.
Something like:
app.factory('selectedStuffService', function(){
var allItems = [];
var selectedItems = [];
function addSelectedItem(item){
selectedItems.push(item);
}
return {
allItems: allItems,
selectedItems: selectedItems,
addSelectedItem: addSelectedItem
}
}
Interactions in directive A change the values in the selectedItems array and directive B can bind to it. You can easily add other methods to the service to filter/manipulate the items as needed and any directive that uses the service should be able to update based on changes made by other directives.

Categories

Resources