Today I encounter some really strange behaviour in AngularJS using $watch. I simplified my code to the following example:
https://jsfiddle.net/yLeLuard/
This example contains a service which will keep track of a state variable. The directives are used to bind a click event to the DOM changing the state variable through the service.
There are two problems in this example:
The first close button (with the ng-click property on it) only changes the state on the second click
The two buttons without the ng-click are not changing the state at all
main.html
<div ng-controller="main">
State: {{ api.isOpen | json }}
<div ng-click="" open>
<button>Open - Working fine</button>
</div>
<div ng-click="" close>
<button>Close - Works, but only on second click</button>
</div>
<div open>
<button>Open - Not Working</button>
</div>
<div close>
<button>Close - Not Working</button>
</div>
</div>
main.js
var myApp = angular.module('myApp', []);
myApp.controller('main', ['$scope', 'state', function($scope, state) {
$scope.api = state;
$scope.api.isOpen = false;
$scope.$watch('api.isOpen', function() {
console.log('state has changed');
});
}]);
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Open</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = true;
});
}
};
}]);
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Close</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
});
}
};
}]);
myApp.service('state', function() {
return {
isOpen: null
};
});
That's because you are using a native event listener on click. This event is asynchronous and out of the Angular digest cycle, so you need to manually digest the scope.
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link: function(scope, element) {
element.on('click', function() {
scope.$apply(function() {
state.isOpen = true;
});
});
}
};
}]);
Fixed fiddle: https://jsfiddle.net/7h95ct1y/
I would suggest changing the state directly in the ng-click: ng-click="api.isOpen = true"
You should put your link function as a pre-link function, this solves the problem of the second click.
https://jsfiddle.net/2c9pv4xm/
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link:{
pre: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
console.log('click', state);
});
}
}
};
}]);
As for the not working part, you're putting your directive on the div, so when you click on the div it works but not on the button. Your open directive should be on the button.
Edit: other commenters suggested that you change the state directly in the ng-click. I wouldn't recommend it, it might work in this case but if you're bound to have more than an assignation to do it's not viable.
Related
I am trying to implement a directive with its own model and change attribute (as an overlay for ng-model and ng-change). It works apparently fine but when the function of the father scope is executed and some variable of the scope is modified in it, it is delayed, the current change is not seen if not the one executed in the previous step.
I have tried adding timeouts, $apply, $digest ... but I can not get it synchronized
angular.module('plunker', []);
//Parent controller
function MainCtrl($scope) {
$scope.directiveValue = true;
$scope.textValue = "init";
$scope.myFunction =
function(){
if($scope.directiveValue === true){
$scope.textValue = "AAAA";
}else{
$scope.textValue = "BBBB";
}
}
}
//Directive
angular.module('plunker').directive('myDirective', function(){
return {
restrict: 'E',
replace: true,
scope: {
myModel: '=model',
myChange: '&change'
},
template: '<span>Check<input ng-model="myModel" ng-change="myChange()"
type="checkbox"/></span>',
controller: function($scope) {
},
link: function(scope, elem, attr) {
var myChangeAux = scope.myChange;
scope.myChange = function () {
setTimeout(function() {
myChangeAux();
}, 0);
};
}
});
// Html
<body ng-controller="MainCtrl">
<my-directive model="directiveValue" change="myFunction()"></my-directive>
<div>Valor model: {{directiveValue}}</div>
<div>Valor texto: {{textValue}}</div>
</body>
The correct result would be that the "myFunction" function runs correctly
Example: https://plnkr.co/edit/q3IqRCIhwLChlGrkDxyO?p=preview
You should use AngularJS' $timeout which is a wrapper for the browser default setTimeout and internally calls setTimeout as well as $digest, all at the right time in the execution.
Your directive code should change as such:
angular.module('plunker').directive('myDirective', function($timeout){
return {
restrict: 'E',
replace: true,
scope: {
myModel: '=model',
myChange: '&change'
},
template: '<span>Check<input ng-model="myModel" ng-change="myChange()" type="checkbox"/></span>',
controller: function($scope) {
},
link: function(scope, elem, attr) {
var myChangeAux = scope.myChange;
scope.myChange = function () {
$timeout(myChangeAux, 0);
};
}
};
});
Docs for AngularJS $timeout
Let's say I have a "filterList" element. Inside is a list of <filter-item> angular directives. I want each <filter-item> to have a "delete" button inside of it. Clicking the delete button will remove the <filter-item> from the DOM.
How can I do this?
<div class="filterList">
<filter-item></filter-item>
<filter-item></filter-item>
<filter-item></filter-item>
</div>
FilterItem directive:
myApp.directive('filterItem', ['$http', function($http){
return {
scope: { },
replace: true,
link: function(scope, element, attrs){
scope.remove = function() {
// Hmm, I sure do wonder what goes here Mr Jones.
}
},
template: '<div>Hi mom! <button on-click="remove()">Remove</button></div>'
}
});
Inside of link you can do element.remove();.
Since AngularJS has jqLite, remove() removes the directive's element from the DOM. If your directive is not tied to any model, this is acceptable. Be careful manipulating the DOM like this when directives are rendered as a result of the model, since you haven't changed the model you can end up with strange effects (e.g. re-rendering).
(Self-answer but found a good post not on StackOverflow)
myApp.directive('filterItem', ['$http', function($http){
return {
scope: { },
replace: true,
link: function(scope, element, attrs){
scope.remove = function() {
element.remove();
}
},
template: '<div>Hi mom! <button on-click="remove()">Remove</button></div>'
}
});
method 1: i dont know if this is the right way to do it but you can use
<div class="filterList">
<filter-item></filter-item>
<filter-item></filter-item>
<filter-item></filter-item>
</div>
FilterItem directive:
myApp.directive('filterItem', ['$http', function($http){
return {
scope: {
hideDir:'=?'
},
replace: true,
link: function(scope, element, attrs){
scope.remove = function() {
// Hmm, I sure do wonder what goes here Mr Jones.
element.innerHTML='';
}
},
template: '<div>Hi mom! <button on-click="remove()">Remove</button></div>'
}
});
method 2: you can also try using ng-if and change variable inside directive
<div class="filterList" ng-if="!hideDir" hide-dir="hideDir">
<filter-item></filter-item>
<filter-item></filter-item>
<filter-item></filter-item>
</div>
FilterItem directive:
myApp.directive('filterItem', ['$http', function($http){
return {
scope: {
hideDir:'=?'
},
replace: true,
link: function(scope, element, attrs){
scope.remove = function() {
// Hmm, I sure do wonder what goes here Mr Jones.
scope.hideDir=true;
}
},
template: '<div>Hi mom! <button on-click="remove()">Remove</button></div>'
}
});
I have a custom directive with children directives:
<rp-nav>
<rp-nav-item cat="1"></rp-nav-item>
<rp-nav-item cat="2"></rp-nav-item>
<rp-nav-item cat="3"></rp-nav-item>
<rp-nav-item cat="4"></rp-nav-item>
<rp-flyout></rp-flyout>
</rp-nav>
Here are the modules I have defined:
var app = angular.module('app', []);
app.directive('rpNav', function() {
return {
restrict: 'E',
controller: function($scope) {
$scope.currentItem = 'none'; //initialize currentItem
this.setCurrentItem = function(itemId) {
$scope.currentItem = itemId;
}
},
};
});
app.directive('rpNavItem', function() {
return {
restrict: 'E',
template: function(el, attrs) {
return '<p>item {{currentItem}} ' + attrs.cat;
},
require: '^rpNav',
link: function(scope, el, attrs, nav) {
el.on('click', function() {
nav.setCurrentItem(attrs.cat);
});
}
};
});
app.directive('rpFlyout', function() {
return {
restrict: 'E',
template: '<p style="background-color: lightblue">{{currentItem}}</p>'
};
});
The idea is to click in any of the items and make the rp-flyout element display information about the clicked rp-nav-item. The scope variable currentItem does change on click, but the template in rp-flyout does not update. What am I missing to achieve this goal? And, is this a "best practice" way of tackling this problem.
Here's a plunker
To expand on the comment, directives are not inherently part of the digest cycle, so you need to add scope.$apply() inside your el.click handler to trigger a digest cycle and update template bindings.
el.on('click', function() {
nav.setCurrentItem(attrs.cat);
scope.$apply();
});
In the following code, i change the object's property on clicking the 'tab' element, but the corresponding ngbind span is not getting updated. Do i have to call some function to update the view?
HTML:
<html ng-app="splx">
...
<body ng-controller="Application">
<span ng-bind="obj.val"></span>
<tabpanel selector="obj">
<div tab value="junk">junk</div>
<div tab value="super">super</div>
</tabpanel>
</body>
</html>
JS:
var cf = angular.module('splx', []);
function Application($scope) {
$scope.obj = {val: "something"};
}
cf.directive('tabpanel', function() {
return {
restrict: 'AE',
scope: {
selector: '='
},
controller: ['$scope', function($scope) {}]
};
});
cf.directive('tab', function() {
return {
require: '^tabpanel',
restrict: 'AE',
scope: true,
link: function(scope, elem, attrs) {
elem.bind('click', function() {
scope.$parent.selector.val = "newthing";
});
}
};
});
cf.directive('tab', function() {
return {
require: '^tabpanel',
restrict: 'AE',
scope: true,
link: function(scope, elem, attrs) {
elem.bind('click', function() {
scope.$apply(function () {
scope.$parent.selector.val = "newthing";
});
});
}
};
});
That works for me. Just missing a little scope.$apply in there.
Might want to have a look at https://coderwall.com/p/ngisma if you find yourself using/having trouble with '$apply already in progress'.
If you want to change the value to what you clicked on, I'd do something like this:
scope.$parent.selector.val = attrs.tab;
As opposed to:
scope.$parent.selector.val = "newthing";
And then you can change your markup to look like this:
<tabpanel selector="obj">
<div tab="junk">junk</div>
<div tab="super">super</div>
</tabpanel>
Hope that helps!
First problem: you are not binding your controller to your app.
You need cf.controller('Application', Application);.
Also you need ng-controller="Application" in HTML on a parent of that span and the tabpanel directive.
Second problem: after changing that scope variable in your click event you need to
scope.$apply() to let Angular know something changed and it needs to $digest it.
You can check out my version here.
Is it possible to "watch" for ui changes on the directive?
something like that:
.directive('vValidation', function() {
return function(scope, element, attrs) {
element.$watch(function() {
if (this.hasClass('someClass')) console.log('someClass added');
});
}
})
Yes. You can use attr.$observe if you use interpolation at the attribute.
But if this is not an interpolated attribute and you expect it to be changed from somewhere else in the application (what is extremely not recommended, read Common Pitfalls), than you can $watch a function return:
scope.$watch(function() {
return element.attr('class');
}, function(newValue){
// do stuff with newValue
});
Anyway, its probably that the best approach for you would be change the code that changes the element class. Which moment does it get changed?
attrs.$observe('class', function(val){});
You can also watch variable in the controller.
This code automatically hides notification bar after some other module displays the feedback message.
HTML:
<notification-bar
data-showbar='vm.notification.show'>
<p> {{ vm.notification.message }} </p>
</notification-bar>
DIRECTIVE:
var directive = {
restrict: 'E',
replace: true,
transclude: true,
scope: {
showbar: '=showbar',
},
templateUrl: '/app/views/partials/notification.html',
controller: function ($scope, $element, $attrs) {
$scope.$watch('showbar', function (newValue, oldValue) {
//console.log('showbar changed:', newValue);
hide_element();
}, true);
function hide_element() {
$timeout(function () {
$scope.showbar = false;
}, 3000);
}
}
};
DIRECTIVE TEMPLATE:
<div class="notification-bar" data-ng-show="showbar"><div>
<div class="menucloud-notification-content"></div>