Angular custom directive: Access parent scope property in linking function - angularjs

I want to set the name of an event (click or touch) on a parent scope in angular as a string. On a child scope I want to use that string to bind an event with element.on.
Example:
angular.module('directives', [])
.directive('Sidebar', function () {
'use strict';
return {
link: function (scope) {
//determines whether or not to use click or touch events
//I want to use this to bind events with
//element.on(scope.sidebarClickEvent) in child scopes
scope.sidebarClickEvent = 'click';
},
restrict: 'E',
templateUrl: 'sidebar.html'
};
})
.directive('collapseBtn', function() {
'use strict';
return function (scope, element) {
element.on(scope.sidebarClickEvent /* undefined */ , function () {
//scope.sidebarClickEvent is available here, when the event handler executes
scope.toggleSidebarCollapseState();
element.find('i').toggleClass('icon-double-angle-right');
});
};
})
The problem is that properties defined on the parent scope aren't available when I bind the events, so scope.sidebarClickEvent is undefined when I bind the event. But if I change it to a regular click event then I can get the property in the event handler.
Can I access properties inherited by the scope at the time that the event binding occurs? I'm not even sure that I'm understanding scope inheritance properly here, so pointing out errors in my understanding would also be appreciated.
Thanks.

Related

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.

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

Calling Angular Directive's controller functions

I have a directive for a chart:
.directive('chart', function() {
return {
...
controller: function($scope) {
this.toggleAnimation = function() {
...
};
},
link: function link(scope, element, attrs) {
...
}
}
});
And I'm using it like so:
<div ng-controller='foo'>
<chart></chart>
</div>
Where foo is:
.controller('foo', function($scope) {
// TODO: call chart's toggleAnimation
});
Now, how do I call the toggleAnimation function on the chart directive from within foo controller?
Or is this not how the setup should be? What I'm trying to do here is create a function for my chart directive that allows whatever's consuming it to turn a variable in the directive to true/false.
.directive('chart', function() {
return {
scope: {
toggle: "#" // pass as string - one way // you could also make this an attr if you want
},
...
controller: function($scope) {
},
link: function link(scope, element, attrs) {
...
var toggleAnimation = function() {
...
};
// when you change this value, it will toggle the animation
// logic which will check the values of this variable so you can do if statements and modify animations
scope.$watch('toggle', function(newVal, oldVal){
console.log(newVal);
if(parseInt(newVal) === 1)
toggleAnimation();
else if(parseInt(newVal) === 0)
; // do something else like toggle back
});
}
}
});
HTML
<div ng-controller='foo'>
<chart toggle="myVariable"></chart>
</div>
Controller JS
.controller('foo', function($scope) {
// TODO: call chart's toggleAnimation
$scope.myVariable = 0; // initialize to this value
function clickSomething(){
$scope.myVariable = 1; // change, hence fire animation
}
});
There are two main mechanisms by which data can flow between specific directives or controllers. Data can either flow down the scope hierarchy (which usually mirrors the DOM tree) using scopes and expressions, or it can flow up the hierarchy using directive controller APIs. Both of these mechanisms entail one directive communicating with one other specific directive.
A third communication mechanism is scope events. This mechanism is about one directive communicating with zero or more other directives/controllers which it doesn't necessarily know about.
Which mechanism to use depends on the specific scenario. The following sections give an overview of each, followed by a round-up of the trade-offs of each. (In the specific example you gave I'd use the first, but you seem to be interested in the general mechanisms and not just in your specific example.)
The idiomatic way to pass data down the tree is to provide the chart access to data from its parent scope. In that case, it would be used like this:
<div ng-controller="Foo">
<chart animated="chartAnimated"></chart>
</div>
The chartAnimated in the above is a scope variable inserted by the controller. Here's how that looks in the Foo controller:
.controller('Foo', function($scope) {
$scope.chartAnimated = true;
$scope.toggleAnimation = function () {
$scope.chartAnimated = ! $scope.chartAnimated;
};
});
The chart directive then needs to support this new attribute, which can be achieved using the scope property in the directive declaration:
.directive('chart', function() {
return {
scope: {
// This requests that Angular parse the expression in the 'animated'
// attribute and write a function for it into the scope as
// 'animationEnabled'.
'animationEnabled': '&animated'
},
link: function link(scope, iElement, attrs) {
// Now we can watch the expression to detect when it changes.
scope.$watch(
scope.animationEnabled,
function (isEnabled) {
// This function will be called once on instantiation and then
// again each time the value of the expression changes.
// Use ``isEnabled`` in here to either enable or disable animation.
console.log('Animation', isEnabled ? 'is enabled' : 'is disabled');
}
);
}
}
});
Although it's not really applicable to your given example, let's also explore the other data flow technique I mentioned, where data flows up the tree.
In this case, a parent directive can expose an API to a child directive. This is, for example, how the ngModel directive interacts with its parent form directive, or how ngSwitchWheninteracts with its parent ngSwitch.
The key here is the require property on the directive declaration, which allows a directive to depend on another directive either on the current element or on some parent element. For the sake of this example, we'll look for it on any parent element.
Let's make a contrived example of a parent directive with many children that it wants to keep track of for some reason:
<parent>
<child name="foo"></child>
<child name="bar"></child>
<child name="baz"></child>
</parent>
We'll define the parent directive first:
.directive('parent', function() {
return {
controller: function () {
this.children = {};
this.registerChild(name, child) {
console.log('Got registration for child', name);
this.children[name] = child;
}
}
}
});
The child directive is where we can make use of the require mechanism:
.directive('child', function() {
return {
require: '^parent', // Must be nested inside a 'parent' directive
link: function (scope, iElement, attrs, parentCtrl) {
// Notice the extra 'parentCtrl' parameter above.
// Provide an API for parent to interact with child.
var child = {};
child.doSomething = function () {
console.log('Child', attrs.name, 'requested to do something');
};
parentCtrl.registerChild(attrs.name, child);
}
}
});
In this case we establish a bidirectional communication channel between the parent and the child, with the child initiating the channel using require, and passing to the parent an object through which it can communicate with the child. When require is used there is an extra argument to link giving the controller of the directive that was requested.
Finally, let's talk about events. These are best applied in a situation where you have a single directive (or indeed, any other code that owns a scope) that wishes to broadcast a particular notification to whoever is listening. For example, $route communicates with ng-view (and anyone else who is listening) using the $routeChangeSuccess event, and ng-view alerts the rest of the application that the view is ready via $viewContentLoaded.
Event watchers belong to scopes, and events propagate up and down the scope hierarchy.
If you're holding a scope, you can watch for any events that might pass by using scope.$on:
scope.$on(
'$viewContentLoaded',
function () {
console.log('view content loaded!');
}
);
If you want to send an event, you can either send a message up the scope hierarchy using $emit:
scope.$emit(
'somethingHappened'
);
...or you can send a message down the scope hierarchy using $broadcast:
scope.$broadcast(
'somethingHappened'
);
In some circumstances you wish to pass an event to the entire application, in which case you can $broadcast on the $rootScope:
$rootScope.$broadcast(
'somethingHappened'
);
One important thing to keep in mind with events is that they are a "point-to-multipoint" mechanism, which is to say that many different recipients may "see" the same message. This makes events a rather poor mechanism for directed communication between two specific participants, and so events should be used sparingly.
So there's an overview of three data-flow mechanisms for directives in AngularJS. There are different trade-offs for each:
Passing data to child directives via scope keeps the child directive decoupled from the parent, but requires the parent directive (or, in your case, controller) to provide the data that the child needs.
The require mechanism is best used to provide template constructs that require the participation of multiple strongly-related elements, such as in the case of ngSwitch where ngSwitchWhen exists only to be used with ngSwitch, and is of no use without it.
Events are best used for broad notifications, where the sender doesn't especially care who receives the message, and the recipient doesn't necessarily know who sent it. In this case, sender and recipient are truly decoupled from one another, and there may not even be a recipient.
You can maybe use $scope.$broadcast to broadcast an event to the child scope in the directive. The directive would listen for that and then run the method when it hears the correct event:
$scope.$broadcast("toggleAnimation", this.textToBroadcast);
Fiddle here:
http://jsfiddle.net/smaye81/q2hbnL5b/3/

how can I select a tab from outside controller

I'm using Tabs (ui.bootstrap.tabs) control\directive described here. The control creates it's own controller which sets active tab:
.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
var ctrl = this,
tabs = ctrl.tabs = $scope.tabs = [];
ctrl.select = function(selectedTab) {
angular.forEach(tabs, function(tab) {
if (tab.active && tab !== selectedTab) {
tab.active = false;
tab.onDeselect();
}
});
selectedTab.active = true;
selectedTab.onSelect();
};
Tabset child tab controls (child elements) can trigger parent's select function when clicked on them.
.directive('tab', ['$parse', function($parse) {
return {
require: '^tabset',
scope: {
onSelect: '&select',
I have my custom controller upwards the DOM which needs to trigger select function on TabsetController to set first tab active. I've read that I could use event broadcasting but I can't modify TabsetController to bind event listener so this doesn't seem to be a viable option. Any suggestions?
EDIT:
Please see Plunker for better understanding - here.
You can declare a scope attribute within the "parent" controller and it will be accessible in the child controller.
see: AngularJS - Access to child scope
Because TabsetController is set on a child DOM element while the MainController is set on a parent element, you can define and manipulate $scope.tabs in the MainController, and it will be seen and intepreted in TabsetController.

Creating a scope-independent fadein/fadeout directive in AngularJS

To set the stage - this is not happening within a single scope, where I can bind a simple attribute. The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope). Further, the button that's clicked is in a child scope about 3 children deep from root.
Here is how I'm currently solving this:
HTML (sitting in root scope):
<ul class="nav-secondary actions"
darthFader fadeDuration="200"
fadeEvent="darthFader:secondaryNav">
Where darthFader is my directive.
Directive:
directive('darthFader',
function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.$on(attrs.fadeevent, function(event,options) {
$(element)["fade" + options.fade || "In"](attrs.fadeduration || 200);
});
}
}
})
So here I'm creating an event handler, specific to a given element, that is calling fadeIn or fadeOut, depending on an option being passed through the event bus (or defaulting to fadeIn/200ms).
I am then broadcasting an event from $rootScope to trigger this event:
$rootScope.$broadcast('darthFader:secondaryNav', { fade: "Out"});
While this works, I'm not crazy about creating an event listener for every instance of this directive (while I don't anticipate having too many darthFader's on a screen, it's more for the pattern I would establish). I'm also not crazy about coupling my attribute in my view with an event handler in both my controller & directive, but I don't currently have a controller wrapping the secondary-nav, so I'd have to bind the secondaryNav to $rootScope, which I don't love either. So my questions:
Is there a way to do this without creating an event handler every time I instantiate my directive? (maybe a service to store a stateful list of elements?)
How should I decouple my view, controller & directive?
Any other obvious questions I'm missing?
Cheers!
You mention in your question
The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope).
I believe if I were to write this same functionality, I would put the element in its own controller--controllers are responsible for managing the intersection of the view and the model, which is exactly what you're trying to do.
myApp.controller('NavController', function($scope) {
$scope.fadedIn = false;
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="fadedIn">
myApp.directive('darthFader', function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var duration = attrs.fadeDuration || 200;
$scope.$watch(attrs.fadeShown, function(value) {
if (value)
$(element).fadeIn(duration);
else
$(element).fadeOut(duration);
});
}
};
});
If you're worried about sharing the fade in/out state between multiple controllers, you should create a service to share this state. (You could also use $rootScope and event handlers, but I generally find shared services easier to debug and test.)
myApp.value('NavigationState', {
shown: false
});
myApp.controller('NavController', function($scope, NavigationState) {
$scope.nav = NavigationState;
});
myApp.controller('OtherController', function($scope, NavigationState) {
$scope.showNav = function() {
NavigationState.shown = true;
};
$scope.hideNav = function() {
NavigationState.shown = false;
};
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="nav.shown">
<!-- ..... -->
<div ng-controller="OtherController">
<button ng-click="showNav()">Show Nav</button>
<button ng-click="hideNav()">Hide Nav</button>
</div>
Create a custom service, inject it in the controller. Call a method on that service that will do the fade-in/fade-out etc. Pass a parameter to convey additional information.

Resources