At the moment I have an app that has a sidebar, and the sidebar loads different html templates using ng-include based on what operation the user chooses to do. It's a map related app, so for example, if the user selects the 'Add Leg' button it will load the add_leg.html template into the sidebar using ng-include:
// The Javascript code:
$scope.addLeg = function() {
$scope.sidebar = true;
$scope.sidebar_template = '/partials/legs/add_leg.html';
}
// Simplified example of HTML:
<html>
<div ng-app="MyApp" ng-controller="MainCtrl">
<a ng-click="addLeg()">Add Leg</a>
<a ng-click="addRoute()">Add Route</a>
<a ng-click="editLeg()">Edit Leg</a>
<a ng-click="editRoute()">Edit Route</a>
...
<div id="sidebar" ng-class="{'sidebar-hidden': sidebar == false}">
<div ng-include src="sidebar_template"></div>
</div>
<google-map></google-map>
</div>
This is all well and good and works as desired, but at the moment my app is only using one controller (MainCtrl in js/controllers.js) and it's starting to get really cluttered. I've got a lot of methods now because the apps functionality is expanding. I'd like to split my controller up into multiple controllers whilst retaining this sidebar functionality.
I want to have MainCtrl as the main controller that controls the loading of the sidebar template (by changing the sidebar_template variable that points to the file destination), and I want it to control some of the global map related methods (like fetching suburb names from coordinates, etc).
I've tried to split it like so:
controllers/js/main.js - MainCtrl
controllers/js/legs.js - LegCtrl
controllers/js/routes.js - RouteCtrl
I want the LegCtrl and RouteCtrl to inherit the MainCtrl so I can access its scope and methods, that's all fine. But now the problem is how do I dynamically load the controller onto the sidebar div based on what functionality is required. Originally all of my methods were in MainCtrl, and that's on the wrapper div that surrounds the sidebar div (see above again for an example), so it wasn't a problem.
For example, say I press the 'Add Leg' button, it's going to need to call addLeg in LegCtrl, but LegCtrl isn't loaded on the app, so it doesn't have access to the method. I could keep addLeg() inside the MainCtrl, and have it change the sidebar_template variable to load the template, but nothing in the template will work because it is calling methods from inside the LegCtrl now.
I need to somehow dynamically load the controller on the sidebar's ng-include div, something like this perhaps:
// MainCtrl
$scope.addLeg = function() {
$scope.required_controller = LegCtrl;
$scope.sidebar = true;
$scope.sidebar_template = '/partials/legs/add_leg.html';
LegCtrl.addLeg();
}
<div id="sidebar" ng-class="{'sidebar-hidden': sidebar == false}">
<div ng-include src="sidebar_template" ng-controller="{{required_controller}}"></div>
</div>
In the non-working example above you can see a possible solution I've thought of, but I need LegCtrlto be the actual controller function, not an object (for ng-controller to work). I also need some way to call addLeg on the LegCtrl from the MainCtrl.addLeg (perhaps using broadcast?).
If anyone can point me in the right direction that'd be great. Sorry for the huge post, it needed a bit of explaining to make it coherent. Hopefully it makes sense. Thanks.
Update: I think I've found a solution using a service to act as the navigation control (it will load the relevant templates and broadcast an event to the new controller being dynamically loaded to tell it what function to execute):
http://plnkr.co/edit/Tjyn1OiVvToNntPBoH58?p=preview
Just trying to test this idea out now, but the broadcast .on doesn't work. I think it's because the broadcast fires before the template loads. How can I fix this? Is this a good solution for what I'm doing?
If i have understood you correctly what you can try would be to create a template view specifically to create a new leg.
From the main controller implement some logic to show the template
$scope.addLeg=function() {
$scope.showAddLeg=true;
}
The AddLeg template view would load the controller and hence provide a mechanism to actually add new leg. The template would look like
<script type="text/ng-template" class="template" id="addLegTemplate">
<div ng-controller='LegsController'>
<!--html for adding a new leg-->
</div>
</script>
You can include this template inside you main html using ng-if + ng-include.
<div ng-if='showAddLeg'><div ng-include='addLegTemplate'/></div>
Basically you can create multiple view and bind to same controller (but instance would differ). In this case the LegsController can be binded to multiple views to support the complete functionality.
I like this data driven scenario, just manipulate the rows in the templates:
$scope.templates = [
{ name: 'include1.html', url: 'partials/include1.html' }
];
$scope.addTemplate = function(name, url) {
$scope.templates.push({ name: name, url: url });
};
and the markup:
<div ng-repeat="template in templates" ng-include="template.url"></div>
To add or remove views, just modify the templates et voila! If you need more controller code, then you can include a link to the script in the include. I guess there may be complications with binding to data in the partial view itself, but $compile should resolve those.
I've forked your plunkr demo into one that I believe does what you're looking for: http://plnkr.co/edit/whsjBT?p=preview
This demonstrates an event being broadcast from one controller to another AFTER the 2nd controller (LegCtrl in our example here) is loaded via ng-include and passing it data.
I used $includeContentLoaded event from ng-include to delay broadcasting the event until angular reports that add_leg.html is loaded. Also I think there were some issues with how you were using $broadcast()... it's first parameter should be the same as the one used in $on() in LegCtrl
Another idea to consider is simply using your Navigation service itself to share state between the controllers if that's appropriate in your scenario.
Related
Question on loading second controller on click of button
Can I use controller inside another controller (like my example
below)
On click of "showMore" div, is it possible to call another
controller to load content in dialog ?
page1.jsp
<div ng-controller="FirstController">
<div ng-click="showMore()">Text 1</div>
<div ng-click="showMore()">Text 2</div>
</div>
<!-- my modal dialog -->
<script type="text/ng-template" id="SecondController.html">
<div ng-controller="SecondController" style="top:20px;left:300px;position:absolute;">
<div ng-repeat="content in Contents">{{content}}</div>
</div>
</script>
page1.js
var app = angular.module('MyApp',[]);
app.controller("FirstController", function($scope, $templateCache){
$scope.showMore = function(){
$templateCache("SecondController.html");
// what should be done here to open Second Controller
// and populate "Contents" div
}
});
Can I use controller inside another controller (like my example below)
Yes, the way you have set up your template will allow you to use a separate controller for the modal subsection. It might however be better to go about it in another way.
On click of "showMore" div, is it possible to call another controller to load content in dialog ?
Angular controllers rarely directly interact, so information is mostly passed through services or broadcasts depending on your need. In this case, you need to populate the Contents variable in either the parent scope (FirstController) or Secondcontroller.Contents. In the latter case, you might need to change the ng-repeat reference to SecondController.Contents.
I would suggest that the easiest way to go about the intended functionality as I understand your intent, is to use a modal library like f.e. the Angular UI: http://angular-ui.github.io/bootstrap/ or ngDialog https://github.com/likeastore/ngDialog . Unless you have a specific reason not to, both of these projects have good examples of using modal dialogs that you can adjust to your own needs in your controllers.
You don't need to "load" the controller explicitly. Angular loads an associated controller when ng-controller directive is $compiled, or when route is used.
For you cases, you can do:
In the FirstController controller, declare a boolean on the scope:
$scope.showMore = false;
In the view:
<div ng-controller="FirstController">
<div ng-click="showMore = true">Text 1</div>
<div ng-click="showMore = true">Text 2</div>
</div>
<div ng-if="showMore" ng-include="'SecondController.html'"/>
You could also use states angular-ui-router (as suggested by another answer), but I feel that states are better suited for well-defined app states, and not so much for dialogs, which you want in your question.
P.S.
Also, the way you named your template - i.e. "SecondController.html" - suggests that your are thinking of controllers and views as coupled. They should not be. The controller should not care about whether it has a view/html associated with, what it is, and whether that HTML is displayed or not. All the controller should care about is managing the ViewModel "state" of the small part of the app that it controls, and marshaling data between ViewModels and Models.
You can have nested controllers, but the markup content will not be loaded from a template unless you do ugly things like <ng-include src="{{ somevar }}" (dont do that)
To actually have real nested routes, you'd be better off using something like:
angular-ui-router: [https://github.com/angular-ui/ui-router]
angular route segment: [http://angular-route-segment.com/]
i have this lightbox that pops up from time to time. There are 3 ways to close it. Close button, clicking the overlay that's outside it and another way. For the close action i decided to do a factory that looks like this:
app.factory('close',function(){
return {
close: function(){
$('.mySelector').fadeOut();
}
}
});
One of my HTML elements that i want to have close the lightbox would be something like this:
close lightbox
The question i am faced with is this: Should i inject the factory in a directive (seeing as it manipulates the DOM) or in a controller? - the controller is there either way since i need it for other stuff. The advantage of the first is that my markup would only have a controller attached to the top element of the lightbox and be done with it. The directive on the other hand will have to be attached to each element individually + there's the directive code itself (little of it may be).
This translates to:
added code for directive +
<top-element-of-lightbox ng-controller="myController"> <!-- controller does NOT have factory injected -->
<a href="" ng-click="close" **my-directive**>close lightbox1</a> <!--directive attached to element -->
<a href="" ng-click="close" **my-directive**>close lightbox2</a> <!--directive attached to element -->
...
</top-element-of-lightbox>
Versus:
0(zero) directive code +
<top-element-of-lightbox ng-controller="myController"> <!-- controller HAS factory injected -->
close lightbox1 <!-- no directive attachment -->
close lightbox2 <!-- no directive attachment -->
...
</top-element-of-lightbox>
Lastly, i am new to Angular so i would appreciate if you justify your response and also please tell me if i am going about this whole thing the right way and if there are areas where i could improve or an other approach would be better.
Do not manipulate your DOM in controller or services(service,
factory). You must do that in directive
Instead of using jQuery selector you can directly get hold of element and manipulate it inside directive
Using angular animations instead of relying on jQuery
Most importantly all your queries and above issue you face is solved in this awesome video which just explains creating a directive to hide stuff with animations.
https://egghead.io/lessons/angularjs-animating-the-angular-way
Cheers!!!!
I have an AngularJS application that I believe is essentially pretty typical (alike many of the examples).
<html ng-app="myApp" ...
<body>
...
<div class="main" ng-view></div>
...
There's a $routeProvider that I've set up with a whole lot of whens to direct users to a view (template partial with a controller), such as:
$routeProvider.when('/incident/:identifier', {templateUrl:'incident.html', controller:"IncidentCtrl"});
This is all working fine. Users can go back and forth between views.
Now, let's say I have a view for an "incident". It has a form with properties of the "incident". (You can change values in the form and "Save", which is besides the point here.) I have the following requirements:
"Add work order" button below the existing form
When button clicked, a form (partial) is loaded below the existing form to enter details for a new "work order"
"Save" button as part of the loaded form (partial) will save the new "work order"
The form (partial) is closed, unloaded or at least visually removed
Possibly go back to 2. to repeat (to add subsequent "work orders")
For the form that is loaded below the existing form I would like to reuse an existing template partial with its subsequent controller, which I'm already using for the top-level ng-view elsewhere. But I've gone down a few roads to implement this at no avail; ng-include seems to not work, as it is pretty static and does not allow for much of a lifecycle on the embedded element. Also, it seems hard to load it dynamically, which means it's gonna be loaded before the "Add work order" button is ever clicked. Could someone point me to a workable strategy? Clearly my goal is to promote reuse of existing template partial and controller, without always having the user to move between views. Much appreciated.
Edit: elaborating: I'm not happy (yet) with any ng-include ideas that I've seen so far, since:
There would be a bunch of them if I had more than just a single kind of view to embed. They would all get loaded ahead of being shown for no good reason; overkill
I do not know how to parameterize the embedded views in the same way I can pass $routeParams from the $routeProvider
I am uncomfortable of too much sharing between parent and child scopes
I have no way to cleanly recreate the controller for subsequent "adds"
I would try to address your concerns with ng-include
There would be a bunch of them if I had more than just a single kind
of view to embed. They would all get loaded ahead of being shown for
no good reason; overkill
ng-if can help you here. This directive would not load DOM till the condition becomes true. And it also destroys the DOM when the condition becomes false. So something like
<div ng-if='conditionwhenthepartialshouldbeshown'>
<ng-include src='templateName' ng-init='model=parent.someProperty'/>
</div>
I do not know how to parameterize the embedded views in the same way I
can pass $routeParams from the $routeProvider
You can use ng-init for passing some parameters when the view is loaded. Check documentation
I am uncomfortable of too much sharing between parent and child scopes
ng-init can help here again. You can create a property on child scope and pass it the parent scope value in ng-init
I have no way to cleanly recreate the controller for subsequent "adds"
ng-if does this for you.
The way that I found to reproduce this behavior was using Two controllers: One for your "main" view and one for your partial view.
In your controllers file
var controllers = angular.module('controllers', []);
controllers.controller('IncidentCtrl', ['$scope', function($scope) {
// Incident Ctrl Body
$scope.showForm = false;
$scope.toggleForm = function() {
$scope.showForm = !$scope.showForm;
}
}]);
controllers.controller('WorkOrderCtrl', ['$scope', function($scope) {
// Partial Form controller
// This controller can talk with $scope from IncidentCtrl
// using the $scope.$parent.$parent
$scope.save = function() {
// ...
$scope.$parent.$parent.showForm = false;
}
}]);
And your views:
<!-- incident.html -->
<button data-ng-click="toogleForm()"> Toggle Form </button>
<div data-ng-show="showForm">
<div ng-include src="'path/to/_work_order.html'"></div>
</div>
<!-- _work_order.html -->
<div data-ng-controller="WorkOrderCtrl">
<!-- view body -->
<button data-ng-click="save()"> Save </button>
</div>
I am building an AngularJS application that has a sidebar and a main content area. Both are populated by separate invocations of the $http service to retrieve some JSON data. The stuff in the sidebar is basically a table of contents and the intent is that clicking on one of the sidebar items will cause the main area to be updated.
My initial stab at this involved putting the sidebar and main area into one partial and then associating it with a controller that does both retrievals. The application has a route that associates the controller to a URL, and the links in the sidebar access these URL with the appropriate parameter that will cause the desired content to appear in the main area. All this works, but it does cause the whole page to refresh. The "partial" really is a "total".
What I'd like to do is somehow cause a click on the sidebar links to trigger a refresh of the main content area only. One thought is to somehow split it into two controller/partial pairs and figure out a way to cause sidebar clicks to request an update. I'm not sure about the mechanics of doing this, though. Another approach is to keep the stuff in one controller and use some kind of shared state that would trigger this update. I attempted to do this by setting an ng-click directive on the links, but this did not update a scope variable, as I had hoped.
Is there a recommended way of structuring an application to achieve this kind of AJAX-driven partial page update? It seems like a fairly common case, but I haven't mastered enough of AngularJS to get a solution.
This is what I'm doing:
I have 2 services, 1 for the sidemenu and the other for the main content. They are both injected into the controller.
To handle cross service calls I use $broadcast to send events.
Works really well and is very clean code.
additional info on using services based on comment
For the sidemenu i have a shared menu service, that way all controllers can use the same menu.
The maincontent service doesnt have to be shared, but i use them because services don't loose their data when the controller goes out of scope. If I didn't the controller would have to use some other mechanism to repopulate its data. For me a service handles it without having to code anything
To show the different views i use the following main layout html
<div >
<!-- left menu -->
<div id="left" ng-include src="menu.view" ></div>
<!-- main content -->
<div id="main" ng-include src="maincontent.view"></div>
</div>
the controller
function myControllerCtrl($scope, $rootScope, menuService, maincontentService) {
$scope.menu = menuService;
$scope.maincontent = mainContentService
}
the menu service
app.factory('menuService', ['$rootScope', function ($rootScope) {
var service = {
view: 'leftmenuview.html',
MenuItemClicked: function (data) {
$rootScope.$broadcast('menuitemclicked', data);
}
};
return service;
}]);
the main content service
app.factory('maincontentService', ['$rootScope', function ($rootScope) {
var service = {
view: 'maincontentview.html',
MenuItemClicked: function(data){
//handle updating of model based on data here
}
};
$rootScope.$on('menuitemclicked', function (event, data) { service.MenuItemClicked(data) });
return service;
}]);
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'"/>