when should i use more than one controller in my app? - angularjs

So recently I started to learn angularJS and I'm working on a SPA project.
it is a game with several steps (the purpose of the game isn't important for the matter of the question)
At the moment i have a controller for each one of the steps, all of them are childs of a mainControlelr so in my html it looks something like:
<div class="container" ng-controller="mainCtrl">
<!--main container-->
<div class="row">
<!--center pane-->
<div id="centerPane" class="col-lg-8 center-block">
.
. some code...
.
<!--steps-->
<div ng-view id="form-views"></div>
.
. some code...
.
</div>
.
. some code...
.
</div>
after speaking with some of my friends, all of them told me that this isn't a good way to do it and i should do one of the following instead:
work with only one controller (for this exmaple, 'mainCtrl')
keep the code like this, but inject a specific service, to all of my controllers, which will hold all of my global vars and make sure the controllers are updating the service vars.
so basically my question is when, as a rule of thumb, one should consider using more than one controller in a SPA?
Thanks,

No, I would advise against putting all your code in one controller. This makes it very hard to understand, as a lot of business logic will be in one file, and it will be hard to unit-test.
First of all, business logic should be put in a service. Keep as much business logic out of the controller and put it in a service or factory.
Assuming you're using ngRoute (I see ng-view in your code) and the steps are one after the other, I would suggest to make full use of the ngRoute component. What this means is that every step in your application should have it's own route and companying controller. As previously stated, business logic that applies to multiple routes should be abstracted to a service or factory.
To make it more clear, the below code could be the configuration of the routes for every step.
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/step-1', {
templateUrl: 'partials/step1.html',
controller: 'StepOneCtrl'
}).
when('/step-2', {
templateUrl: 'partials/step2.html',
controller: 'StepTwoCtrl'
}).
otherwise({
redirectTo: '/step-1'
});
}]);

Related

"Header" controller which will always be present on top

Started learning AngularJS today, using webpack and ES6. I'm kind of stuck on a simple problem that I cannot figure out. I'm building a really simple SPA app, like a shopping cart app, which should have the "header" component with a couple of links to other pages and the sum and total of shopping cart.
One of these other controllers could be just a random dummy page, one could be the shopping cart summary, and the main controller would be where the user can actually add items to the shopping cart.
What I have thus far is just the following routes:
export default function routes($stateProvider) {
$stateProvider
.state('main', {
url: '/',
template: require('./main.html'),
controller: 'MainController',
controllerAs: 'main'
})
.state('other', {
url:'/other',
template: require('./other.html'),
controller: 'OtherController',
controllerAs: 'other',
})
}
index.js:
export default angular.module('app.main', [uirouter, productService])
.config(routing)
.controller('MainController', MainController)
.controller('OtherController', OtherController)
.name;
productService isn't relevant here, so I exclude that code.
Just as a test I tried to set the OtherController as a separate view in index.html:
<body>
<div ui-view="other"></div>
<div class="container" ui-view></div>
</body>
Kind of like suggested here: How to update partial of html using ES6 syntax with angular-webpack-seed
Also tried to use ng-controller="other" instead of ui-view="other", but that throws error:
The controller with the name 'other' is not registered
And I don't know where I would then register it...
Some places I read that you could use a directive for making a "header"/navbar, but that doesn't seem correct to me at all. Also tried that though, in this way: https://stackoverflow.com/a/33714913/6294072 Read about ng-include, but that wouldn't work, since I need the service for the shopping cart.
I am missing something fundamental here, but can't figure it out. My brain is also (unfortunately) wired on Angular (4), since I have some experience with that :)
Sorry for the noob question, I feel ashamed, but I have been struggling for this for hours now and therefore going crazy. Help is very much appreciated!
Use components instead of controllers with controllerAs. You'll find it much more similar to Angular 4. Then depending on how you want to do it, you can use either bindings or requires to create relationships between your components.
Since you have tight control over this, I would go with requires. Create a component with your navbar and stuff in it, then create child components and require the parent in them. We have a generic in-house framework for this stuff. The outer component we call the site, so we get a reference to it in our child components like this:
require: { site: '^f1Site' }
If you're not already doing so, use ui-router 1.0.x and use route to component to handle navigation & state.
Index.html just has <f1-site>Loading</f1-site> in it's body.
The f1-site template has basic layout stuff in it - including navbars and such that we've moved off into their own components - and then the ui-view directive is on the tag where we want to load all of dynamic components.

Is it good or bad to specify controller in templates only?

I found myself annoyed when I need to specify which controller to use for a template every time I use it for a route or a directive. It get worse when template coupled to a controller with controllerAs syntax, and I have to remember which name it has to be.
$routeProvider.when('/', {
templateUrl: 'stateTemplate.html',
controllerAs: 'ctrl',
controller: 'StateController'
});
ngDialog.open({
template: 'stateTemplate.html',
controller: "StateController",
controllerAs:"ctrl"
});
I’d rather prefer to specify ng-controller StateController as ctrl in the template and totally skip controller and controllerAs in the other places.
The question is: does this approach has some pitfalls I don’t see now? Why it's bad (if it is)? Are the benefits of explicit controller and controllerAs parameters against using ng-controller in the corresponding template? Where canI learn more about it?
I think a lot of people are misunderstanding your question. Am I correct in paraphrasing it as:
What is wrong with only defining a view in your Route Config and then binding that view to a controller using ng-controller="StateController as ctrl"?
So instead of your examples above you would have:
$routeProvider.when('/', {
templateUrl: 'stateTemplate.html',
});
ngDialog.open({
template: 'stateTemplate.html',
});
Then in your stateTemplate.html you would have something like:
<div ng-controller="StateController as ctrl">
<h1>State</h1>
. . .
</div>
Is this what you are asking?
If so, nothing is wrong with this. You will lose the ability to reuse controllers with different views. But honestly that might not be a real concern.
If you use bindToController in directives (you should) you will still want to declare the controller in the Directive Data Object rather than use ng-controller. bindToController causes the property to be bound to the controller before the controller is instantiated.
The other thing you will lose out on is controller dependancy resolve objects.
With uiRouter and ngRouter you can use a resolve object that will then get passed into the controller when the dependancies are resolved. For instance you do not want to display the user details page until the call to get the user details has been returned. Once it is returned it can be passed into the constructor of the controller. This is another thing you will lose out on using with ng-controller. https://github.com/johnpapa/angular-styleguide#resolving-promises-for-a-controller
I am sure there are other benefits to using the declared controllers in the route config. Maybe someone else will point them out.
That said, there is nothing wrong with using it. You should probably be consistent with it so that your code is predictable. If you choose to use ng-controller, it may not not be a big deal to change it in the future. Especially if you have protractor web tests set up so that you know you haven't broken anything.
The quickest answer - bad.
You cannot define business logic inside controller template. What if you need to use now 30 different controllers? are you going to specify 30 different controllers? It's messy, it's unadvised and it's a bad practice all together.
It's like writing BLL inside the input logic layer, or authentications in client side code. You are just making it hard on yourself here.
Defining controllers in templates is defiantly not the answer you are looking for.

Is this the correct use for a Directive?

I have been tasked with setting up a initial application structure for a large angular application, I came across a few blog posts that basically said everything should be a directive (which I mostly agree with) but I have a feeling I have took this idea too far..
what I have got is basically - when you navigate to portal ui-router will load the portal template from the templates folder, all that's inside that actual template is <portal-view></portal-view.. the portalView directive basically the entire view wrapped up in a directive.
Route
angular.module('portal').config([
'$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
$stateProvider.state('portal', {
url: "/",
templateUrl: "templates/portal.tpl.html"
});
}]);
portal.tpl.html
<div class="container">
<portal-view></portal-view>
</div>
portalView directive
angular.module('portal').directive('portalView', function() {
return {
controller: 'portalController',
controllerAs: 'vm',
templateUrl: "/directives/portalView/portalView.tpl.html"
}});
portalView folder
portalView
portalView.controller.js
portalView.js
portalView.less
portalView.tpl.html
In my head this seems a good idea but I can it becoming a chore when we start adding more view to the application but I am hoping some angular pro will tell me this is the best way to do it :)
Any help, advice and links would be much appreciated!
Componentizing the view and putting it into directive looks like a smart idea to me. This will ease the migration to another router also if it takes place.
portal.tpl.html is unnecessary if you plan to follow this practice, template: '<portal-view></portal-view>' would be enough. But don't reject it if you have plans on using the template for css and js asset loading (using the scripts in templates isn't straight-forward, but it is possible), it would fit the scheme quite well.

Dynamically loading AngularJS modules from within Templates (Views)

Background: Let's suppose for the sake of argument that you have 100,000 views (partials). Let's also suppose you have accompanying view-scoped controllers, and potentially view-scoped services and filters as well. Try to envision an aggregating application that hosts 100,000 disparate small applications.
Issue: When you have "partials" that require accompanying controllers, the typical solution is to do something like this:
$routeProvider.when('/app1', {
templateUrl: 'partials/view1.html',
controller: 'controller1'
});
The controller is typically loaded from index.html via:
<script src="js/directives/Controller1.js"></script>
The problem with this approach is that it doesn't scale. There are solutions out there for dynamically loading controllers, but they still require adding touch points in various config.
Ideal Solution: Ideally - again for very small applications whose numbers are in the 000's, the controller could be loaded dynamically, and from within the partial itself. This would alleviate the need to manage several files and several configuration touch points (not to mention network requests), and keep each partial very well contained.
It would look something like this:
In router:
$routeProvider.when('/apps/:appId', {
templateUrl: 'partials/app-frame.html',
controller: 'AppCtrl'
});
In containing html (app-frame) include the relatively disparate "mini app":
<h1>Currently hosting {{appId}}</h1><hr>
<div class="ng-include: appUrl"></div>
In partial resolved with appUrl, define controller and markup in one:
<script>
myApp.controller('controller1', ['$scope', function ($scope) {
$scope.foo = "bar";
}]);
</script>
<div ng-controller="controller1">
{{foo}}
</div>
For cases like this, where there are a lot of partials and a 1-1 mapping for controller and view, it can make sense to couple the two for development efficiencies and maintenance. It's a lot cleaner than using several files and additional configuration touch points.
The problem is, this doesn't work. It could be as simple as forcing the script to load prior to applying the directive... but not sure how to do that?
Here are some similar explanations of the problem:
https://groups.google.com/forum/#!topic/angular/H4haaMePJU0
Loading Partial Page With Angular and Compile The Controller
Igor from the AngularJS team says:
I see.. we looked into supporting script tags in jqlite, but what needs to be done to get a cross-browser support involves a lot of black magic. For this reason we decided that for now we are just going to recommend that users use jquery along with angular in this particular case. It doesn't make sense for us to rewrite one third of jquery to get this working in jqlite.
But I don't know what he means by "use jquery" ... JQuery is already loaded into the application from index.html (and prior to angularjs), but it sounds like I need to do something specifically within the partial itself.
You cannot add new controllers through module('app').controller(name, function() { .. }) after AngularJS bootstrap. In order make it work you should use $controllerProvider.register(name, function() { .. }).
You can override the original controller registering function in following way to be able to add controllers pre and pos bootstrap:
var app = angular.module('app', [
'ui.router'
]);
app.config(function($controllerProvider) {
app.controller = function (name, controller) {
$controllerProvider.register(name, controller);
};
});

Router and refresh multiples ng-inludes

I start with code:
when('/admin', {
templateUrl: 'partials/admin/layout.html',
controller: AdminCtrl
})
when('/admin/products', {
templateUrl: '????',
controller: AdminProductsCtrl
})
Template "tree":
index.html ---> <div ng-view/>
---layout.html ---> <div ng-include=menu/> and <div ng-include=body/>
------menu.html
------products.html
Actually I do this:
function AdminCtrl($scope) {
$scope.menu = 'partials/admin/menu.html';
}
function AdminProductsCtrl($scope) {
$scope.menu = 'partials/admin/menu.html';
$scope.body = 'partials/admin/products/index.html';
}
The point is: What I put in '????', if I put layout.html this work fine, but I like just "refresh" ng-include=body. I think that my concepts about Angularjs is wrong.
Other problem is, when AdminProductsCtrl "take the control" of layout.html I miss the AdminCtrl $scope, this implicates repeat all AdminCtrl $scope in AdminProductsCtrl $scope (for example $scope.menu).
Thanks a lot, and sorry for "my english".
UPDATE
After think.. and think... I understanding that routes not apply for my app, then I manage all functionality under one url 'site.com/#/admin'. The menu.html is manage for AdminMenuCtrl, this controller contains a model for each 'ng-include' and contains one method for each menu entry. When the user click a menu entry, the associate method in the $scope replace $scope.includes.body with the 'new' html. The partial cointains your ng-controller.
This works fine by now :D. And the best is that I don't need use $rootScope.
The new problem is a bit more complicated, the ng-include require a tag (i.e DIV) and ng-controller too. Then my design is affected for this. In code language:
DESING:
<div>MENU-HTML</div>
<div>BODY-HTML</div>
TEMPLATE:
<div ng-include="menu"></div>
<div ng-include="body"></div>
AFTER RETRIEVE PARTIALS:
<div ng-include="menu"><div ng-controller="MenuCtrl">MENU-HTML</div></div>
<div ng-include="body"><div ng-controller="ListProductsCtrl">BODY-HTML</div></div>
THE IDEAL THING:
1 - ng-include don't 'include' into the DIV, instead 'replace' the DIV.
2 - ng-controller DIV is replaced for nothing in the DOM.
It's possible now with angular? Is a bad approach this idea? The point 2 with $route is possible, not with ng-controller directive.
I believe you are correct in your example you would set ???? to layout.html but the idea is to have different views based on the route so pointing to the same layout.html is not ideal.
If you are trying to keep a static menu on all pages I would add the menu to your index.html and then choose a different templateUrl for each route (ie /admin goes to partials/admin.html and /admin/products goes to partials/products.html) and not use the ngInclude.
I'm new to AngularJS but I'm getting the impression that you generally want to use ngView with routes to templateUrls OR use ngInclude (possibly with ngSwitch) if you want to roll your own view switching. I'm sure there are times when using both is appropriate but as a newbie it confuses me somewhat. Resident experts please correct me if I'm wrong!
For your second issue there might be some helpful information here and here for tips on sharing the same model across multiple controllers but you probably don't need to for your example.
An alternative is to use a string constant path to your partial in layout.html and remove the references to $scope.menu in your controller code by using:
<div ng-include="'partials/admin/menu.html'"/>

Resources