I am using the TodoMVC app to get better with the AngularJS framework. In the index.html on lines 14-16 you see this:
<form id="todo-form" ng-submit="addTodo()">
<input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" autofocus>
</form>
Notice how the ng-submit directive calls the addTodo() function without the newTodo model being passed as an argument.
A short time later I came across the following code in the very same file on line 19:
<input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">
You can see the author decided to pass the allChecked model to the markAll() function this time. If I understand correctly, they could have referenced $scope.allChecked inside the controller instead of passing it in.
Why use two different approaches in the same file? Is one approach better in some circumstances? Is this a case of inconsistency or is there a deeper logic being used?
The $scope in an AngularJS is a built-in object, which contains application data and methods. You can create properties to a $scope object inside a controller function and assign a value or function to it. The $scope is glue between a controller and view (HTML).
The $ in "$scope" indicates that the scope value is being injected into the current context. $scope is a service provided by $scopeProvider . You can inject it into controllers, directives or other services using Angular's built-in dependency injector: module.
The main difference is the availability of the property assigned with the object. A property assigned with $scope cannot be used outside the controller in which it is defined whereas a property assigned with $rootScope can be used anywhere.
In Angular 2.0, there will be no $scope .
I would prefer to always pass in arguments to the function:
Consider the following situation:
$scope.addToDo = function(){
//This declaration is not clear what parameters the function expects.
if ($scope.parameter1){
//do something with parameter2
}
}
And even worse:
$scope.addToDo = function(){
//This declaration is not clear what parameters the function expects.
if ($scope.someobject.parameter1){ //worse
}
}
Because of scope inheritance parameter2
may come from parent scope, accessing parameter2
inside the function creates a tight coupling, also causes troubles when you try to unit-test that function.
If I define the function like this:
//It's clearer that the function expects parameter1, parameter2
$scope.addToDo = function(parameter1, parameter2){
if (parameter1){
//do something with parameter2
}
}
In case your parameter2
is inherited from parent scope, you could still pass it in from the view. When you do unit-testing, it's easy to pass all parameters.
If you have ever worked with ASP.NET MVC, you would notice something similar: the framework tries to inject parameters into action function instead of accessing it directly from Request
or HttpContext
object
It's also good in case others have mentioned like working with ng-repeat
In my opinion, Controller and Model in angular are not quite clearly separated. The $scope object looks like our Model with properties and methods (Model contains also logic). People from OOP background would think that: we only pass in parameters that don't belong to object. Like a class Person already has hands
, we don't need to pass in hands
for every object method. An example code like this:
//assume that parameter1 belongs to $scope, parameter2 is inherited from parent scope.
$scope.addToDo = function(parameter2){
if ($scope.parameter1){ //parameter1 could be accessed directly as it belongs to object, parameter2 should be passed in as parameter.
//do something with parameter2
}
}
There are two parts to this answer, the first part is answering which one is the better option, the other part is the fact that neither of them is a good option!
Which one is correct?
This one is:
$scope.addToDo = function(params1, ...) {
alert(params1);
}
Why? Because A - it is testable. This is important even if you are not writing tests, because code that is testable is pretty much always more readable and maintainable in the long run.
It is also better because of B - it is agnostic when it comes to the caller. This function can be reused by any number of different controllers/services/etc because it does not depend on the existence of a scope or on the structure of that scope.
When you instead do this:
$scope.addToDo = function() {
alert($scope.params1);
}
Both A and B fail. It is not easily testable on its own, and it cannot easily be reused because the scope you use it in might be formatted differently.
Edit: If you are doing something very closely tied to your specific scope and running the function from the template, then you might run in to situations where trying to make it reusable just doesn't make sense. The function simply isn't generic. In that case, don't bother with that, some functions cannot be reused. View what I wrote about as your default mode but remember that in some cases it won't fit.
Why are both wrong?
Because as a general rule you should not be doing logic in your controllers, that is the job of a service. The controller can use a service and call the function or expose it in a model, but it should not define it.
Why is this important? Because again it makes it easy to reuse the function. A function that is defined in a controller cannot be reused in another controller without putting limits on how the controllers are invoked in the HTML. A function that is defined in a service can be injected and reused wherever you feel like it.
But I don't need to reuse the function! - Yes you do! Maybe not right now and maybe never for this specific function, but sooner or later you will end up wanting to reuse a function that you where convinced that you would never need to reuse. And then you will have to rework code that you have already half forgotten, which always take extra time.
It's better to just do it properly from the start and move all logic that you can into services. That way, if you ever need them somewhere else (even in another project) you can just grab it and use it without having to rewrite it to fit your current scope structure.
Of course, services don't know about your scope so you are forced to use the first version. Bonus! And don't fall for the temptation of passing the entire scope to a service, that will never end well :-)
So this is IMO the best option:
app.service('ToDoService', [function(){
this.addToDo = function(params1, ...){
alert(params1);
}
}]);
And inside the controller:
$scope.addToDo = ToDoService.addToDo;
Note that I wrote "general rule". In some cases it is reasonable to define the function in the controller itself as opposed to a service. One example would be when the function only relates to scope specific things, like toggling a state in the controller somehow. There is no real way to do that in a service without things becoming strange.
But it sounds like this is not the case here.
The Zen of Angular suggests:
Treat scope as read only in templates Treat scope as write only in controllers
Following this principle, you should always call functions explicity with parameters from the template.
However, in any style you follow, you do have to be careful about priorities
and order of execution of directives. In your example, using ng-model
and ng-click
leaves the order of execution of the two directives ambiguous. The solution is using ng-change
, where the order of execution is clear: it will be executed only after the value changes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With