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;
}]);
Related
I'm currently using one controller for my web app. Data is loaded from a JSON file and shown to the user via ng-repeat. When a user makes a selection, only data for the user's selection is in the scope.
I'd like to be able to keep the same scope data and use it across different web pages (or states using UI-Router).
I've looked into using ui-router but it seems like the controller would be refreshed with every state change.
I'd prefer to use ui-router due to design requirements.
Part of my controller code:
(function() {
"use strict";
angular
.module("parkit", ["ngMap"])
.controller("parkitController", function($scope, $rootScope, $http,
parkitFactory, NgMap) {
parkitFactory.getSpots().then(function(spots) {
$scope.spots = spots.data;
});
$scope.showSpot = function(spot) {
$scope.spot = spot;
}
});
})();
Factory code for loading JSON data:
(function() {
"use strict";
angular.module("parkit")
.factory("parkitFactory", function($http) {
function getSpots() {
return $http.get('/data/spots.json');
}
return {
getSpots: getSpots
}
});
})();
As it has been answered before, you can use a factory or service to keep track of the selected item(s). This would store the selected values in the instance of the service/factory and therefore would be lost if someone refreshes the page.
A more resilient, and in my opinion beautiful solution, would be to add the selected item(s) as state parameter in ui-router. Using this method, you will also be able to deep-link to certain selected states and if someone refreshes the page, the same items would still be selected, as you would add your state parameters in the url.
See URL Parameters in the documentation: https://github.com/angular-ui/ui-router/wiki/URL-Routing
You may probably create a new property in the factory function to keep track of the selected item.Set the property when user does a selection. Get the property in other components whereever you need to use the data.
use $rootScope instead of $scope to save your data, That will allow you to use it anywhere in controllers of same domain.
example: $rootScope.yourData = yourData; and then you can assign $rootScope.yourData to any controller in same domain.
I want to create a directive of a sidebar.
I have function in the controller that toggles the sidebar.
How can I access this function from any other controller in my application?
Thanks!
If you want to manipulate with your SideBar from whole your application, it is probably better way to save the state of SideBar in the Service. So you will have access to this service whenever you inject it, and you will able to change state of SideBar by changing this related Service value (It is possible because Services are always have only one instance, like Singleton pattern).
So, I will attach image which will show basic communication pattern for current case.
If we will change the state of active variable in the State Service, the SideBar Directive will be affected, because all three controllers are linked to the same instance of service.
Here Controllers service communication is small example of communication between controllers via service.
Hope it will help you!
I have function in the controller that toggles the sidebar
ItsĀ“not recomended to use any UI logic inside Controllers. In the World of Angular these things can be achieved by using declerative HTML.
<a ng-click="isReplyFormOpen = !isReplyFormOpen">Reply</a>
<div ng-show="isReplyFormOpen" id="replyForm"></div>
To answer your question: You can define a service which contains your function.
(function(){
angular.module('YourApp')
.factory('ToggleBar', function(){
function anyFunc(){
//Logic here...
}
return{
toggle: anyFunc
};
});
)();
Than inject the service to any Controller you whant to.
(function(){
angular.module('YourApp')
.controller('ControllerName',['ToggleBar',function(ToggleBar){
//Call the service func...
ToggleBar.toggle();
}]);
)();
When working with the $ionicModal in Ionic Framework, I noticed a lot of people instantiate the Modal inside the controller and pass the controller scope to the Modal.
Like so,
$ionicModal.fromTemplateUrl("views/call_options_view.html", function ($ionicModal) {
$scope.menu = $ionicModal;
}, {
scope: $scope,
animation: "slide-in-up"
});
Doing this allows the modal to invoke methods in the controller scope. Is there some way we can give a separate controller to the Modal?
Right now, using the controller scope, isn't there a MVC violation? The controller owns two views. Suppose I want the same modal available on another controller, then I would have to duplicate my functionality for the modal on both the controllers. MVC is supposed improve code reuse. So essentially, I want to re-enforce MVC by giving my modal a separate controller.
One way I thought of fixing this is by putting the modal in the Root Controller. Doing so, will make it accessible from all the child controllers and the functionality for the modal will only be available in the root controller. I still don't like this fix, cause i don't want to clutter my root controller with too much logic.
Any other suggestions?
I stumbled on your question while trying to come up with a solution similar to your concern.
Because I had a problem regarding navigation in my routes, I decided to use $ionicModal to show a view of another state in a modal view. I came up with a solution I crafted there (but I did not implement it for my working context yet) that should work in my case, while I'm not really satisfied with it.
To summarize, all my states are nested under tabs; when I am in the tabs.home state, I want to directly show the tabs.settings.sub state.
However, tabs.settings.sub relies on data populated by its parent state tabs.settings. Hence my problem with giving the scope of my current state (tabs.home) to tabs.settings.sub.
My modal uses a template that will include the template of my view:
<script id="templates/modal.html" type="text/ng-template">
<ion-modal-view>
<ng-include src="templateUrl" ng-controller="controller"></ng-include>
</ion-modal-view>
</script>
I can then reuse the view from the state. Regarding the scope, I used $scope.new(true) to isolate it, and populated it with data required by my modal template:
var subState = $state.get ('tabs.settings.sub');
var subScope = $scope.$new (true); // true: isolate
subScope.title = 'Sub';
subScope.templateUrl = subState.templateUrl;
subScope.controller = function () {
if (subState.controller)
return $controller (subState.controller, {$scope:subScope});
return null;
};
The modal is instantiated using this scope (that's one problem to my opinion: mixing the scope of the modal and the scope of the controller).
The controller has to be a function that returns the appropriate controller.
$ionicModal.fromTemplateUrl ('templates/modal.html', {
scope: subScope
}).then (function (modal) {
modal.show ();
});
The major problem with my solution is to transit data up to the controller of the view to show (in this case SubCtrl). But it is more narrowed to my specific context: my modal is not aware of the chain of inheritance of controllers adn states, because this is handled by UI router.
I don't know if it is possible to access the state associated to a controller (the usual pattern seems to be to use $state.parent, but this cannot be used here, as mentioned by the UI router wiki).
The workaround I use here (this is the part I am not satisfied with) is to federate data through the states:
.state ('tabs.settings', {
data: { status: 'valid' }
}
I have access to it when creating my modal:
subScope.status = subState.data.status;
And I have access to it from the parent controller:
$scope.status = $state.current.data.status;
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>
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.