I'm new to angularjs and I'm having a very difficult time attempting to create a directive to implement a three-state checkbox. I'd like to create a two-way binding between the isolated scope variable in my directive and my controller scope variable. Obviously somehow I've cocked it up.
My goal is to be able to update the state of the checkbox in the dom whenever the state variable gets updated in the controller, and to update the state variable in the controller whenever the user clicks on the checkbox.
After snooping around on the google machine i decided I needed to set up a $watch on my directive attribute (my knowledge of which is tenuous as best). I haven't been able to get that to work. It never gets called when my variable is changed from the controller. Below is a snippet of my markup and the meat of my directive. See my fiddle below for the details.
<input checkbox-three-state='item.state' type='checkbox' />
directive('checkboxThreeState', ['$compile', function($compile) {
return {
restrict: 'A',
scope: {
state: '=checkboxThreeState'
},
link: function (scope, element, attributes) {
element.on('click', function () {
// update the controller scope
});
scope.$watch(attributes.checkboxThreeState, function (newVal, oldVal) {
// update the dom
});
}
}
}]);
I think I have my other objective (update controller scope on click) down.
Here's my fiddle.
I realize now that my question was poorly framed, it really should have been asking why the watch I set up in my directive wasn't working. Now i know that I was telling it to watch the wrong scope property, one that didn't actually exist. Mea culpa. Previously I was telling it to watch my directive attribute -- I had just copied somebody else's example, because I'm a monkey.
scope: {
state: '=checkboxThreeState'
},
link: function (scope, element, attributes) {
// other stuff
scope.$watch(attributes.checkboxThreeState, function (newVal, oldVal) {
// update the dom
});
}
After doing a lot more snooping -- I don't know why this isn't spelled out more ubiquitously for idiots like myself -- I recognized that I had to indicate which property on my scope to watch, namely 'state'.
scope.$watch('state', function (newVal, oldVal) {
// do my jam
});
Now that i've figured that out it seems entirely obvious, but I can be pretty dense.
Here's my now working fiddle.
Updated Question
Why doesn't the watch function in my directive get fired when the controller changes the value of the variable that gets passed into my directive?
Answer Summary:
The watch function should watch a variable that actually exists on your directive scope variable. This is never spelled out in the documentation and although it may be obvious to most, I didn't put that together immediately. I was under the impression -- and to be fair to myself there are a lot of really bad examples out there that severely misled me -- that I should be watching my directive attribute, whose value comes from the controller scope.
Related
I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.
I have made a user-name directive for this question
.directive('userName', function($http) {
return {
restrict: 'E',
scope: {
user: '='
},
replace: true,
template: '<div><span ng-bind="user.username"></span> <a ng-if="user.active">Add</a></div>',
};
});
It is important that the directive uses a minimal number of watches when I use one-time binding af the user attribute (<user-name user="::user"></user-name>).
I have a number of questions. I have created this plunker to test my directive.
Why is there watchers in the one-time-binding case even though user will not update?
Can I get down to zero watchers in this directive when using one-time binding?
I have read in the documentation that I need a $watch in a link function to update the DOM - this doesn't seem to be the case for me. When is $watch necessary in the link function?
getWatchers function doesn't provide meaningful feedback on watchers (summing up the watchers from scopes doesn't make much sense also). You can try $$postDigest instead to monitor this kind of things.
Anyway, logging the scope from the directive after it was linked will give better feedback instantly.
As it shows, there are three watchers:
{isolateScope: Array[2], scope: Array[1]}
Two of them are user.username and user.active:
<div><span ng-bind="user.username"></span> <a ng-if="user.active">Add</a></div>
And the last one is {{$index}}:
<li ng-repeat="user in users" id="user_a_{{$index}}">
Make them all one-time, and there should be zero.
When is $watch necessary in the link function?
Exactly as it was said, when something in the DOM that doesn't have data binding has to be updated when watched scope property is updated.
$watch gives a good amount of control when compared to bindings, e.g. the recipe for one-time watcher is
var unwatch = scope.$watch('oneTime', function (newVal, oldVal) {
unwatch();
...
});
And it can be changed to something like 'stop watching as soon as the variable is defined'.
It can also be used for watching the stuff outside the scope
scope.$watch(function () { return globalVar }, function () { scope.scopeVar = ... });
I'm somewhat new to AngularJs, so forgive me if this is a newb question, but I've looked around a bit and haven't been able to figure this out.
I'm trying to send an attribute of an object into a directive and I'm not quite sure why this isn't working.
I've got a scope variable that's an object, something like:
$scope.player = {name:"", hitpoints:10};
In my HTML, I'm attempting to bind that to a directive:
<span accelerate target="player.hitpoints" increment="-1">Take Damage</span>
In my directive, I'm attempting to modify player.hitpoints like this:
scope[attrs.target] += attrs.increment;
When I trace it out, scope[attrs.target] is undefined, even though attrs.target is "player.hitpoints." When I use target="player", that traces out just fine but I don't want to have to manipulate the .hitpoints property explicitly in the directive.
Edit: I've made a jsfiddle to illustrate what I'm trying to do: http://jsfiddle.net/csafo41x/
There is a way to share scope between your controller and directive. Here is very good post by Dan Wahlin on scope sharing in Directive - http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-2-isolate-scope
There are 3 ways to do so
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and invoked
Just a very basic example on how the above mentioned scope are to be used
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' //Two-way data binding
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
There are a number of things going on here:
#1 - scope
Once you define your isolated scope (along the lines of #Yasser's answer), then you don't need to deal with attrs - just use scope.target.
#2 - template
Something actually needs to handle the click event. In your fiddle there is just <span class="btn"...>. You need ng-click somewhere. In your case, you probably want the directive to handle the click. So modify the directive's template and define the click handler in the directive's scope:
...
template: "<button class='btn' ng-click='click()'>Button</button>",
link: function(scope, element, attrs)
{
scope.click = function(){
scope.target += parseInt(attrs.increment);
}
}
...
#3 - transclude
Now, you need to get the contents of the directive to be the contents of the button within your directive's template. You can use transclude parameter with ng-transclude - for location, for that. So, the template above is modified to something like the following:
...
template: "<button class='btn' ng-click='click()'><div ng-transclude</div></button>",
transclude: true,
...
Here's your modified fiddle
So I know with two way binding, =, in a directive a controller's value can be passed into the directive. But how do you pass a change in the isolated directive back to the controller?
So for example, I have a form that has a slider built with a directive with an isolated scope. Initial value is set from controller, then changes in directive with isolate scope.
What I'm trying to do is change the controller's value when the directive variable with two way binding changes as well.
Any suggestions?
You have two possible ways of achieving this. One is creating a watch statement in the controller on the variable that you passed into the directives isolate scope.
// code in view controller
$scope.sliderValue = 0;
$scope.$watch('sliderValue', function(newValue) {
if (angular.isDefined(newValue)) {
// Do stuff with new slider value
}
});
Note that we need the isDefined, because every watch fires on scope compilation with the initial value being undefined.
The other way is enhancing your directive with a parameter which is evaluated as your slider value changes (much like a callback function).
// sample directive code
angular.module('my-ui-controles', []).directive('mySlider', [function() {
return {
template: '...',
scope: {
value: '=mySlider',
onChange: '&'
},
link: function(scope, elem, attrs) {
// assume this is called when the slider value changes
scope.changeValue = function(newValue) {
// do other internal stuff and notify the outside world
scope.onChange({value: newValue});
}
}
}
}])
And now you can use this in the template likes this:
<div my-slider="sliderValue" on-change="doStuff(value)"></div>
What happens now is as soon as the slider value changes we evaluate the onChange expression that is passed into the directive. The value in doStuff is filled with your new slider value. The object that we have passed on to onChange actually is the scope with which the expression is evaluated and doStuff can be any method from your controller.
The main benefit is that you have the logic for notifying somebody in your directive rather than implicitly in your controller through a watch.
Hope that gets you in the right direction.
If you have pass any variable to isolated scope you can bind it (set any listener on it) in both sides.You can event use Angularjs Events to send any signal, if variable as been changed.
maybe this articles can help you.
http://www.w3docs.com/snippets/angularjs/bind-variable-inside-angularjs-directive-isolated-scope.html
http://www.w3docs.com/snippets/angularjs/change-variable-from-outside-of-directive.html
I need to perform some operations on scope and the template. It seems that I can do that in either the link function or the controller function (since both have access to the scope).
When is it the case when I have to use link function and not the controller?
angular.module('myApp').directive('abc', function($timeout) {
return {
restrict: 'EA',
replace: true,
transclude: true,
scope: true,
link: function(scope, elem, attr) { /* link function */ },
controller: function($scope, $element) { /* controller function */ }
};
}
Also, I understand that link is the non-angular world. So, I can use $watch, $digest and $apply.
What is the significance of the link function, when we already had controller?
After my initial struggle with the link and controller functions and reading quite a lot about them, I think now I have the answer.
First lets understand,
How do angular directives work in a nutshell:
We begin with a template (as a string or loaded to a string)
var templateString = '<div my-directive>{{5 + 10}}</div>';
Now, this templateString is wrapped as an angular element
var el = angular.element(templateString);
With el, now we compile it with $compile to get back the link function.
var l = $compile(el)
Here is what happens,
$compile walks through the whole template and collects all the directives that it recognizes.
All the directives that are discovered are compiled recursively and their link functions are collected.
Then, all the link functions are wrapped in a new link function and returned as l.
Finally, we provide scope function to this l (link) function which further executes the wrapped link functions with this scope and their corresponding elements.
l(scope)
This adds the template as a new node to the DOM and invokes controller which adds its watches to the scope which is shared with the template in DOM.
Comparing compile vs link vs controller :
Every directive is compiled only once and link function is retained for re-use. Therefore, if there's something applicable to all instances of a directive should be performed inside directive's compile function.
Now, after compilation we have link function which is executed while attaching the template to the DOM. So, therefore we perform everything that is specific to every instance of the directive. For eg: attaching events, mutating the template based on scope, etc.
Finally, the controller is meant to be available to be live and reactive while the directive works on the DOM (after getting attached). Therefore:
(1) After setting up the view[V] (i.e. template) with link. $scope is our [M] and $controller is our [C] in M V C
(2) Take advantage the 2-way binding with $scope by setting up watches.
(3) $scope watches are expected to be added in the controller since this is what is watching the template during run-time.
(4) Finally, controller is also used to be able to communicate among related directives. (Like myTabs example in https://docs.angularjs.org/guide/directive)
(5) It's true that we could've done all this in the link function as well but its about separation of concerns.
Therefore, finally we have the following which fits all the pieces perfectly :
Why controllers are needed
The difference between link and controller comes into play when you want to nest directives in your DOM and expose API functions from the parent directive to the nested ones.
From the docs:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
Say you want to have two directives my-form and my-text-input and you want my-text-input directive to appear only inside my-form and nowhere else.
In that case, you will say while defining the directive my-text-input that it requires a controller from the parent DOM element using the require argument, like this: require: '^myForm'. Now the controller from the parent element will be injected into the link function as the fourth argument, following $scope, element, attributes. You can call functions on that controller and communicate with the parent directive.
Moreover, if such a controller is not found, an error will be raised.
Why use link at all
There is no real need to use the link function if one is defining the controller since the $scope is available on the controller. Moreover, while defining both link and controller, one does need to be careful about the order of invocation of the two (controller is executed before).
However, in keeping with the Angular way, most DOM manipulation and 2-way binding using $watchers is usually done in the link function while the API for children and $scope manipulation is done in the controller. This is not a hard and fast rule, but doing so will make the code more modular and help in separation of concerns (controller will maintain the directive state and link function will maintain the DOM + outside bindings).
The controller function/object represents an abstraction model-view-controller (MVC). While there is nothing new to write about MVC, it is still the most significant advanatage of angular: split the concerns into smaller pieces. And that's it, nothing more, so if you need to react on Model changes coming from View the Controller is the right person to do that job.
The story about link function is different, it is coming from different perspective then MVC. And is really essential, once we want to cross the boundaries of a controller/model/view (template).
Let' start with the parameters which are passed into the link function:
function link(scope, element, attrs) {
scope is an Angular scope object.
element is the jqLite-wrapped element that this directive matches.
attrs is an object with the normalized attribute names and their corresponding values.
To put the link into the context, we should mention that all directives are going through this initialization process steps: Compile, Link. An Extract from Brad Green and Shyam Seshadri book Angular JS:
Compile phase (a sister of link, let's mention it here to get a clear picture):
In this phase, Angular walks the DOM to identify all the registered
directives in the template. For each directive, it then transforms the
DOM based on the directive’s rules (template, replace, transclude, and
so on), and calls the compile function if it exists. The result is a
compiled template function,
Link phase:
To make the view dynamic, Angular then runs a link function for each
directive. The link functions typically creates listeners on the DOM
or the model. These listeners keep the view and the model in sync at
all times.
A nice example how to use the link could be found here: Creating Custom Directives. See the example: Creating a Directive that Manipulates the DOM, which inserts a "date-time" into page, refreshed every second.
Just a very short snippet from that rich source above, showing the real manipulation with DOM. There is hooked function to $timeout service, and also it is cleared in its destructor call to avoid memory leaks
.directive('myCurrentTime', function($timeout, dateFilter) {
function link(scope, element, attrs) {
...
// the not MVC job must be done
function updateTime() {
element.text(dateFilter(new Date(), format)); // here we are manipulating the DOM
}
function scheduleUpdate() {
// save the timeoutId for canceling
timeoutId = $timeout(function() {
updateTime(); // update DOM
scheduleUpdate(); // schedule the next update
}, 1000);
}
element.on('$destroy', function() {
$timeout.cancel(timeoutId);
});
...