I've created some directives which have some functions, something like this:
myCtrl.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
'something': '='
},
link: function (scope, el, attr) {
function func1(){
//some stuff
scope.$apply();
}
function func2(){
//some stuff
scope.$apply();
}
function func3(){
//some stuff
scope.$apply();
}
//...
}
}
});
I have to call scope.$apply() in all the functions to update the view. In addition I don't want to define them in the controller. I don't know if there is a trick to avoid this kind of pattern. It is working but I think it's not a good solution.
UPDATE
There are more than 10 directives which we've created for some shapes, e.g. rectangle, circle, square etc. In these functions, which I called $apply in them, some methods like drag and scale are implemented. So I need to call $apply to modify changes in model and consequently the view will be updated. I don't know how can I make them able to aware scope automatically unless I write about 100 functions in a controller!
In addition, I'm using some 3rd party libraries like d3.js. Some event like clicked are bounded for calling these functions.
The real truth is that you need to call $scope.$apply() whenever you're changing something outside angular digest cycle. Angular detects the changes by dirty checking during the digest cycle.
$timeout is also doing $apply ($rootScope.$apply())
Check Angular source code.
What I can suggest is create helper function which will handle that. Example:
var ngAware = function (fnCallback) {
return function () {
var result = fnCallback.apply(this, arguments);
$scope.$apply();
return result;
};
};
// usage
function func1 () {
// impl...
}
var ngFunc1 = ngAware(func1);
// or
var func2 = ngAware(function func2 () {
// impl...
});
Sure you need apply()?
apply() is used when integrating 3rd party libraries (such as jQuery). In most cases, if you are not hooking up 3rd party javascript libraries, you can avoid $apply() by using the build in binding mechanism
For instance, the following snippet will call func1 using a 3 seconds timeout callback. then, func1 will manipulate the scope variable something and the view will be updated without the need to trigger $apply()
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.value = 'Hello';
});
app.directive('myDirective', function ($timeout) {
return {
restrict: 'E',
template:'<h1>{{something}}</h1>',
scope: {
'something': '='
},
link: function (scope, el, attr) {
function func1(){
scope.something += ' ' + scope.something;
}
$timeout(func1, 3000);
}
}
});
html
<body ng-controller="MainCtrl">
<my-directive something="value"></my-directive>
</body>
http://plnkr.co/edit/42VYs0W2BC5mjpaVWQg3?p=preview
perhaps if you explain more about your use case i could expand my answer to your specific needs
wraps the function within $scope.$apply()
check this :
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.value = 'Hello';
});
app.directive('myDirective', function ($timeout) {
return {
restrict: 'E',
template:'<h1>{{something}}</h1>',
scope: {
'something': '='
},
link: function (scope, el, attr) {
scope.$apply(function(){
function func1(){
//some stuff
}
function func2(){
//some stuff
}
function func3(){
//some stuff
}
})
}
}
});
If you have common functionality in you directives (circle, square, etc) you should group it. By example, we could have a directive to drag
.directive('ngDraggable', [function () {
return {
link: function ($scope) {
var stopHandler = function () {
$scope.apply(function () {
// Code to handler the end of the dag event
// Notify the other directive.
$scope.$emit('dragStopped', data);
});
};
// Initialization of the drag functionality.
}
}
}])
You can used as
<div ng-circle ng-draggable></div>
So, you earn three thing. First, the circle directive can do changes in its scope without call to $apply, because $emit already is in the digest cycle. Second, you only use $apply where is necessary. And third, you improve the performance by use $apply in scopes most small and with less children.
You could merge this answer with the answer of #sasko_dh
Maybe the Observer Pattern could be you useful.
Related
I'm looking for a good design pattern to provide angular directives render acording to some global specified parametrs.
For example, I have some factory called "Settings", that holds the value "directiveColor: red".
When i do the link in my directive, I ask the Settings about my directiveColor value. Everything is working fine - I got red and put element on the page. But I have hundreds of this elements on the page, and every directive before render ask for settings... I think it's not very good way.
What will you recomend?
UPD
factory
app.factory('Settings', function() {
var data = {
//...
directiveColor: red //set by user
}
//...
GetSettings : function () {return data}
}
directve
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
link: function(scope, elem, attr) {
scope.data = {
//...
color: Settings.GetSettings().directiveColor
};
};
}]);
//later "color" used in template through the scope
That's how it works for now (works fine). But every time, when I render directive (many many times on the page, ngRepeat for table data), my directive ask for Settings to choose its color. I think, it is not good. Or maybe not?
There are two considerations here. First, you are right that it is not optimal, and directive actually provides a way to do that call once, read about Compile-PreLink-PostLink in angular directives. Basically you want this call in Compile step if it is the same for all directives in your app.
Second consideration is that Settings.GetSettings().directiveColor will give really really small overhead if GetSettings() returns just an object that you only create once ( and that is what happened as angular factories are singletons )
In your case you can do
app.factory('Settings', function() {
var data = {
directiveColor: 'red' //set by user
}
return {
GetSettings : function () {return data}
}
})
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
compile: function(elem, attrs) {
var color = Settings.GetSettings().directiveColor
return function postLink(scope, elem, attr) {
scope.data = {
color: color
};
}
}
}
}])
instead of declaring link property on directive.
I hope someone can help me. I have not been able to figure this one out.
I wrote a directive (see below) to dump a pre-written ul-list on a page based on html data that I retrieved async from a database server. Both the Directive and The Service work.
I assumed that the "then" in "MenuService.getMenuData().then" would force a wait until the data arrived to the directive but some how the directive completes and shows the '3empty' message before the data arrived, which indicates that the directive completed earlier. I know I could put a timeout delay but that is not good. Do you have a suggestion as to what could the problem be?
The other technique I used was to put a ng-show="dataarrived" and set the dataarrived to true only when the promised completed. But same issue.
The purpose of this directive is to retrieve the Nav menu list from the serve and display it on the index.html but It does Not matter if I put this code in a controller or in a service or directive I get the same result. It shows nothing. It is particular to displaying it in the index.html before any other view is displayed.
Here is my directive if it make sense.
TBApp.directive('tbnavMenu', function ($compile, MenuService) {
var tbTemplate = '3empty';
MenuService.getMenuData().then(function (val) {
tbTemplate = val;
});
var getTemplate = function () {
return tbTemplate;
}
var linker = function (scope, element, attrs) {
element.html(tbTemplate).show();
$compile(element.contents())(scope);
}
return {
restrict: "E",
replace: true,
link: linker,
controller: function ($scope, $element) {
$scope.selectedNavMenu = GlobalService.appData.currentNavMenu;
$scope.menuClicked = function ($event, menuClicked) {
$event.preventDefault();
$scope.selectedNavMenu = menuClicked;
$scope.tbnavMenuHander({ navMenuChanged: menuClicked });
};
$scope.isSelected = function (menuClicked) {
return $scope.selectedNavMenu === menuClicked;
}
},
scope: {
tbnavMenuHander: '&'
}
}
}
I could be incredibly wrong but if your service is returning an $http object at the getMenuData method then these lines:
MenuService.getMenuData().then(function (val) {
tbTemplate = val;
});
should change to either:
MenuService.getMenuData().then(function (val) {
tbTemplate = val.data;
});
or
MenuService.getMenuData().success(function (val) {
tbTemplate = val;
});
My personal recomendation is to use the .then option as it enables the concatenation of more promises.
Consider this snippet from AngularJS by Brad Green.
var directives = angular.module('guthub.directives', []);
directives.directive('butterbar', ['$rootScope',
function ($rootScope) {
return {
link: function (scope, element, attrs) {
element.addClass('hide');
$rootScope.$on('$routeChangeStart', function () {
element.removeClass('hide');
});
$rootScope.$on('$routeChangeSuccess', function () {
element.addClass('hide');
});
}
};
}]
);
directives.directive('focus', function () {
return {
link: function (scope, element, attrs) {
element[0].focus();
}
};
});
Notice that for the "butterbar" directive he passes in an array where the first item is just a string with the dependency name "$rootScope", and the second item is a function. That function declares a dependency on $rootScope. Why do we repeat ourselves here? Especially when it appears to be possible to just do this:
directives.directive('butterbar', function ($rootScope) {
return {
link: function (scope, element, attrs) {
element.addClass('hide');
$rootScope.$on('$routeChangeStart', function () {
element.removeClass('hide');
});
$rootScope.$on('$routeChangeSuccess', function () {
element.addClass('hide');
});
}
};
});
I'm guessing that the dependency name being a string has some sort of significance. Can anyone tell me why they do this throughout the book (and not just for directives)?
When JavaScript is minified, the names of parameters are often changed to something shorter such as a. This would break dependency injection.
If you use an array, Angular knows what to inject and where to inject it. This works with the array because the string elements of the array are not modified by minification.
In this example:
app.controller('test', function( $scope, $someProvider ) {
});
minified code might look something like this:
app.controller('test',function(a,b){});
This would no longer work since Angular will not know what to inject, whereas with this:
app.controller('test', ['$scope', '$someProvider', function( $scope, $someProvider) {
}]);
the minified code might end up like this:
app.controller('test',['$scope','$someProvider',function(a,b) {}]);
This would still work because Angular still knows what to inject. See the note on minification in the Angular tutorial.
Usually I just add the array style when I'm ready for production.
What is the functional difference between the following code (in Widget Uno) using a directive definition object (I think it's called..?)...
angular.module("app").
directive("widgetUno", ["$http", function ($http) {
return {
// A whole bunch of crap going on here
},
templateUrl: "widgetUno.html"
};
}]);
...And this code in Widget Dos?
angular.module("app").directive('widgetDos', function($http) {
return function(scope, element, attrs) {
// A whole bunch of crap going on here
};
});
I'm trying to convert a directive that's like Widget Uno into Widget Dos, but where do I reference the templateUrl? Is this possible in Widget Dos?
Returning only a function in a directive is just a shorthand for the link function in the full definition.
If you are specifying something other than a link function (like templateUrl) then you need to write it the long way:
angular.module("app").
directive("widgetUno", ["$http", function ($http) {
return {
link: function(scope, element, attrs) {
// A whole bunch of crap going on here
},
templateUrl: "widgetUno.html"
};
}]);
This difference is actually documented here - http://docs.angularjs.org/guide/directive
The one returning the function is actually the shortcut for:
angular.module("app").directive('widgetDos', function($http) {
return {
link: function(scope, element, attrs) {
//...
};
}
});
Use it in case your directive doesn't need template, controller etc. Other than that, there's absolutely no functional difference between those two calling approaches.
It should work like this:
angular.module("app").directive('widgetDos', function($http) {
return {
templateUrl: "....",
link: function(scope, element, attrs) {
// A whole bunch of crap going on here
};
}
});
See also http://docs.angularjs.org/guide/directive (long version). There is an example.
So I have this directive called say, mySave, it's pretty much just this
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
element.bind("click", function() {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
}
});
}
});
the element itself looks like this
<button my-save my-save-callback="callbackFunctionInController()">save</button>
callbackFunctionInController is for now just
$scope.callbackFunctionInController = function() {
alert("callback");
}
when I console.log() attrs.mySaveCallback inside my-save directive, it just gives me a string callbackFunctionInController(), I read somewhere that I should $parse this and it would be fine, so I tried to $parse(attrs.mySaveCallback) which gave me back some function, but hardly the one I was looking for, it gave me back
function (a,b){return m(a,b)}
What am I doing wrong? Is this approach flawed from the beginning?
So what seems like the best way is using the isolated scope as suggested by ProLoser
app.directive('mySave', function($http) {
return {
scope: {
callback: '&mySaveCallback'
}
link: function(scope, element, attrs) {
element.on("click", function() {
$http.post('/save', scope.$parent.data).success(returnedData) {
// callback defined on my utils service here
scope.callback(); // fires alert
}
});
}
}
});
For passing parameters back to controller do this
[11:28] <revolunet> you have to send named parameters
[11:28] <revolunet> eg my-attr="callback(a, b)"
[11:29] <revolunet> in the directive: scope.callback({a:xxx, b:yyy})
There are a lot of ways to go about what you're doing. The FIRST thing you should know is that the $http.post() is going to be called as soon as that DOM element is rendered out by the template engine, and that's it. If you put it inside a repeat, the call will be done for each new item in the repeater, so my guess is this is definitely not what you want. And if it is then you really aren't designing things correctly because the existence of DOM alone should not dictate queries to the backend.
Anyway, directly answering your question; if you read the albeit crappy docs on $parse, it returns you an evaluation expression. When you execute this function by passing the scope to evaluate on, the current state of that expression on the scope you passed will be returned, this means your function will be executed.
var expression = $parse(attrs.mySave);
results = expression($scope); // call on demand when needed
expression.assign($scope, 'newValu'); // the major reason to leverage $parse, setting vals
Yes, it's a little confusing at first, but you must understand that a $scope changes constantly in asynchronous apps and it's all about WHEN you want the value determined, not just how. $parse is more useful for a reference to a model that you want to be able to assign a value to, not just read from.
Of course, you may want to read up on creating an isolate scope or on how to $eval() an expression.
$scope.$eval(attrs.mySave);
You can use .$eval to execute a statement in the given scope
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
scope.$eval(attrs.mySaveCallback)
}
}
});
TD: Demo
If you want to share data between a directive and a controller you can use the two way binding
app.controller('AppController', function ($scope) {
$scope.callbackFunctionInController = function() {
console.log('do something')
};
$scope.$watch('somedata', function(data) {
console.log('controller', data);
}, true);
});
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&mySaveCallback' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
scope.data = data;
scope.callback(); //calling callback, this may not be required
});
}
};
});
Demo: Fiddle
scope: {
callback: '&mySaveCallback'
}
Setting the scope explicitly could be a good solution but if you want the reach other parts of the original scope you can't because you have just overwritten it. For some reason, I needed to reach other parts of the scope too so I used the same implementation as ng-click do.
The use of my directive in HTML:
<div my-data-table my-source="dataSource" refresh="refresh(data)">
Inside the directive (without setting the scope explicitly):
var refreshHandler = $parse(attrs.refresh);
scope.$apply(function () {
refreshHandler( {data : conditions}, scope, { $event: event });
});
With this I can call the function in controller and pass parameters to it.
In the controller:
$scope.refresh= function(data){
console.log(data);
}
And it prints the conditions correctly out.
This worked for me
Inside the view script
<tag mycallbackattrib="scopemethod">
Inside the directive
$scope[attrs.mycallbackattrib](params....);
It is correctly called and params are passed, but maybe is not a best 'angular way' to work.
You should be using ng-click instead of creating your own directive.
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
if (scope.callback()) scope.callback().apply(data);
});
}
};
});