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 ?
Related
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/
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...
});
Hi I am struggling with the following:
http://jsfiddle.net/uqZrB/9/
HTML
<div ng-controller="MyController">
<p>Button Clicked {{ClickCount}} Times </p>
<my-clicker on-click="ButtonClicked($event)">
</my-clicker>
</div>
JS
var MyApp = angular.module('MyApp',[]);
MyApp.directive('myClicker', function() {
return {
restrict: 'E',
scope: {
onClick: "="
},
link: function($scope, element, attrs) {
var button = angular.element("<button>Click Me</button>");
button.bind("mousedown", $scope.onClick);
element.append(button);
}
};
});
MyApp.controller("MyController", function ($scope) {
$scope.ButtonClicked = function($event) {
$scope.ClickCount++;
};
$scope.ClickCount = 0;
});
(using angular1.2 rc : https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.js)
The custom directive "myClicker" should insert a button into the tag, and bind its mousedown event to a function supplied in the directive scope...
I.e. i can pass the a function from the controller, to execute when the button is clicked.
As you can see, when you run the fiddle, the bound event gets run 11 times, on load.... i.e. before the button has event been clicked.
Running its 11 times causes the "10 $digest() iterations reached. Aborting!" error.
Then, when I click the button I get "Cannot call method 'call' of undefined", as if the method was not declared in the scope.
Why does angular try and run the method on loading?
Why is the "onClick" method not available in the scope?
I think I am misunderstanding something about the directive's isolated scope.
Thanks in advance!
The onClick: "=" in your scope definition expects a two-way data-binding, use onClick: "&" to bind executable expressions into an isolate scope. http://docs.angularjs.org/guide/directive
please changes your code in controller as below
var MyApp = angular.module('MyApp',[]);
MyApp.directive('myClicker', function() {
return {
restrict: 'E',
scope: {
onClick: "&"
},
link: function($scope, element, attrs) {
var button = angular.element("<button>Click Me</button>");
button.bind("mousedown", $scope.onClick);
element.append(button);
}
};
});
MyApp.controller("MyController", function ($scope) {
$scope.ButtonClicked = function($event) {
$scope.ClickCount++;
};
$scope.ClickCount = 0;
});
I have a directive that uses an isolate scope to pass in data to a directive that changes over time. It watches for changes on that value and does some computation on each change. When I try to unit test the directive, I can not get the watch to trigger (trimmed for brevity, but the basic concept is shown below):
Directive:
angular.module('directives.file', [])
.directive('file', function() {
return {
restrict: 'E',
scope: {
data: '=',
filename: '#',
},
link: function(scope, element, attrs) {
console.log('in link');
var convertToCSV = function(newItem) { ... };
scope.$watch('data', function(newItem) {
console.log('in watch');
var csv_obj = convertToCSV(newItem);
var blob = new Blob([csv_obj], {type:'text/plain'});
var link = window.webkitURL.createObjectURL(blob);
element.html('<a href=' + link + ' download=' + attrs.filename +'>Export to CSV</a>');
}, true);
}
};
});
Test:
describe('Unit: File export', function() {
var scope;
beforeEach(module('directives.file'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
};
it('should create a CSV', function() {
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
//I've also tried below but that does not help
scope.$apply(function() { scope.input = {}; });
});
What can I do to trigger the watch so my "In watch" debugging statement is triggered? My "In link" gets triggered when I compile.
For a $watch to get triggered, a digest cycle must occur on the scope it is defined or on its parent. Since your directive creates an isolate scope, it doesn't inherit from the parent scope and thus its watchers won't get processed until you call $apply on the proper scope.
You can access the directive scope by calling scope() on the element returned by the $compile service:
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
e.isolateScope().$apply();
This jsFiddler exemplifies that.
I want to create an AlertFactory with Angular.factory.
I defined an html template like follow
var template = "<h1>{{title}}</h1>";
Title is provided by calling controller and applied as follow
var compiled = $compile(template)(scope);
body.append(compiled);
So, how I can pass isolated scope from controller to factory?
I'm using in controller follow code
AlertFactory.open($scope);
But $scope is global controller scope variable. I just want pass a small scope for factory with just title property.
Thank you.
You can create a new scope manually.
You can create a new scope from $rootScope if you inject it, or just from your controller scope - this shouldn't matter as you'll be making it isolated.
var alertScope = $scope.$new(true);
alertScope.title = 'Hello';
AlertFactory.open(alertScope);
The key here is passing true to $new, which accepts one parameter for isolate, which avoids inheriting scope from the parent.
More information can be found at:
http://docs.angularjs.org/api/ng.$rootScope.Scope#$new
If you only need to interpolate things, use the $interpolate service instead of $compile, and then you won't need a scope:
myApp.factory('myService', function($interpolate) {
var template = "<h1>{{title}}</h1>";
var interpolateFn = $interpolate(template);
return {
open: function(title) {
var html = interpolateFn({ title: title });
console.log(html);
// append the html somewhere
}
}
});
Test controller:
function MyCtrl($scope, myService) {
myService.open('The Title');
}
Fiddle
Followings are the steps:
Add your HTML to the DOM by using var comiledHTML =
angular.element(yourHTML);
Create a new Scope if you want var newScope = $rootScope.$new();
Call $comile(); function which returns link function var linkFun =
$compile(comiledHTML);
Bind the new scope by calling linkFun var finalTemplate =
linkFun(newScope);
Append finalTemplate to your DOM
YourHTMLElemet.append(finalTemplate);
check out my plunkr. I'm programmatically generating a widget directive with a render directive.
https://plnkr.co/edit/5T642U9AiPr6fJthbVpD?p=preview
angular
.module('app', [])
.controller('mainCtrl', $scope => $scope.x = 'test')
.directive('widget', widget)
.directive('render', render)
function widget() {
return {
template: '<div><input ng-model="stuff"/>I say {{stuff}}</div>'
}
}
function render($compile) {
return {
template: '<button ng-click="add()">{{name}}</button><hr/>',
link: linkFn
}
function linkFn(scope, elem, attr) {
scope.name = 'Add Widget';
scope.add = () => {
const newScope = scope.$new(true);
newScope.export = (data) => alert(data);
const templ = '<div>' +
'<widget></widget>' +
'<button ng-click="export(this.stuff)">Export</button>' +
'</div>';
const compiledTempl = $compile(templ)(newScope);
elem.append(compiledTempl);
}
}
}
I assume when you are talking about an isolate scope you are talking about a directive.
Here is an example of how to do it.
http://jsfiddle.net/rgaskill/PYhGb/
var app = angular.module('test',[]);
app.controller('TestCtrl', function ($scope) {
$scope.val = 'World';
});
app.factory('AlertFactory', function () {
return {
doWork: function(scope) {
scope.title = 'Fun';
//scope.title = scope.val; //notice val doesn't exist in this scope
}
};
});
app.controller('DirCtrl', function ($scope, AlertFactory) {
AlertFactory.doWork($scope);
});
app.directive('titleVal',function () {
return {
template: '<h1>Hello {{title}}</h1>',
restrict: 'E',
controller: 'DirCtrl',
scope: {
title: '='
},
link: function() {
}
};
});
Basically, attach a controller to a directive that has defined an isolate scope. The scope injected into the directive controller will be an isolate scope. In the directive controller you can inject your AlertFactory with wich you can pass the isolate scope to.