How to make this custom directive better? - angularjs

I am trying to implement a custom directive for a counter widget.
I have been able to implement it, but there are many things i need some light to be thrown on.
Can this directive be written in a better way ?
How do i use the scope:(isolate scope) in a better way ?
on click of any reset button i want all the startnumber to be reset to "1" ?
Where does the scope inherit from?Does it inherit from the element being called from?
HTML snippet
<body>
<counter-widget startnumber=1 ></counter-widget>
<counter-widget startnumber=1 ></counter-widget>
<counter-widget startnumber=1 ></counter-widget>
</body>
JS snippet
angular.module("myApp",[])
.directive("counterWidget",function(){
return{
restrict:"E",
scope:{
},
link:function(scope,elem,attr){
scope.f = attr.startnumber;
scope.add = function(){
scope.f = Number(scope.f) + 1;
}
scope.remove = function(){
scope.f =Number(scope.f) - 1;
}
scope.reset = function(){
scope.f = 1;
}
},
template:"<button ng-click='add()'>more</button>"+
"{{f}}"+
"<button ng-click='remove()'>less</button>&nbsp"+
"<button ng-click='reset()'>reset</button><br><br>"
}
})
Thanks in advance for the help.

First, pass in your startnumber attribute, so we can reset to that number instead of having to hard code in a number.
You want to isolate the scope if you are going to have multiple counters.
But here is how you can implement a global reset:
app.directive("counterWidget",function(){
return{
restrict:"E",
scope:{
startnumber: '=',
resetter: '='
},
link:function(scope,elem,attr){
scope.f = attr.startnumber;
scope.add = function(){
scope.f++
}
scope.remove = function(){
scope.f--
}
scope.reset = function(){
scope.f = attr.startnumber
scope.$parent.triggerReset()
}
scope.$watch(function(attr) {
return attr.resetter
},
function(newVal) {
if (newVal === true) {
scope.f = attr.startnumber;
}
})
},
template:"<button ng-click='add()'>more</button>"+
"{{f}}"+
"<button ng-click='remove()'>less</button>&nbsp"+
"<button ng-click='reset()'>reset</button><br><br>"
}
})
And in the controller you just add a small reset function that each directives is watching:
$scope.triggerReset = function () {
$scope.reset = true;
console.log('reset')
$timeout(function() {
$scope.reset = false;
},100)
}
I wouldn't overcomplicate the decrement and increment functions. ++ and -- should be fine.
We create the global reset function by adding an attribute and passing it in to the directive. We then watch that attribute for a true value. Whenever we click reset we trigger a function in the $parent scope (the triggerReset() function). That function toggles the $scope.reset value quickly. Any directive which has that binding in it's resetter attribute will be reset to whatever is in the startnumber attribute.
Another nice thing is that the reset will only affect counters you want it to. You could even create multiple groups of counters that only reset counters in it's own group. You just need to add a trigger function and variable for each group you want to have it's own reset.
Here is the demo:
Plunker
EDIT:
So the question came up in comments - can the $watch function 'miss' the toggle?
I did a little testing and the best answer I have so far is, on plunker if I set it to 1ms or even remove the time argument completely, the $watch still triggers.
I have also asked this question to the community here: Can $watch 'miss'?

You can use ng-repeat in your html.you can define count = 3
<body>
<div ng-repeat="index in count">
<counter-widget startnumber=1 ></counter-widget></div>
</body>
Also follow the link .They have explained scope inheritance in a better way
http://www.sitepoint.com/practical-guide-angularjs-directives-part-two/
Parent Scope (scope: false) – This is the default case. If your directive does not manipulate the parent scope properties you might not need a new scope. In this case, using the parent scope is okay.
Child Scope (scope:true) – This creates a new child scope for a directive which prototypically inherits from the parent scope. If the properties and functions you set on the scope are not relevant to other directives and the parent, you should probably create a new child scope. With this you also have all the scope properties and functions defined by the parent.
Isolated Scope (scope:{}) – This is like a sandbox! You need this if the directive you are going to build is self contained and reusable. Your directive might be creating many scope properties and functions which are meant for internal use, and should never be seen by the outside world. If this is the case, it’s better to have an isolated scope. The isolated scope, as expected, does not inherit the parent scope.

This will be a better approach.
HTML
<body>
<counter-widget counter="controllerScopeVariable" ></counter-widget>
</body>
JS
angular.module("myApp",[])
.directive("counterWidget",function(){
return{
restrict:"E",
scope:{
counter:'='
},
link:function(scope,elem,attr){
scope.counter = parseInt(scope.counter);
scope.add = function(){
scope.counter+=1;
}
scope.remove = function(){
scope.counter-=1;
}
scope.reset = function(){
scope.counter = 1;
}
},
template:"<button ng-click='add()'>more</button>"+
"{{f}}"+
"<button ng-click='remove()'>less</button>&nbsp"+
"<button ng-click='reset()'>reset</button><br><br>"
}
});

Related

Removing element from DOM via ng-if bound to attribute directive scope property

I assumed this would be straightforward, but it's seemingly not!
I'm trying to create a generic attribute directive that will call a method in one of my services and conditionally cause the element in which it is placed to not be added to the DOM if the service method returns false. Basically, ng-if, but an ng-if that internally calls a service method and acts on that
Link to Plunker
I have an element containing an attribute directive: e.g
<p ng-if="visible" my-directive>Hi</p>
I set visible to true in the myDirective directive. I was expecting the <p> element to be removed from the DOM when visible was falsy and added to the DOM when it's truthy. Instead, the ng-if never seems to spot that visible has been set to true in the directive's link function and, hence, the <p> element never displays.
I wasn't 100% sure it would work since the directive is removing the element on which it exists, bit of a catch 22 there.
I've spent far too long on this and have so far tried (unsucessfully):
Adding an ng-if attribute in the link function via these two methods
attr.ngIf = true;
element.attr('ng-if', true);
Changing the ng-if in the <p> to ng-show, thereby not removing the element (which I really want to do)
I'm wondering if it's something as simple as scope? Since the ng-if is bound to a property of the <p> element, is setting visible in the directive scope setting it on the same scope?
On the other hand, I may be drastically over-simplifying, I have a nasty feeling I may have to consider directive compilation and transclusion to get a solution for this.
Does anyone have any feel for where I might be going wrong?
tldr: apparently you want your directive to be self-contained and it should be able to remove and add itself to the DOM. This is possible and makes the most sense via isolated scope or manual manipulation of the DOM (see below).
General
When you do <p ng-if="visible" my-directive>Hi</p> angular looks for the visible on the current scope, which is the parent scope of the directive. When visible is defined, the directive is inserted in the DOM, e.g. taken from your plunker
<body ng-controller="MainCtrl">
<p my-directive="showMe" ng-if="visible">I should be shown</p>
</body>`<br>
app.controller('MainCtrl', function($scope) {
$scope.visible = 3;
});
would make the directive being shown. As you defined an isolated scope on your directive
app.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
link: function(scope, element, attr, ctrl) {
scope.visible = (scope.myDirective == 'showMe') ? true : false;
}
}
});
scope.visible in the directive does not affect the visible taken into account for ngIf.
Child Scope
You could define a child scope to get access to the parent scope. If you do that, you can actually affect the right visible property, but you have to put it on an object so that the directive can follow the scope prototype chain.
<body ng-controller="MainCtrl">
<p my-directive ng-if="visibleDirectives.directive1">I should be shown</p>
</body>
The $timeouts are there for demonstration purposes. Initially the ngIf has to evaluate to true else the directive is not being created at all.
app.controller('MainCtrl', function($scope) {
$scope.visibleDirectives = { directive1 : true };
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : true,
link: function(scope, element, attr, ctrl) {
console.log(scope);
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
}, 2000);
}, 2000);
}
}
});
Like this the directive has to know about the property that defines it's visibility beforehand (in this case scope.visibleDirectives.visible1), which is not very practical and prohibits several directives.
Isolated Scope
In your example you used an isolated scope. This allows reusing the directive. In order for the directive to be able to modify the appropriate property for ngIf you have to again give it the right reference.
<body ng-controller="MainCtrl">
<p my-directive="directive1" ng-if="directive1.visible">I should be shown</p>
</body>
Again you have to provide the property on an object so that the directive can follow the object reference to modify the right visible.
app.controller('MainCtrl', function($scope) {
$scope.directive1 = {
visible : true
};
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : {
myDirective : '='
},
link: function(scope, element, attr, ctrl) {
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
}, 2000);
}, 2000);
}
}
});
In these cases the directive gets recreated everytime ngIf evaluates to true.
Manual manipulation of the DOM
You can also just manually remove and append the node of the directive without consulting angular.
<body ng-controller="MainCtrl">
<p my-directive>I should be shown</p>
</body>
In this case you don't need the angular version of setTimeout and can even use a setInterval as the Interval is created only once, but you have to clear it.
app.controller('MainCtrl', function($scope) { });
app.directive('myDirective', function() {
return {
restrict: 'A',
scope : { },
link: function(scope, element, attr, ctrl) {
var el = element[0];
var parent = el.parentNode;
var shouldBeShown = false;
var interval = setInterval(function() {
var children = parent.children;
var found = false;
for(var i = 0; i < children.length; i++) {
if(children[i] === el) {
found = true;
break;
}
}
if(shouldBeShown) {
if(!found)
parent.appendChild(el);
}
else {
if(found)
parent.removeChild(el);
}
shouldBeShown = !shouldBeShown;
}, 2000);
scope.$on('$destroy', function() {
clearInterval(interval);
});
}
};
});
If you want an element to be removed, use ng-show="visible" this will evaluate as a Boolean and show the element if it evaluates to true. Use "!visible" if you need to flip it.
Also, but adding the scope attribute to your directive you are adding an additional scope, think alternate timeline, that your controller scope that is tied to the page cannot see. That would explain why ng-show may not have worked for you before.

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;
}
})
}
};
}

AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding

I have a directive with isolated scope with a value with two way binding to the parent scope. I am calling a method that changes the value in the parent scope, but the change is not applied in my directive.(two way binding is not triggered). This question is very similar:
AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
but I am not changing the value from the directive, but changing it only in the parent scope. I read the solution and in point five it is said:
The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't the parent's value is copied to the isolated scope.
Which means that when my parent value is changed to 2, a watch is triggered. It checks whether parent value and directive value are the same - and if not it copies to directive value. Ok but my directive value is still 1 ... What am I missing ?
html :
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{myValue}}</strong>
<span data-test-directive data-parent-item="myValue"
data-parent-update="update()"></span>
</div>
</div>
js:
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template:
'<button data-ng-click="lock()">Lock</button>' +
'</div>',
controller: function ($scope, $element, $attrs) {
$scope.lock = function () {
console.log('directive :', $scope.key);
$scope.parentUpdate();
//$timeout($scope.parentUpdate); // would work.
// expecting the value to be 2, but it is 1
console.log('directive :', $scope.key);
};
}
};
});
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
console.log('CTRL:', $scope.myValue);
};
});
Fiddle
Use $scope.$apply() after changing the $scope.myValue in your controller like:
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
$scope.$apply();
console.log('CTRL:', $scope.myValue);
};
});
The answer Use $scope.$apply() is completely incorrect.
The only way that I have seen to update the scope in your directive is like this:
angular.module('app')
.directive('navbar', function () {
return {
templateUrl: '../../views/navbar.html',
replace: 'true',
restrict: 'E',
scope: {
email: '='
},
link: function (scope, elem, attrs) {
scope.$on('userLoggedIn', function (event, args) {
scope.email = args.email;
});
scope.$on('userLoggedOut', function (event) {
scope.email = false;
console.log(newValue);
});
}
}
});
and emitting your events in the controller like this:
$rootScope.$broadcast('userLoggedIn', user);
This feels like such a hack I hope the angular gurus can see this post and provide a better answer, but as it is the accepted answer does not even work and just gives the error $digest already in progress
Using $apply() like the accepted answer can cause all sorts of bugs and potential performance hits as well. Settings up broadcasts and whatnot is a lot of work for this. I found the simple workaround just to use the standard timeout to trigger the event in the next cycle (which will be immediately because of the timeout). Surround the parentUpdate() call like so:
$timeout(function() {
$scope.parentUpdate();
});
Works perfectly for me. (note: 0ms is the default timeout time when not specified)
One thing most people forget is that you can't just declare an isolated scope with the object notation and expect parent scope properties to be bound. These bindings only work if attributes have been declared through which the binding 'magic' works. See for more information:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/
Instead of using $scope.$apply(), try using $scope.$applyAsync();

Angular.js -- Directive to controller communication

I am very new to angular so please excuse my lack of understanding.
I have a directive called "draggable" which I want to be able to track the x position of and perform some logic on it in the controller. When the user drags the element (a stick figure) to the right, additional stick figures should appear directly behind it. The controller should know the x position and based upon where it is, increment a counter which will dictate how many stick figures appear behind the draggable element.
This code does not currently work as the controller does not have receive the value of x.
My directive:
app.directive('draggable', function() {
return {
restrict: 'A',
scope: "=x",
link: function (scope, element, attrs) {
$(element).draggable({
containment: "parent",
axis: "x",
drag: function(){
scope.x = $(this).offset().left;
}
});
}
};
});
My controller:
app.controller("main-controller", function($scope) {
$scope.range = function(n) {
return new Array(figures);
};
$scope.$watch("x", function(){
console.log($scope.x);
figures = x / (stick_figure_height)
});
});
My HTML:
<div class="human-slider" ng-controller="main-controller">
<div draggable class="human-draggable">
<img src="images/stickfigure.png"/>
</div>
<div ng-repeat="i in range()">
<img src="images/stickfigure.png"/>
</div>
</div>
The reason the controller was not picking up the updated value of x from the draggable directive was because of where the value of x is being updated. X is updated in a turn that has been created in a method outside of the angularJS library (the drag event handler). The solution to this problem was to use $.apply which will update the binding.
The updated code:
// Create our angular app
var app = angular.module('celgeneApp',[]);
// Main controller
app.controller("main-controller", function($scope) {
$scope.x = 0;
$scope.figures = 0;
$scope.range = function(n) {
return new Array($scope.figures);
};
$scope.$watch('x', function(){console.log($scope.x);});
});
// Draggable directive
app.directive('draggable', function() {
return {
restrict: 'A',
scope: false,
link: function (scope, element, attrs) {
$(element).draggable({
containment: "parent",
axis: "x",
drag: function(){
// Need to use $apply since scope.x is updated
// in a turn outside a method in the AngularJS library.
scope.$apply(function(){scope.x = element.offset().left;});
}
});
}
};
});
You can communicate between a directive and a controller through a service. A directive can also access a controller's scope variables via parameters. You can access the variables in different ways, depending on your needs:
As just text with the # prefix
With a one way binding with the & prefix
With a two bay binding with the = prefix
Check out this excellent article about directives, especially the scope section
Take a look at this directive I made, it is just a wrapper around jQuery's draggable just like yours, maybe you can get some ideas:
angular-draggable
Check my this for how parent controller and directive communicates :)
http://plnkr.co/edit/GZqBDEojX6N87kXiYUIF?p=preview plnkr

In an AngularJS directive, how do I set a parent controller's property?

Here's a jsFiddle that shows what I'm trying to do: http://jsfiddle.net/P3c7c
I'm using the Google Places AutoComplete widget to obtain lat/long coordinates, which I then wish to use in a subsequent search function. It seemed that the proper way to implement this, considering the need to add an event listener to an element, was to use a directive, and to attach the listener using the directive's linking function. However, inside of this listener, I need it to set the location property of the SearchForm controller, which is its parent. And I have not figured out how to make that connection. Here's the relevant chunk of code:
/* Controllers */
function SearchForm($scope){
$scope.location = ''; // <-- this is the prop I wish to update from within the directive
$scope.doSearch = function(){
if($scope.location === ''){
alert('Directive did not update the location property in parent controller.');
} else {
alert('Yay. Location: ' + $scope.location);
}
};
}
/* Directives */
angular.module('OtdDirectives', []).
directive('googlePlaces', function(){
return {
restrict:'E',
replace:true,
transclude:true,
scope: {location:'=location'}, // <--prob something wrong here? i tried #/& too, no luck
template: '<input id="google_places_ac" name="google_places_ac" type="text" class="input-block-level"/>',
link: function($scope, elm, attrs, ctrl){
var autocomplete = new google.maps.places.Autocomplete($("#google_places_ac")[0], {});
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
// THIS IS THE STRING I WANT TO SAVE TO THE PARENT CONTROLLER
var location = place.geometry.location.lat() + ',' + place.geometry.location.lng();
// THIS IS NOT DOING WHAT I EXPECT IT TO DO:
$scope.location = location;
});
}
}
});
Thanks in advance.
Two minor corrections and it should work:
<google-places location="location"></google-places>
and when you set location inside your directive you also need to do $scope.$apply()
$scope.$apply(function() {
$scope.location = location;
});
You have to do $apply() because the event happens outside of angular digest loop, so you have to let angular know that something has changed inside the scope and it needs to "digest" it's bi-directional bindings and other internal async stuff.
Also, I don't think you need transclude:true.
http://jsfiddle.net/P3c7c/1/

Resources