Angular - receiving info from controller inside directive - angularjs

I have a directive which is responsible to render audio and video items on a page:
myApp.directive('videoitem', function($compile){
return {
replace: true,
templateUrl: '/templates/directives/videoitem.html',
scope: {
videoChunkItem: "=",
videostartedCallback: "&videostarted",
videoendedCallback: "&videoended",
},
link: function($scope, $element, $attributes){
var startVideo = function(){
$element[0].load();
$element[0].play();
$scope.videostartedCallback();
};
$element.bind("ended", function(){
$scope.videoendedCallback();
$scope.$apply();
});
/**
* Here I am using on click event to start the video
* but the real case is that the trigger for video playing should come from controller.
* Any idea how to do it?
*/
$element.on('click', function(){
startVideo();
});
}
};
});
Is it possible from a route controller to send some event or something else to communicate from controller to directive to call startVideo method?
The flow of communication is from route controller to directive ... When in a controller occure some event I want to invoke directive's startVideo method.

Without knowing your exact use-case, my approach would be to define a new scope property, e.g. play : "=" (to set it to stop when the video ends), and $observe attrs.play or $scope.$watch "play" it. In your template you bind it to a $scope variable that is triggered by the controller.
<video-item play="videoControls.play"></video-item>
Here's a plunker where you use two-way binding, note how you don't have to do anything:
http://plnkr.co/edit/3OI801KcvYVoySDvcm8i?p=preview
If you want to allow expressions, you have to observe the attribute to get the interpolated value:
http://plnkr.co/edit/gi6FSPPlN599CtDjKBOb?p=preview

Related

Angular event or command that signals the end of processing child directives

I'm currently coding a tab navigation example to gain practical experience with Angular. This example uses custom directives and controller inheritance.
Plunker can be found here.
The issue: once the directives have finished processing I'd like to select a default tab to display. But at the point of calling the selectTab method of the myTabs controller (line 41 in script.js $ctrl.selectTab(0)), Angular hasn't yet finished processing the myTab directive (which generates the tab links), so the tabs array is empty and the selection fails.
I tried using $timeout without a delay, but that fails. $timeout only works with a delay of say 500ms set, which is hacky.
Is there an event or command available that signals the end of Angular processing certain directives, particularly directives that inherit from one another?
What I suspect is happening:
The myTabs directive finishes processing then fires its link function, but it's fired before the myTab directive is done processing. I can't place the selectTab method call within the myTab link function because would get called multiple times (based on the number of tabs processed.) Hope I'm explaining this clearly...I need a massage
app.directive('myTabs', ['$timeout', function($timeout) {
return {
restrict: 'E',
transclude: true,
controllerAs: 'myTabsCtrl',
templateUrl: 'my-tabs.htm',
scope: {},
controller: function ($scope) {
vm = this;
this.tabs = [];
this.addTab = function (tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab (tabIndex) {
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i].selected = (i === tabIndex ? true : false);
}
};
},
link: function ($scope, $element, $attrs, $ctrl) {
$timeout(function () {
$ctrl.selectTab(0);
});
}
};
}]);
app.directive('myTab', function() {
return {
restrict: 'E',
require: '^^myTabs',
transclude: true,
templateUrl: 'my-tab.htm',
scope: {
title: '#'
},
link: function (scope, element, attrs, ctrl){
scope.tab = {
title: scope.title,
selected: false
};
ctrl.addTab(scope.tab);
}
};
});
Each individual tab is registering itself with the parent directive controller. Simply set the selected flag on the first tab when it registers itself:
this.tabs = [];
this.addTab = function (tab) {
console.log("add tab ", tab);
if (!this.tabs.length) {
//Set flag for first tab to register
tab.selected = true;
};
this.tabs.push(tab);
};
The DEMO on PLNKR
AngularJS 1.5.3 introduced the $postLink life-cycle hook:
Life-cycle hooks
Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the directive:
$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs.
--AngularJS $compile Service API Reference -- Life-cycle hooks
The $postLink life-cycle hook won't work in this case because the directives use templateUrl. It also won't work with directives that build DOM after postlink; ng-repeat, ng-if, ng-include, etc.

Angularjs directive two-way bound variable changes are not triggering $digest on the parent scope

Apologies if some of the JS is syntactically off. I wrote it while looking at my CoffeeScript
I have a text editor that I've extracted into a directive and I want to share some state between it and its containing template:
Main containing template
<div class="content">
<editor class="editor" ng-model="foo.bar.content" text-model="foo.bar"></editor>
</div>
Template Controller
angular.module('foo').controller('fooController', ['$scope', ... , function ($scope, ...) {
$scope.foo = {}
$scope.foo.bar = {}
$scope.foo.bar.content = 'starting content'
$scope.$watch('foo.bar', function () {
console.log('content changed')
}, true)
}
The template two-way binds on its scope object $scope.foo.bar with the editor directive. When the text is changed, the editor's 'text-change' handler is fired and a property on the bound object is changed.
Editor Directive
angular.module('foo').directive('editor'), function (
restrict: 'E',
templateUrl: 'path/to/editor.html',
require: 'ng-model',
scope: {
textModel: '='
},
controller: [
...
$scope.editor = 'something that manages the text'
...
],
link: function (scope, ...) {
scope.editor.on('text-change', function () {
$scope.textModel.content = scope.editor.getText()
// forces parent to update. It only triggers the $watch once without this
// scope.$parent.$apply()
}
}
However, changing this property in the directive seems not to be hitting the deep $watch I've set on foo.bar. After some digging, I was able to use the directive's parent reference to force a $digest cycle scope.$parent.$apply(). I really shouldn't need to though, since the property is shared and should trigger automatically. Why does it not trigger automatically?
Here are some good readings that I've encountered that are pertinent:
$watch an object
https://www.sitepoint.com/understanding-angulars-apply-digest/
The on function is a jqLite/jQuery function. It will not trigger digest cycle. It is basically outside the angular's conscious. You need to manually trigger digest cycle using $apply.

Dependencies between angular directives

I've created a small app that has 2 directives: One that adds a google map div and initialize it, and the second, which shows layers that contain markers. The directives are independent, but I want them to communicate: The layers directive is independent, yet it needs to use the google maps directive to add markers to the map. I used $broadcast through $rootScope to communicate between the directives.
The directives are defined as follows:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen for event fired when markers are added
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
angular.module('layersDirective', [])
.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScope){
// Get layers of markers, etc.
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
After this long prologue here are my questions:
How should the connection between the two directives be implemented? Is it correct to use $rootScope and $broadcast or should there be a dependency between the myLayers directive and the myGoogleMap directive?
Furthermore, I've read about when to use controller, link and compile, yet I don't know the right way to use them here. My guess is that myGoogleMap should define its API in a controller and that myLayers should be dependent on myGoogleMap.
The example I wrote here works fine in my application. I'm looking for guidance on how to do it right and to understand what I did wrong here.
Thanks, Rotem
There are some ways for directives to cooperate/communicate
If one is a sibling or child of the other, you can use require. E.g. for this template:
<dir1>
<dir2></dir2>
</dir1>
Use this code:
app.directive('dir1', function() {
return {
...
controller: function() {
// define api here
}
};
});
app.directive('dir2', function() {
return {
...
require: '^dir1',
link: function(scope, elem, attrs, dir1Controller) {
// use dir1 api here
}
};
});
A service, used by both directives to communicate. This is easy and works well if the directives can only be instantiated once per view.
Using $broadcast/$emit on the $rootScope (there is a slight difference: $broadcast will "flood" the scope hierarchy, possibly affecting performance; $emit will only call listeners on the $rootScope, but this means you have to do $rootScope.$on() and then remember to deregister the listener when the current scope is destroyed - which means more code). This approach is good for decoupling components. It may become tricky in debugging, i.e. to find where that event came from (as with all event-based systems).
Other
controller, link and compile
Very short:
Use the controller to define the API of a directive, and preferably to define most its logic. Remember the element, the attributes and any transclude function are available to the controller as $element, $attrs and $transclude respectively. So the controller can, in most cases, replace the link function. Also remember that, unlike the link function, the controller is elligible for dependency injection. (However you can still do dependency injection at the directive level, so, after all, the link function can also access dependencies.)
Use the link function to access required controllers (see case 1 above). Or, if you are feeling lazy, to define the directive's logic. I think the controller is cleaner though.
Use the compile function... rarely :) When you need very special transformations to the template (repetition is the first thing that comes to mind - see ng-repeat) or other mystical stuff. I use directives all the time, about 1% of them needs a compile function.
My guess is that myGoogleMap should define its API in the controller and that myLayers should be dependent on myGoogleMap
The question is how will you communicate this API using events? You probably need only to create an API in a custom event object. The listeners of your event will be using that custom event. If so, the controller does not really need to define an API.
As a bottom line, I am perfectly OK with using events in your case.
Generally communication between directives should be handled via controllers and using the require property on the directive definition object.
If we re-work your first directive we can simply add that method to the controller:
directive('myGoogleMap', function () {
return {
template: '<div id="map" />',
controller: function ($scope) {
var _this = this;
//Part of this directives API
_this.addMarkers = function(arg){
//Do stuff
}
}
};
});
Now we can require this controller in another directive, but one little known features is that you can actually require an array of directives. One of those directives can even be yourself.
All of them will be passed, in order, as an array to your link function:
directive('myLayers', function () {
return {
templateUrl: 'someLayersHtml.html',
controller: function ($http, $scope, $rootScore) {
// Some get layers of markers functionality
},
// Require ourselves
// The '?' makes it optional
require: ['myLayers', '?myGoogleMap'],
link: function(scope, elem, attrs, ctrls){
var myLayersCtrl = ctrls[0];
var myGoogleMapCtrl = ctrls[1];
//something happens
if(myGoogleMapCtrl) {
myGoogleMapCtrl.addMarkers(markers);
}
}
};
});
Now you can communicate explicitly opt-in by using the ? which makes the controller optional.
In order for that to work, you have to define both directives in the same module, i.e.:
var module = angular.module('myModule');
module.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen to event for adding markers
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
module.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScore){
// Some get layers of markers functionality
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
Read more here.
EDIT:
Sorry i didn't understand your question, but according to your comment, quoting from the AngularJs Best Practices:
Only use .$broadcast(), .$emit() and .$on() for atomic 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
You have already highlight one may for the directives to communicate using rootscope.
Another way directive can communicate if they are defined on the same html hierarchy is by use directive controller function. You have highlighted that too in your question. The way it would be done is (assuming myGoogleMap is defined on parent html), the two directive definitions become:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function () {
template: '<div id="map" />',
controller: function ($scope) {
this.addMarkersEvent = function (data) {}
// some initializations
}
angular.module('layersDirective', [])
.directive('myLayers', function ($http, $rootScope) {
templateUrl: 'someLayersHtml.html',
require: '^myGoogleMap',
link: function (scope, element, attrs, myGoogleMapController) {
$scope.doWork = function () {
myGoogleMapController.addMarkersEvent(data);
}
}
});
Here you use the require property on the child directive. Also instead of using child controller all the functionality in the child directive is now added to link function. This is because the child link function has access to parent directive controller.
Just a side note, add both directives to a single module (Update: Actually you can have the directives in different modules, as long as there are being referenced in the main app module.)

Directive inside ng-repeat

I'm trying to get a directive inside an ng-repeat to work.. it worked when it was hardcoded in HTML but after switching to an ng-repeat certain aspects of it stopped working.
<div ng-repeat="section in filterSections" filter-tray>
Test {{section.label}}
</div>
I have a module with a controller that emits events:
controller: function($scope, $element) {
this.activateTray = function(trayID) {
$scope.$emit('filterTray::show', {
tray: trayID
});
};
};
I have a directive on the page - it should receive events from the controller. Since switching to using ng-repeat receiving the event has stopped working. It still initialises, it just doesn't listen for the event.
.directive('filterTray', function() {
return {
restrict: 'A',
require: '^filter',
link: function($scope, $element, attrs, filterNavController) {
console.log('this debug statement works');
$scope.$on('filterTray::show', function(e, data) {
console.log('this debug statement never runs');
});
}
};
})
Since adding the repeat has the $scope variable been affected? Perhaps $on isn't listening to the correct thing anymore? Any ideas / tips would be appreciated!
Yes - ngRepeat creates a new scope.
Not sure if events are the right design choice, but if so, use broadcast instead of emit, and the events will reach your directive's scope.
controller: function($scope, $element) {
this.activateTray = function(trayID) {
$scope.$broadcast('filterTray::show', {
tray: trayID
});
};
};
emit shoots events up the scope tree to all ancestors
broadcast reaches all descendants.
When creating a directive that will be reused (this includes ngRepeat) it is best practice to create an isolate scope for your directive. This way you can send the trayID without having to use events. Check out the section on isolate scope in the AngularJS docs on directives here.

AngularJS - exposing controller api to directive

How do I make controller functions visible to a directive? Do I attach the methods to the scope, and inject the scope into the directive? Is it a good idea in the first place? I want to manipulate model data from within the UI.
It really dependes on what you want to do.
Since you want to access the controller's scope from the directive, I suggest you declare your directive with it's scope shared with the parent controller by setting it's scope prop to false:
app.directive('directiveName', function() {
scope: false,
link: function(scope) {
// access foo from the controler's scope
scope.foo;
}
});
This is a nice example with how directives can be hooked up to a controller
http://jsfiddle.net/simpulton/GeAAB/
DIRECTIVE
myModule.directive('myComponent', function(mySharedService) {
return {
restrict: 'E',
controller: function($scope, $attrs, mySharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'Directive: ' + mySharedService.message;
});
},
replace: true,
template: '<input>'
};
});
HOOKUP IN CONTROLLER
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
VIEW
<my-component ng-model="message"></my-component>
Adding to #grion_13 answer, scope:true would also work since it creates a new scope that is child of the parent scope so has access to parent scope data.
But a true reusable directive is one which get it input data using isolated scope. This way as long as your html+ controller can provide the right arguments to the directive isolated scope, you can use the directive in any view.

Resources