AngularJS directive not being updated with parent scope variable changes - angularjs

I have a directive and a controller in my AngularJS app as shown below, where I need the directive to be updated with the controller scope variable changes.
Problem I am facing is that any change to the controller scope variable do not update the directive. I've tried using {scope: false}, tried making an isolated scope and one-way binding with the controller scope variable as shown below but none worked, so can someone please check my code and let me know what I am missing / doing wrong here? Thanks
First trial using isolated scope in directive
.directive('loginPanelDir', function() {
return {
restrict: 'A',
scope: {
loginStatus: "&userLoginStatus"
},
link: function(scope, element, attrs) {
console.log(scope.loginStatus()); //will always print 0 despite of changes to the scope var in controller
}
};
});
.controller('LoginController', function ($scope, $location) {
$scope.LoginStatus = "0";
$scope.clickMe = function(){
$scope.LoginStatus = "1";
};
});
<div id="login" login-panel-dir user-login-status="LoginStatus">
<button id="btnLogin" type="submit" ng-click="clickMe()">Login</button>
Second trial using {scope:false} in directive
.directive('loginPanelDir', function() {
return {
restrict: 'A',
scope: false,
link: function(scope, element, attrs) {
console.log(scope.LoginStatus()); //will always print 0 despite of changes to the scope var in controller
scope.$watch(function(){ scope.LoginStatus }, function(){
console.log('Login status : '+scope.LoginStatus); //will always return 0...
});
}
};
});
.controller('LoginController', function ($scope, $location) {
$scope.LoginStatus = "0";
$scope.clickMe = function(){
$scope.LoginStatus = "1";
};
});
<div id="login" login-panel-dir>
<button id="btnLogin" type="submit" ng-click="clickMe()">Login</button>

You don't have to use $timeouts or $intervals to watch changes for certain scope values. Inside your directive you can watch for the changes of your login status via watching the user-login-status attribute.
DEMO
Something like this:
JAVASCRIPT
.controller('LoginController', function($scope) {
$scope.LoginStatus = "0";
$scope.clickMe = function(){
$scope.LoginStatus = "1";
};
})
.directive('loginPanelDir', function() {
return function(scope, elem, attr) {
scope.$watch(attr.userLoginStatus, function(value) {
console.log(value);
});
}
});
HTML
<div id="login" login-panel-dir user-login-status="LoginStatus">
<button id="btnLogin" type="submit" ng-click="clickMe()">Login</button>
</div>

Working plunk.
Use $timeout not setTimeout:
setTimeout(function(){
$scope.LoginStatus = "1";
}, 3000);
should be:
$timeout(function(){
$scope.LoginStatus = "1";
}, 3000);
setTimeout is native Javascript, so it will not run a digest and angular won't be notified of the changes hence no updates to the bindings, $timeout runs a digest when it completes, forcing an update to the binding.
Well its working here, just realized your $watch is also wrong:
it should be:
scope.$watch("LoginStatus", function(){
console.log('Login status : '+scope.LoginStatus); //will always return 0...
});

Related

Avoid using $timeout in Angular UI Modal

In this plunk I have an Angular UI Modal wrapped in a directive. From the controller, I call a method to open the modal, but to do so I need to use $timeout, otherwise, the DOM hasn't finished rendering the directive.
This seems to work, however, what would happen if whatever needs to be completed hasn't finished after the $timeout expires? The $timeout may work in a development environment but may fail in production. Is it a bad practice to use $timeout? How to avoid using it in this example?
HTML
<div modal control="modalCtl"></div>
Javascript
var app = angular.module('app', ['ui.bootstrap']);
app.controller('myCtl', function($scope,$timeout) {
$scope.modalCtl = {};
$timeout(function(){
$scope.modalCtl.openModal();
},100);
})
.directive('modal', function ($uibModal) {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
control: '='
};
directive.link = function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.openModal = function() {
scope.modalInstance = $uibModal.open({
template: '<button ng-click="close()">Close</button>',
scope: scope
})
};
scope.close = function () {
scope.modalInstance.close();
};
};
return directive;
});
To avoid using $timeout the directive can notify controller when everything is ready. Take a look:
.directive('modal', function ($uibModal) {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
control: '=',
onReady: '&' // <-- bind `onReady` with `onModalReady`
};
directive.link = function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.openModal = function() {
scope.modalInstance = $uibModal.open({
template: '<button ng-click="close()">Close</button>',
scope: scope
})
};
scope.close = function () {
scope.modalInstance.close();
};
scope.onReady(); // <-- notify controller
};
return directive;
});
Out HTML:
<div modal on-ready="onModalReady()" control="modalCtl"></div>
Our controller:
$scope.onModalReady = function(){
$scope.modalCtl.openModal();
}
Changed Plunker
About comment #Eduardo La Hoz Miranda
you should be ok with the timeout. I would decrease the time to 0, tho, since timeout will send your call to the bottom of the event loop.
Generally when we initialize $timeout with 0 milliseconds or with no argument as:
$timeout(function(){
$scope.modalCtl.openModal();
});
We delay $scope.modalCtl.openModal() to run before next digest cycle a.e. last in queue. So in this case directive link will run 1st from beginning to to the end and only after you will enter to $timeout.
The $timeout may work in a development environment but may fail in production.
On Production you have the same code. It should work. I believe the problem is in something else. If you are not confident with $timeout use above mentioned way I posted.
Your Logged Plunker
When link function of directive is finished, it can emit a message that it's ready.
And controller listens to this message and displays modal when received.
Code:
var app = angular.module('app', ['ui.bootstrap']);
app.controller('myCtl', function($scope,$timeout) {
$scope.modalCtl = {};
$scope.$on("hey", function() {
$scope.modalCtl.openModal();
});
})
.directive('modal', function ($uibModal) {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
control: '='
};
directive.link = function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.openModal = function() {
scope.modalInstance = $uibModal.open({
template: '<button ng-click="close()">Close</button>',
scope: scope
})
};
scope.close = function () {
scope.modalInstance.close();
};
scope.$emit("hey");
};
return directive;
});
Using timeout for any arbitrary waits on code executing is generally bad. As you state in your question, depending on the overall context of the page you are loading, you have no guarantee that the directive will be ready at the time your controller runs.
It seems like you have too many levels of abstraction here. Something is rendering a div that when it is fully rendered, shows a modal.
Wouldn't it make more sense to just have the thing that is rendering the div create and show the modal instead ?

Angular $watch not working on controller variable updated by directive

I am trying to place a watch on controller variable which gets updated from a directive using function mapping. variable is getting updated and logged in console but watch on it not working.
Code Snippet :
index.html
<body ng-app="myApp" ng-controller="myCtrl">
<div>
<test on-click="update()"></test>
</div>
app.js
var myApp = angular.module('myApp', []);
myApp.controller('myCtrl', function($scope){
$scope.test = {
value: false
};
$scope.update = function() {
$scope.test.value = !$scope.test.value;
console.log("Update: " + $scope.test.value);
};
$scope.$watch('test', function(newVal){
console.log("Watch: " + newVal.value);
}, true);
});
myApp.directive('test', function($compile){
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
onClick: '&'
},
template: '<div ng-transclude=""></div>',
link: function(scope, element, attrs) {
var $buttonElem = $('<button>Test</button>').appendTo(element);
$buttonElem.click(function(){
scope.onClick();
});
}
}
});
Plunker Link is : https://plnkr.co/edit/41WVLTNCE8GdoCdHHuFO?p=preview
The problem is that the directive is raising the event using code that is not apart of AngularJS instead of using an ng-click in its template. If you can't modify the directive, then wrap your event handler in $scope.$apply instead.
$scope.update = function() {
$scope.$apply(function(){
$scope.test.value = !$scope.test.value;
console.log("Update: " + $scope.test.value);
});
};

Accessing scope from within element.bind, within link function of directive

I am experiencing some problems within the link function of my directive. I am starting a new timeout within mousedown event bound to the element, then clearing it on the mouseup. The timeout is not clearing and also other variables I call on the scope are not updating within the element.bind functions. When I log to console, both functions are being triggered but the $scope doesn't seem to update until after the timeout has completed?
How can I make this work? JS fiddle here: http://jsfiddle.net/xrh6dhf9/
HTML
<div ng-app="dr" ng-controller="testCtrl">
<holddelete param="myDeletedMessage" update-fn="doCallback(msg)"></test>
JavaScript
var app = angular.module('dr', []);
app.controller("testCtrl", function($scope) {
$scope.myDeletedMessage = "Deleted Succesfully";
$scope.doCallback = function(msg) {
alert(msg);
}
});
app.directive('holddelete', ['$timeout', function( $timeout) {
return {
restrict: 'E',
scope: {
param: '=',
updateFn: '&'
},
template: "<a href> <i class='fa fa-times fa-fw'></i>Delete {{message}}</a>",
replace: true,
link: function($scope, element, attrs) {
$scope.mytimeout = null;
$scope.message = ">";
element.bind('mousedown', function (e) {
console.log("mousedown");
$scope.message = "- Hold 2 Secs";
$scope.mytimeout = $timeout(function(){
$scope.updateFn({msg: $scope.param});
}, 2000)
});
element.bind('mouseup', function (e) {
console.log("mouseup");
$scope.mytimeout = null;
$scope.message = ">";
})
}
}
}]);
Instead of setting timeout as null use
$timeout.cancel($scope.mytimeout);
Also instead of setting event handlers using element.bind pass execute methods in scope with ng-mousedown and ng-mouseup
Fiddle: http://jsfiddle.net/xrh6dhf9/1/

Extending a controller function within directive

I have a cancel function in my controller that I want to pass or bind to a directive. This function essentially clears the form. Like this:
app.controller('MyCtrl', ['$scope', function($scope){
var self = this;
self.cancel = function(){...
$scope.formName.$setPristine();
};
}]);
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
cancel : '&onCancel'
},
templateUrl: 'form.html'
};
});
form.html
<div>
<form name="formName">
</form>
</div>
However, the $setPristine() don't work as the controller don't have access on the form DOM. Is it possible to extend the functionality of controller's cancel within the directive so that I will add $setPristine()?
Some suggested using jQuery to select the form DOM, (if it's the only way) how to do that exactly? Is there a more Angular way of doing this?
Since the <form> is inside the directive, the controller should have nothing to do with it. Knowing it would break encapsulation, i.e. leak implementation details from the directive to the controller.
A possible solution would be to pass an empty "holder" object to the directive and let the directive fill it with callback functions. I.e.:
app.controller('MyCtrl', ['$scope', function($scope) {
var self = this;
$scope.callbacks = {};
self.cancel = function() {
if( angular.isFunction($scope.callbacks.cancel) ) {
$scope.callbacks.cancel();
}
};
});
app.directive('customDirective', function() {
return {
restrict: 'E'
scope: {
callbacks: '='
},
templateUrl: 'form.html',
link: function(scope) {
scope.callbacks.cancel = function() {
scope.formName.$setPristine();
};
scope.$on('$destroy', function() {
delete scope.callbacks.cancel;
});
}
};
});
Use it as:
<custom-directive callbacks="callbacks"></custom-directive>
I'm not sure I am OK with this either though...

How to pass async data from directive to controller?

I want to compile a third-party api (uploadcare) to a directive.
The api will return the data info after uploaded in async then I want to do something with the return data in my controller but I have to idea how to pass the return data from directive to controller. Below is my code.
in js
link: function (scope, element, attrs) {
//var fileEl = document.getElementById('testing');
var a = function() {
var file = uploadcare.fileFrom('event', {target: fileEl});
file.done(function(fileInfo) {
//scope.$apply(attrs.directUpload)
//HERE IS MY PROBLEM.
//How can I get the fileInfo then pass and run it at attrs.directUpload
}).fail(function(error, fileInfo) {
}).progress(function(uploadInfo) {
//Show progress bar then update to node
console.log(uploadInfo);
});
};
element.bind('change', function() {a()});
}
in html
<input type="file" direct-upload="doSomething()">
in controller
$scope.doSomething = function() {alert(fileInfo)};
AngularJS allows to execute expression in $parent context with specified values, in your case doSomething().
Here's what you need to do that:
In directive definition, mark directUpload as expression:
scope: {
directUpload: "&"
}
In done callback, call:
scope.directUpload({fileInfo: fileInfo})
Update markup:
<input type="file" direct-upload="doSomething(fileInfo)">
To summorize: scope.directUpload is now a callback, which executes expression inside attribute with specifeid values. This way you can pass anything into controller's doSomething.
Read $compile docs for detailed explanation and examples.
Example you might find useful:
angular
.module("app", [])
.directive("onDone", function ($timeout) {
function link (scope, el, attr) {
$timeout(function () {
scope.onDone({
value: "something"
});
}, 3000)
}
return {
link: link,
scope: {
onDone: "&"
}
}
})
.controller("ctrl", function ($scope) {
$scope.doneValue = "nothing";
$scope.done = function (value) {
$scope.doneValue = value;
};
})
<body ng-controller="ctrl">
Waiting 3000ms
<br>
<div on-done="done(value)">
Done: {{doneValue}}
</div>
</body>
You can pass through an object to the scope of the directive using = within the directive to do two way data binding. This way you can make updates to the data within the directive on the object and it will be reflected in it's original location in the controller. In the controller you can then use $scope.watch to see when the data is changed by the directive.
Something like
http://plnkr.co/edit/gQeGzkedu5kObsmFISoH
// Code goes here
angular.module("myApp",[]).controller("MyCtrl", function($scope){
$scope.something = {value:"some string"}
}).directive("simpleDirective", function(){
return {
restrict:"E",
scope:{someData:"="},
template:"<button ng-click='changeData()'>this is something different</button>",
link: function(scope, iElem, iAttrs){
scope.changeData=function(){
scope.someData.value = "something else";
}
}
}
});

Resources