I tried to find some good examples on best practice in moving $watch functions from a controller to a factory, for example.
What I have found is that actually there isn't a unanimous opinion on what's best to do. I've seen examples of injection the $rootScope into a factory and $watching for value changes there.
Another suggestion is to avoid them whenever possible, and to use ngChange instead on the element itself, for example:
<div ng-model="foo.bar" ng-change="updateValue(foo.bar)"></div>
What is your proposed way? I've been putting $watches in my controllers ever since I started learning AngularJS but now I want to embrace best practice approaches, trying to make and keep my controllers as thin as possible.
I guess where to put $watch heavily depends on the use-case scenario. The most important thing to be careful about $watch is not to do any hard-work inside the handle function especially if what you are watching is changing a lot; that would highly damper your performance. And be sure that in your handle function you don't change something else which is already being watched as this will cause a chain of change updates.
If you are sure that the variable you are watching can only be changed from one place, it is better idea to use ng-change rather than $watch as you already know the place where it gets changed.
It is generally good practice not to fill rootScope with unnecessary objects, but if you are watching for some variable which is used in entire application such as current user's attribute, I guess it would make sense to put $watch expression into $rootScope.
Related
I am working on a project where in there are almost 90+ modules.
All modules has a set of input fields and on submit the data should be saved on the server.
At any given point in time only one module is active. But there can be open modules in the background.
Submit button is common to all modules, meaning there is only one Submit button throughout the application.
Below picture explains it more.
The prime motto is to keep the individual module changes to minimum and a way to handle certain things(validation, reload etc) in the module from a central place.
The current approach I am planning is,
Use a 'moduleInit' directive that all module should include in its
partial.
The directive takes the $scope of the module and pass it to a
common service/factory (pushConfigService)
The pushConfigService stores and keep this scope as long as the
module is open. Once the scope is destroyed the reference of the
same will be removed from the pushConfigService.
The footer panel is another directive with Submit button in it and
calls a save function in the pushConfigService which in turn calls
a $scope function in the module to get the form data.
pushConfigService talks to a bunch of other services like
dirtyChecker, apiGenerator and finally post data to the server.
Each module will have a set of scope methods defined with some standard names. Eg: _submit, _onSubmit, _cancel, _reload etc.
Another way to handle this, broadcast the submit event and each module listens to the same. There is possibility more actions will be added to the footer panel.
So I am little bit hesitant to use the broadcast approach.
My question, Is it a good idea to pass controller scope to a service? Any alternate suggestions?
Thanks in advance.
I believe your core concept is a nice way to handle this setup. Yet I'd suggest to split business logic from UI. I don't have a sample of your code so it is a little hard to build an exact example. Yet since you're using the $scope variable I'm going to assume you're not using a styleguide like or similar to John Papa's. His ways encourage you to not use the $scope and to stay close to actual JavaScript "classes".
How does this make a difference?
Instead of passing the whole scope, you'd be able to just pass the instance of your specific module. For one it is less confusing to you and colleagues to have a concrete interface to operate on instead of having to figure out the composition of given scope. In addition it prevents services from being able to alter the $scope.
The latter could be considered a good practice. Having just the controllers alter the scope make it easy to find the code which alters and manages the UI. From there on the controller could access services to do the actual logic.
Taking it one step further
So passing the class instance instead of scope should be an easy adjustment to the already proposed setup. But please consider the following setup as well.
It seems there are quite some different ways to handle and process the data provided by the module/end user. This logic is now implemented in the controller. One might think some of these modules share similar handling methods (big assumption there). You could move this logic to, so to speak, saving strategies, in services. On activation of a module, this module will set its preferred saving strategy in the service which handles the submit button click. Or more precisely, the save data method which should be called from the onClick handler in the controller.
Now these services/strategies might be shared among controllers, potentially setting up for a better workflow and less duplicated code.
I have a Bootstrap modal which corresponds to a ng-repeat list. I would like to access the ng-repeat scope data from its parent, which contains the modal. I do this so that when the modal button is clicked on the list, the corresponding data from the JSON appears in the modal.
I have found 2 ways of doing this, and I wonder which is the best alternative?
Method 1
View:
<li ng-init="myFunction(item,$parent)" ng-repeat="item in data.webapps_1.items>
Controller:
$scope.myFunction = function(item,parent){
parent.selected=item.counter-1;
};
Method 2 View:
<li ng-init="$parent.selected=item.counter-1" ng-repeat="item in data.webapps_1.items>
With nothing in the controller.
I have read in the Angular ngInit docs that
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
But the list of special properties of ngRepeat does not include $parent.
So, which is the better practice? Including the expression $parent.selected=item.counter-1in the controller or in ngInit directive?
Thanks in advance
Either of the two fine really, so long as you're consistent. Depends on the scale of the app though.
IMO if the app is going to be large you'll want to go the function way, to better adhere to the whole MVC philosophy of decoupling and separation of concerns (http://victorblog.com/2013/03/18/angularjs-separation-of-concerns/).
ng-init="myFunction(item,$parent)"
It's a better structure because you want to keep most of your business logic in the javascript controllers, not in the view.
Personally, I prefer the ng-init approach. Mainly for consistency of the timing of the call (i.e. more like an "onLoad" event). I understand the concerns regarding SoC and that makes sense as well. However, I have seen some specific scenarios (particularly pages with a lot of directives), when changing scope variables with the = binding in the (non ng-init) function, caused extra (premature) digest loops. Obviously it depends on what you're actually doing in the function as well as your scope bindings, but in my opinion its best to just avoid the situation and use the ng-init directive across the board. Your experience may obviously vary wildly. Like others said just pick an approach and stick with it and be aware of the impact of every call that you're making in the function itself. :)
EDIT: Regarding the accepted answer, the example was used passing values to the init function in ng-init. And hes correct that is a violation of the pattern. I typically use parameterless init functions and call it like:
ng-init="model.init()"
Any reference to $scope or what have you would happen inside the function itself. I rarely pass anything to init, unless its a static value. For example in several cases on an ASP.NET WebApi project, I wanted minimize round trips to the server so I would resolve a value from the MVC View Model, which is just rendered and passed as a string like so:
ng-init="model.init(#model.myValue)"
In those cases its usually best to keep it to simple value types (i.e. string, int, etc), but I have occasionally for small directives such as one that does nothing but display a dropdown, I have passed arrays to pre-populate the dropdown binding. Those cases are extremely situational and this one in particular would obviously only work when using server side rendering of the template.
we recently switched to Angular 1.3.x and now noticed that it seems to perform much better dirty checking. However there is a case (changing the language for the complete site) where our old implementation expects a reevaluation of certain expressions. This doesn't work anymore. What we would need is to somehow reset angular's dirty checking to make it reevaluate all expressions at least once. Is there a way to do this?
$scope or $rootScope.apply() obviously does not work.
UPDATE for clarification:
We have created our own i18n filter, which uses a service to get appropriate translations. It can be used like:
{{'MyString'|i18n}}
If we change the language, it will cause the service to use different a translation package. However we change the language somewhere in our SPA and since the expression is just a string it will never change, thus it will not be evaluated, translated and re-rendered. Honestly, now that I think about it, I don't know how it ever worked with Angular 1.2.
BR,
Daniel
Note: Sorry for the length of the post, but the reason I decided not to break it down in separate questions was because I find these issues hard to address without a complex problem like this. I am dazzled, and a bit afraid, that I'm trying to force Angular to do stuff for me which is not the 'Angular way'. Any advice would be highly appreciated, and would probably get me on the right track with Angular.
My problem is as follows:
I have a dynamically generated form, controlled by myFormCtrl. I want to go extremely modular: I want to use it whenever and wherever. This means, that sometimes I'll need to put it somewhere as-is, sometimes I need to nest forms dynamically (like when I change a form value, and other sub-form appears), or control two separate forms as one in a view by a parent controller, with one 'Save' button for both. The myFormCtrl uses $scope.type_id and $scope.rowid to know, which record should it display from the database. The records are then Ajax-fetched by a service, and saved under the myFromCtrl's $scope.formItems. When saved, the form sends back data to the server (via service) with the type_id and scope credentials, so the restful api knows where to put the record.
In theory that would be really easy to do in Angular.js.
It definitely would be in every object-orientated language: The parent class could call a public method of getFormValues() of myFormCtrl. Now that can't be done in Angular: parent can't read children's scope.
For me, it seems, that this is not a simple 'how to communicate between controllers' issue. I know that the answer to that question is services, events, scope inheritance.
Also, a number of other problems seem to emerge from each solution I found sofar.
So I have a myFormCtrlBase class, which does basic stuff, and other more advanced classes extend this one. I also have a formControls.html, and a formLayout.html partial. The first contains an ng-switch, and gives the appropriate input element based on $scope.formItem.controltype, the second has the html layout of a common form, ng-including formControls.html at the right places. It utilizes ng-repeat="formItem in formItems", so that's where formControls.html's $scope.formItem comes from.
When I want the form to have a different layout for example, I create a customFormLayout.html partial ng-controlled by the myFormCtrl class.
First question: what if my form layout can't be put in an ng-repeat?
Like when form elements need to be placed scattered across the page, or form layout is not something which could be fit in an ng-repeat loop. my formControls.html still expects a $scope.formItem to work with. Easy OO solution: parent puts formItem in child's scope.
My solution: I created a <formItemScopeChanger formItemScope="formItems[1]"> directive which gets formItems[1] as an attribute, and turns it to $scope.formItem variable. This solutions feels messy: directives are not meant to be used like this. Doesn't seem very Angulary. Is this really the best solution?
Second question: Is ng-init really that evil?
Say, form is not put in the view by $routeProvider, but in a custom partial: rent-a-car.html. Here, I want to have a form where the user can select a car, and an other form, where I get his contacts. The two forms work with different $scope.type_id's, so there need to be two different forms:
<h1>Rent a car!</h1>
<div ng-controller="myFormCtrl" ng-init="type_id='rentOrder'">
<div ng-include="'formLayout.html'"></div>
</div>
<h2>Your contact information</h2>
<div ng-controller="myFormCtrl" ng-init="type_id='User';rowid='{{userData.rowid}}'">
<div ng-include="'formLayout.html'"></div>
</div>
Angular docs sais, that the only appropriate use of ng-init is when aliasing ng-repeat values. I don't see what the problem is with the example above - it is still the cleanest solution, isn't it?
I use the same technique with nested forms - I put a controller in with a template, initialized from the html by ng-init, and shown/hidden with an ng-if condition.
BTW, this is the only real initialization technique I found beside writing a new controllers (extending myFormCtrlBase). In an OO language, parent would write into the child's scope and then initialize it.
Perhaps my approach is influenced by my previously used languages and programming techniques, and is absolutely wrong.
Some would say, 'get init values from parent scopes!', but I can't seem to understand how that would be safe and efficient. I'd need to do $scope.type_id=($scope.type_id || $routeParams.type_id) with every scope property, which is first: really not nice to look at, second: is risky. Maybe this is a single form in a simple template, but somewhere in the scope hierarchy, there is a possibility, that it will find a completely different type_id. Perhaps it will be a completely different controller's type_id.
I don't see how using '.'-s in my scope variables would help. I has the same risk as I see it.
Third question: how to handle rentACar.html submission?
When hitting a Save button on my rentACar.html page, the rentACarCtrl (the controller in charge of the model of the view) should somehow retrieve the values of the two forms, and handle the validation and submission. I can't seem to understand how the common mantra 'controllers communicate through services' would be applicable here. A service for only to these two forms?
Am I on the right track? Every one of these solutions seem quirky. I feel lost :)
+ 1 question: Even after all this hassle, I can't seem to find a good reason why Angular wouldn't let parents call children's public stuff. Is there a good reason? Most of the above problems would have an easy answer in every true OO js framework.
You need to think about how you would test the logic of each of these components. Ask yourself how each of these 'features' work in isolation.
A few tips to help get you back on track:
Try and say away from a 'base' controller, I have hit many dead ends with scope inheritance, the logic gets muddled and hard to follow. Also this affects testing, because you find yourself having to stand up more objects than should be necessary for a test
Favor a singleton (angular service) for shared state over scope inheritance (a parent controller)
Create a directive and bind to the shared services state before using ng-include (prefer interacting with a service over scope inheritance)
Use an event pattern when another service or controller needs to now about events triggered from directives. A shared service (state) can listen for those events
What your asking is quite complex and I would like to help, Try to focus on one feature at a time and provide some code, I can show you how to use a shared service and the event pattern once you provide some examples
Also, taking a test first approach will often reveal the best 'Angular Way' of doing things.
Thanks to Mark-Sullivan, and a lot of work, trial-and-error attempts, the whole thing has boiled down to this. I'd like to get feedback from Mark, and other Angular gurus about this. What do you think?
You don't do class/prototypical inheritance in Angular.js. It is hard to test, and thats a big problem. For those, who are looking for 'inheritance' in Angular, I recommend this:
Your base class is the controller. The controller is an abstract model anyways, so it is perfect for that purpose. Use a $scope.init() function in your controller, but don't call it from there!
If you want to 'extend' your controller's functionality, use directives. In you directive link() function, call the controller's $scope.init(). (when compiling, angular runs controllers first, and directive link functions after). If scope had a $scope.name='base', in the directive link you will be able to redefine $scope.name=child, and after that, run $scope.init().
But wait! But this only allows a single-level inheritance. - Yes, thats true. But if you are looking for multilevel inheritance, you should use Services.
Multilevel inheritance is nothing else, but sharing the same code in a hierarchical class structure. For this purpose, use Services, and throw in these services with the dependency injector into your directives. Soo easy. This should be easy to accomplish, easy to understand, and tests run smooth.
Directives are very powerful tools, because you can dynamically combine partials with controllers.
I am confused on a couple of things with Globals in Angular. Below is my pseudo code.
1) With the way I have my GlobalCtrl placed, I am able to reference my $scope.modalOptions from all of my controllers. That being the case, I'm confused as to why I see people adding global properties to $rootScope instead of just adding them like I am doing here. Is that just in case they want to inject it into a service or something?
2) Should I be using a service instead of adding properties and methods to my GlobalCtrl? If so, why?
<div ng-app="app" ng-controller="GlobalCtrl">
<div ng-view></div>
</div>
function GlobalCtrl($scope, $location) {
$scope.modalOptions = {
backdropFade: true,
dialogFade: true
};
}
The 'Main Controller' approach is definitely preferable to using $rootScope.
Scope inheritance is there, so why not leverage it. In my opinion, that solution works well for most cases, i.e. unless you need to have a parallel controller somewhere (that wouldn't be a child of Main). In that case, the best way to go is to use a service and inject it where needed. Services (or rather factories, because that's what you'll probably be using -- read more about them here) are singletons and work well for sharing data across controllers.
Important to know about scopes
Scope inheritance is pretty much regular JavaScript inheritance at play. You should tend to use objects for your data, because they are passed by reference.
If you have a primitive like $scope.myString = 'is of a primitive data type'; in your parent controller and try to overwrite the value in a child controller, the result won't be what you'd expect -- it will create a new string on the child controller instead of writing to the parent.
Suggested reading here
Final thoughts
If you are using the nested controllers approach, do not forget to still inject $scope (and other dependencies) in the child controller. It might work without, but it's slower and hard to test, and last but not least -- the wrong way to do it.
Finally, if you have a lot of state variables to keep track of and/or a lot of usage points, it's definitely a good idea to extract them into a service.
Generally speaking global variables are considered bad practice, as they don't encourage encapsulation, make debugging difficult, and promote bloated code. Here's a good discussion of global variables: http://c2.com/cgi/wiki?GlobalVariablesAreBad.
A good rule of thumb is to add properties and methods to the most local scope possible and use services to share data between modules.