Can I use the same html for two components in Angular - angularjs

I have an interesting dilemma, I've written a project that at one point had two controllers and two templates
.state('resources',
{
url: '/resources',
controller: 'resourcesController',
controllerAs: 'resourcesController',
templateUrl: '/app/pages/resources/resources.view.html',
})
.state('packages',
{
url: '/packages',
controller: 'packagesController',
controllerAs: 'packagesController',
templateUrl: '/app/pages/packages/packages.view.html',
})
After some time, it was apparent that the packages.view.html and the resources.view.html were almost identical, and I am spending a chunk of time copy pasting between the two..
The Controllers are quite different, and of course within resources.view.html, I refer to:
<button ng-click="resourcesController.fireButton()">Click me</button>
and in packages.view.html
<button ng-click="packagesController.fireButton()">Click me</button>
Now, before, back in good ole Angular 1.3, I'd have used $scope in each controller, and could declare the same html.. but now? is it even possible?
I ask because I'm about to include a third controller/template, and want to avoid writing and changing three templates.
p.s. I think I could, if I had the budget/hours/time, refactor into a sub-component and pass all the data to it, so that's not going to be a valid answer.
(yet if the only solution takes more time, then I'll present that to my manager)

Related

From 1 to 3 controllers in Angular

I'm new to angularJS so I'm still learning the "angular way" of doing things and therefore seek for advice.
I started building my login/register/forgotten_password views which now work perfectly fine. So the routing looks basically like this:
mainApp.config(function ($routeProvider) {
$routeProvider
.when('/login',
{
controller: 'loginCtrl',
templateUrl: 'views/login.html'
})
.when('/register',
{
controller: 'registerCtrl',
templateUrl: 'views/register.html'
})
.when('/dashboard',
{
controller: 'dashboardCtrl',
templateUrl: 'views/dashboard.html'
})
})
Now here's the problem and/or question I have :
After a successful login, you get redirected to the dashboard which gets loaded into ng-view. The dashboard is indeed the view but there should be more controllers and templates once you enter the app (sidebar, topbar, chat...).
How would you approach this ?
I also have a globalCtrl on the html element to handle other things, just in case that information might be helpful in any way.
You shouldn't think in controllers anymore. Rather think in "components". There are lots of articles on the internet on how to learn this (better) approach. Here is one to start: Refactoring Angular Apps to Component Style
tl;dr; Create a component (element directive) for every section/part of your view. In your case a <dashboard>, <sidebar>, <topbar>, ...
You can (and should) route to components! See this issue for more information. Here is a "real life" example on how to achieve this: https://github.com/ui-router/sample-app-ng1
This is the con giving of using $routeProvider in Angular JS. When you use a $routeProvider to define routes in angular JS, a single route can only point to a single view and there is no concept of nested views using $routeProvider.
And when you talk about having more than one template inside the view, you are talking about having nested views inside a single view. Unfortunately Angular's $routeProvider doesn't provide that. But now comes the ui-router, which replace the concept of route with states. You can define multiple sub states in a state and each sub state can point to different template. Look at the following link and follow simple steps to power up views and nested views in your app.
ui-router

ui-router require in multiple sibling views

How it is
I'm working on an application which has a little but more complex form than usual. I've splitted it up by having different views and controllers, but still keeping them inside a form element.
It has different sections but the payload of the request should contains all the information when I do the POST request. So basically that's why it has one main form (well it's not about having it inside a form or not, I could even have it without a form and then collecting models and submitting it by including all model's data into one main).
Question
My question is:
Does ui-router has a kind of required field to set? Just in order to have the required controller's functions and scope.
Does anyone knows a better solution?
Code
The code is just an example, this is not literally the code I have.
View (sectionMain.html)
<form>
<div ui-view="section1"></div>
<div ui-view="section2"></div>
<div ui-view="section3"></div>
<button type="submit">Submit</button>
</form>
State definition
$stateProvider.state('myState', {
url: '/mstate',
parent: 'stateForAuthenticatedUsers',
views: {
//this will be loaded what I have above. Just a part of the page.
'content': {
templateUrl: 'modules/myModule/views/sectionMain.html',
controller: 'Section1Ctrl as vm'
},
'section1#myState': {
templateUrl: 'modules/myModule/views/section1.html',
controller: 'Section1Ctrl as vmState1'
},
'section2#myState': {
templateUrl: 'modules/myModule/views/section2.html',
controller: 'Section2Ctrl as vmState2'
},
'section3#myState': {
templateUrl: 'modules/myModule/views/section3.html',
controller: 'Section3Ctrl as vmState3'
},
}
});
Controller (for example State2Ctrl)
function State2Ctrl($scope) {
//stuffs here. Need to refer parent's scope somehow
$scope.vm.someVariableOrObject
}
My concern
My only concern is that trusting a value vm which has been defined in the state's configuration doesn't give me the feeling that it's a well composed structure. I'd really need something I can require to, to have it explicitly instead of blindly trusting in a value vm.
Also my views contains the vm.someVariable in order to refer the parent's scope. But I don't like it.
Why not simply use your $rootScope? An easy way to achieve what you are looking for, without passing in a $scope every time.

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.

Combating AngularJS executing controller twice

I understand AngularJS runs through some code twice, sometimes even more, like $watch events, constantly checking model states etc.
However my code:
function MyController($scope, User, local) {
var $scope.User = local.get(); // Get locally save user data
User.get({ id: $scope.User._id.$oid }, function(user) {
$scope.User = new User(user);
local.save($scope.User);
});
//...
Is executed twice, inserting 2 records into my DB. I'm clearly still learning as I've been banging my head against this for ages!
The app router specified navigation to MyController like so:
$routeProvider.when('/',
{ templateUrl: 'pages/home.html',
controller: MyController });
But I also had this in home.html:
<div data-ng-controller="MyController">
This digested the controller twice. Removing the data-ng-controller attribute from the HTML resolved the issue. Alternatively, the controller: property could have been removed from the routing directive.
This problem also appears when using tabbed navigation. For example, app.js might contain:
.state('tab.reports', {
url: '/reports',
views: {
'tab-reports': {
templateUrl: 'templates/tab-reports.html',
controller: 'ReportsCtrl'
}
}
})
The corresponding reports tab HTML might resemble:
<ion-view view-title="Reports">
<ion-content ng-controller="ReportsCtrl">
This will also result in running the controller twice.
AngularJS docs - ngController
Note that you can also attach controllers to the DOM by declaring it
in a route definition via the $route service. A common mistake is to
declare the controller again using ng-controller in the template
itself. This will cause the controller to be attached and executed
twice.
When you use ngRoute with the ng-view directive, the controller gets attached to that dom element by default (or ui-view if you use ui-router). So you will not need to attach it again in the template.
I just went through this, but the issue was different from the accepted answer. I'm really leaving this here for my future self, to include the steps I went through to fix it.
Remove redundant controller declarations
Check trailing slashes in routes
Check for ng-ifs
Check for any unnecessary wrapping ng-view calls (I accidentally had left in an ng-view that was wrapping my actual ng-view. This resulted in three calls to my controllers.)
If you are on Rails, you should remove the turbolinks gem from your application.js file. I wasted a whole day to discover that. Found answer here.
Initializing the app twice with ng-app and with bootstrap. Combating AngularJS executing controller twice
When using $compile on whole element in 'link'-function of directive that also has its own controller defined and uses callbacks of this controller in template via ng-click etc. Found answer here.
Just want to add one more case when controller can init twice (this is actual for angular.js 1.3.1):
<div ng-if="loading">Loading...</div>
<div ng-if="!loading">
<div ng-view></div>
</div>
In this case $route.current will be already set when ng-view will init. That cause double initialization.
To fix it just change ng-if to ng-show/ng-hide and all will work well.
Would like to add for reference:
Double controller code execution can also be caused by referencing the controller in a directive that also runs on the page.
e.g.
return {
restrict: 'A',
controller: 'myController',
link: function ($scope) { ....
When you also have ng-controller="myController" in your HTML
When using angular-ui-router with Angular 1.3+, there was an issue about Rendering views twice on route transition. This resulted in executing controllers twice, too. None of the proposed solutions worked for me.
However, updating angular-ui-router from 0.2.11 to 0.2.13 solved problem for me.
I tore my app and all its dependencies to bits over this issue (details here: AngularJS app initiating twice (tried the usual solutions..))
And in the end, it was all Batarang Chrome plugin's fault.
Resolution in this answer:
I'd strongly recommend the first thing on anyone's list is to disable it per the post before altering code.
If you know your controller is unintentionally executing more than once, try a search through your files for the name of the offending controller, ex: search: MyController through all files. Likely it got copy-pasted in some other html/js file and you forgot to change it when you got to developing or using those partials/controllers. Source: I made this mistake
I had the same problem, in a simple app (with no routing and a simple ng-controller reference) and my controller's constructor did run twice. Finally, I found out that my problem was the following declaration to auto-bootstrap my AngularJS application in my Razor view
<html ng-app="mTest1">
I have also manually bootstrapped it using angular.bootstrap i.e.
angular.bootstrap(document, [this.app.name]);
so removing one of them, it worked for me.
In some cases your directive runs twice when you simply not correct close you directive like this:
<my-directive>Some content<my-directive>
This will run your directive twice.
Also there is another often case when your directive runs twice:
make sure you are not including your directive in your index.html TWICE!
Been scratching my head over this problem with AngularJS 1.4 rc build, then realised none of the above answers was applicable since it was originated from the new router library for Angular 1.4 and Angular 2 at the time of this writing. Therefore, I am dropping a note here for anyone who might be using the new Angular route library.
Basically if a html page contains a ng-viewport directive for loading parts of your app, by clicking on a hyperlink specified in with ng-link would cause the target controller of the associated component to be loaded twice. The subtle difference is that, if the browser has already loaded the target controller, by re-clicking the same hyperlink would only invoke the controller once.
Haven't found a viable workaround yet, though I believe this behaviour is consistent with the observation raised by shaunxu, and hopefully this issue would be resolved in the future build of new route library and along with AngularJS 1.4 releases.
In my case, I found two views using the same controller.
$stateProvider.state('app', {
url: '',
views: {
"viewOne#app": {
controller: 'CtrlOne as CtrlOne',
templateUrl: 'main/one.tpl.html'
},
"viewTwo#app": {
controller: 'CtrlOne as CtrlOne',
templateUrl: 'main/two.tpl.html'
}
}
});
The problem I am encountering might be tangential, but since googling brought me to this question, this might be appropriate. The problem rears its ugly head for me when using UI Router, but only when I attempt to refresh the page with the browser refresh button. The app uses UI Router with a parent abstract state, and then child states off the parent. On the app run() function, there is a $state.go('...child-state...') command. The parent state uses a resolve, and at first I thought perhaps a child controller is executing twice.
Everything is fine before the URL has had the hash appended.
www.someoldwebaddress.org
Then once the url has been modified by UI Router,
www.someoldwebaddress.org#/childstate
...and then when I refresh the page with the browser refresh button, the $stateChangeStart fires twice, and each time points to the childstate.
The resolve on the parent state is what is firing twice.
Perhaps this is a kludge; regardless, this does appear to eliminate the problem for me: in the area of code where $stateProvider is first invoked, first check to see if the window.location.hash is an empty string. If it is, all is good; if it is not, then set the window.location.hash to an empty string. Then it seems the $state only tries to go somewhere once rather than twice.
Also, if you do not want to rely on the app's default run and state.go(...), you can try to capture the hash value and use the hash value to determine the child state you were on just before page refresh, and add a condition to the area in your code where you set the state.go(...).
For those using the ControllerAs syntax, just declare the controller label in the $routeprovider as follows:
$routeprovider
.when('/link', {
templateUrl: 'templateUrl',
controller: 'UploadsController as ctrl'
})
or
$routeprovider
.when('/link', {
templateUrl: 'templateUrl',
controller: 'UploadsController'
controllerAs: 'ctrl'
})
After declaring the $routeprovider, do not supply the controller as in the view. Instead use the label in the view.
In my case it was because of the url pattern I used
my url was like /ui/project/:parameter1/:parameter2.
I didn't need paramerter2 in all cases of state change. In cases where I didn't need the second parameter my url would be like /ui/project/:parameter1/. And so whenever I had a state change I will have my controller refreshed twice.
The solution was to set parameter2 as empty string and do the state change.
I've had this double initialisation happen for a different reason. For some route-transitions in my application I wanted to force scrolling to near the top of the page (e.g. in paginated search results... clicking next should take you to the top of page 2).
I did this by adding a listener to the $rootScope $on $viewContentLoaded which (based on certain conditions) executed
$location.hash('top');
Inadvertently this was causing my routes to be reevaluated and the controllers to be reinitialised
My issue was updating the search parameters like so $location.search('param', key);
you can read more about it here
controller getting called twice due to append params in url
In my case renaming the controller to a different name solved the problem.
There was a conflict of controller names with "angular-ui-tree" module: I renamed my controller from "CatalogerTreeController" to "TreeController" and then this controller starts to be initiated twice on the page where "ui-tree" directive used because this directive uses controller named "TreeController".
I had the same problem and after trying all the answers I finally found that i had a directive in my view that was bound to the same controller.
APP.directive('MyDirective', function() {
return {
restrict: 'AE',
scope: {},
templateUrl: '../views/quiz.html',
controller: 'ShowClassController'
}
});
After removing the directive the controller stopped being called twice. Now my question is, how can use this directive bound to the controller scope without this problem?
I just solved mine, which was actually quite disappointing. Its a ionic hybrid app, I've used ui-router v0.2.13. In my case there is a epub reader (using epub.js) which was continuously reporting "no document found" once I navigate to my books library and select any other book. When I reloaded the browser book was being rendered perfectly but when I selected another book got the same problem again.
My solve was very simple. I just removed reload:true from $state.go("app.reader", { fileName: fn, reload: true }); in my LibraryController
I have the same issue in angular-route#1.6.7, and it because the extra slash in the end of regex route:
.when('/goods/publish/:classId/', option)
to
.when('/goods/publish/:classId', option)
and it works correctly.
Just adding my case here as well:
I was using angular-ui-router with $state.go('new_state', {foo: "foo#bar"})
Once I added encodeURIComponent to the parameter, the problem was gone: $state.go('new_state', {foo: encodeURIComponent("foo#bar")}).
What happened?
The character "#" in the parameter value is not allowed in URLs. As a consequence, angular-ui-router created my controller twice: during first creation it passed the original "foo#bar", during second creation it would pass the encoded version "foo%40bar". Once I explicitly encoded the parameter as shown above, the problem went away.
My issue was really difficult to track down. In the end, the problem was occurring when the web page had missing images. The src was missing a Url. This was happening on an MVC 5 Web Controller. To fix the issue, I included transparent images when no real image is available.
<img alt="" class="logo" src="">
I figured out mine is getting called twice is because i was calling the method twice from my html.
`<form class="form-horizontal" name="x" ng-submit="findX() novalidate >
<input type="text"....>
<input type="text"....>
<input type="text"....>
<button type="submit" class="btn btn-sm btn-primary" ng-click="findX()"
</form>`
The highlighted section was causing findX() to be called twice. Hope it helps someone.

Resources