Preserve state with Angular UI-Router - angularjs

I have an app with a ng-view that sends emails to contact selected from a contact list.
When the users select "Recipient" it shows another view/page where he can search, filter, etc. "Send email" and "Contact list" are different html partials that are loaded in the ng-view.
I need to keep the send form state so when the users select someone from the Contact List it returns to the same point (and same state). I read about different solutions ($rootScope, hidden divs using ng-show, ...) but I want to know if UI-router will help me with it's State Manager. If not, are there other ready-to-use solutions?
Thanks!

The solution i have gone with is using services as my data/model storage. they persist across controller changes.
example
the user service ( our model that persists across controller changes )
app.factory('userModel', [function () {
return {
model: {
name: '',
email: ''
}
};
}]);
using it in a controller
function userCtrl($scope, userModel) {
$scope.user = userModel;
}
the other advantage of this is that you can reuse your model in other controllers just as easly.

I'm not sure if this is recommended or not, but I created a StateService to save/load properties from my controllers' scopes. It looks like this:
(function(){
'use strict';
angular.module('app').service('StateService', function(){
var _states = {};
var _save = function(name, scope, fields){
if(!_states[name])
_states[name] = {};
for(var i=0; i<fields.length; i++){
_states[name][fields[i]] = scope[fields[i]];
}
}
var _load = function(name, scope, fields){
if(!_states[name])
return scope;
for(var i=0; i<fields.length; i++){
if(typeof _states[name][fields[i]] !== 'undefined')
scope[fields[i]] = _states[name][fields[i]];
}
return scope;
}
// ===== Return exposed functions ===== //
return({
save: _save,
load: _load
});
});
})();
To use it, I put some code at the end of my controller like this:
angular.module('app').controller('ExampleCtrl', ['$scope', 'StateService', function ($scope, StateService) {
$scope.keyword = '';
$scope.people = [];
...
var saveStateFields = ['keyword','people'];
$scope = StateService.load('ExampleCtrl', $scope, saveStateFields);
$scope.$on('$destroy', function() {
StateService.save('ExampleCtrl', $scope, saveStateFields);
});
}]);

I have found Angular-Multi-View to be a godsend for this scenario. It lets you preserve state in one view while other views are handling the route. It also lets multiple views handle the same route.
You can do this with UI-Router but you'll need to nest the views which IMHO can get ugly.

Related

Update $scope variable that is used in ng-repeat, from other controller

I have a controller HomeworkPageController where I get all the topics from MongoDB using method getAllMainTopics from TopicService. $scope.topics is then used to show all topics. I have a button that open a modal where a new topic is add in MongoDB. The modal is using another controller AddTopicController. How can I update $scope.topics from HomeworkPageController in AddTopicController ? I want to do this because after I close the modal, the list of all topics should be refreshed, it must contain the topic that has been added. I tried to use HomeworkPageController in AddTopicController and then call the method getAllMainTopics but the $scope.topics from html is not updated. Thanks.
Here is HomeworkPageController:
app.controller('HomeworkPageController', ['$scope','TopicService',
function ($scope, TopicService) {
$scope.topics = [];
$scope.getAllMainTopics = function () {
TopicService.getAllMainTopics('homework')
.success(function(data) {
$scope.topics = data;
}
$scope.addTopic = function () {
ModalService.openModal({
template: "templates/addTopic.html",
controller: 'AddTopicController'
});
}
]);
Here is AddTopicController:
app.controller('AddTopicController', ['$scope','$controller', '$timeout','TopicService', '$modalInstance',
function ($scope, $controller, $timeout,TopicService, $modalInstance) {
var homeworkPageController = $scope.$new();
$controller('HomeworkPageController',{$scope : homeworkPageController });
$scope.save = function() {
TopicService.saveTopic(data)
.success(function(result){
homeworkPageController.getAllMainTopics();
$modalInstance.close();
})
}
}]);
Here is the view where I use $scope.topics:
<div class="homework-content-topic-list" ng-repeat="topic in topics">
<label> {{ topic.subject }} </label>
</div
You should probably keep your list of topics in a service and then inject that service into both controllers. This way you would be able to access and update the topics in both of your controllers. It could look something like
app.controller('HomeworkPageController', ['$scope','TopicService',
function ($scope, TopicService) {
$scope.topics = TopicService.topics;
// Do stuff here
]);
Then you just need to modify your TopicService to have it's methods work on the stored object.
you can solve this by two methods
1)look at the example given in ui-bootstrap's website. They have given an example that will suit your requirement - plunker. There are three items in the modal - item1, item2, item3. If you select one of those items and click 'ok', the selected item is sent to the main controller through "resolve" attribute in the $scope.open function.
2)You can write a custom service that acts as a bridge to the two controllers and you can write getter and setter methods in the service.
angular.module('app').service('popupPageService', function() {
var topics;
var setDetails = function(param) {
topics = param;
};
var getDetails = function() {
return topics;
};
return {
setDetails: setDetails,
getDetails: getDetails,
};
});
call the setDetails function in the AddTopicController and once when you come out of the modal, update your $scope.topics in HomeworkPageController by pushing the new value added (getDetails)

Can not figure out how to store $rootScope in angular.bootstrap

I'm trying to call a web service in AngularJS bootstrap method such that when my controller is finally executed, it has the necessary information to bring up the correct page. The problem with the code below is that of course $rootScope is not defined in my $http.post(..).then(...
My response is coming back with the data I want and the MultiHome Controller would work if $rootScope were set at the point. How can I access $rootScope in my angular document ready method or is there a better way to do this?
angular.module('baseApp')
.controller('MultihomeController', MultihomeController);
function MultihomeController($state, $rootScope) {
if ($rootScope.codeCampType === 'svcc') {
$state.transitionTo('svcc.home');
} else if ($rootScope.codeCampType === 'conf') {
$state.transitionTo('conf.home');
} else if ($rootScope.codeCampType === 'angu') {
$state.transitionTo('angu.home');
}
}
MultihomeController.$inject = ['$state', '$rootScope'];
angular.element(document).ready(function () {
var initInjector = angular.injector(["ng"]);
var $http = initInjector.get("$http");
$http.post('/rpc/Account/IsLoggedIn').then(function (response) {
$rootScope.codeCampType = response.data
angular.bootstrap(document, ['baseApp']);
}, function (errorResponse) {
// Handle error case
});
});
$scope (and $rootScope for that matter) is suppose to act as the glue between your controllers and views. I wouldn't use it to store application type information such as user, identity or security. For that I'd use the constant method or a factory (if you need to encapsulate more logic).
Example using constant:
var app = angular.module('myApp',[]);
app.controller('MainCtrl', ['$scope','user',
function ($scope, user) {
$scope.user = user;
}]);
angular.element(document).ready(function () {
var user = {};
user.codeCampType = "svcc";
app.constant('user', user);
angular.bootstrap(document, ['myApp']);
});
Note Because we're bootstrapping the app, you'll need to get rid of the ng-app directive on your view.
Here's a working fiddle
You could set it in a run() block that will get executed during bootstrapping:
baseApp.run(function($rootScope) {
$rootScope.codeCampType = response.data;
});
angular.bootstrap(document, ['baseApp']);
I don't think you can use the injector because the scope isn't created before bootstrapping. A config() block might work as well that would let you inject the data where you needed it.

Save state of views in AngularJS

I develop a HTML 5 app, and have a view where user comments are loaded. It is recursive: any comment can have clickable sub-comments which are loading in the same view and use the same controller.
Everything is OK. But, when I want to go back, comments are loading again, and I lose my position and sub-comments I loaded.
Can I save the state of the view when I go back? I thought I can maybe use some kind of trick, like: append a new view any time I click on sub-comment and hide the previous view. But I don't know how to do it.
Yes, instead of loading and keeping state of your UI inside your controllers, you should keep the state in a service. That means, if you are doing this:
app.config(function($routeProvider){
$routeProvider.when('/', {
controller: 'MainCtrl'
}).when('/another', {
controller: 'SideCtrl'
});
});
app.controller('MainCtrl', function($scope){
$scope.formData = {};
$scope./* other scope stuff that deal with with your current page*/
$http.get(/* init some data */);
});
you should change your initialization code to your service, and the state there as well, this way you can keep state between multiple views:
app.factory('State', function(){
$http.get(/* init once per app */);
return {
formData:{},
};
});
app.config(function($routeProvider){
$routeProvider.when('/', {
controller: 'MainCtrl'
}).when('/another', {
controller: 'SideCtrl'
});
});
app.controller('MainCtrl', function($scope, State){
$scope.formData = State.formData;
$scope./* other scope stuff that deal with with your current page*/
});
app.controller('SideCtrl', function($scope, State){
$scope.formData = State.formData; // same state from MainCtrl
});
app.directive('myDirective', function(State){
return {
controller: function(){
State.formData; // same state!
}
};
});
when you go back and forth your views, their state will be preserved, because your service is a singleton, that was initialized when you first injected it in your controller.
There's also the new ui.router, that has a state machine, it's a low level version of $routeProvider and you can fine grain persist state using $stateProvider, but it's currently experimental (and will ship on angular 1.3)
Use a Mediator
If you use a Mediator you'll be decreasing your Out-Degree (Fan-Out) by a factor of 2.
Benefits:
You're not coupling your module directly to your server ($http).
You're not coupling your module to an additional service (State).
Everything you need for state-persistence is right there in your controller ($scope / $scope.$on, $emit, $broadcast).
Your Mediator knows more and can direct the application more efficiently.
Downside(?):
Your modules need to fire interesting events ($scope.$emit('list://added/Item', $scope.list.id, item))
mediator.js
angular.module('lists.mediator', ['lists', 'list', 'item']).run(function mediate($rootScope){
var lists = [];
$rootScope.lists = lists;
$rootScope.$watch('lists', yourWatcher, true);
function itemModuleOrControllerStartedHandler(e, itemId, disclose){
if(!lists.length){
$http.get(...).success(function(data){
lists.push.apply(lists, data);
var item = getItem(lists, itemId);
disclose(item); // do not copy object otherwise you'll have to manage changes to stay synchronized
});
} else {
var item = getItem(lists, itemId);
disclose(item);
}
}
$rootScope.$on('item://started', itemModuleOrControllerStartedHandler);
});
// angular.bootstrap(el, ['lists.mediator'])
item-controller.js
var ItemController = function ItemController($scope, $routeParams){
var disclosure = $scope.$emit.bind($scope, 'item://received/data', (+new Date()));
$scope.itemId = $routeParams.id;
$scope.item = { id: -1, name: 'Nameless :(', quantity: 0 };
function itemDataHandler(e, timestamp, item){
$scope.item = item;
}
$scope.$on('item://received/data', itemDataHandler);
$scope.$emit('item://started', $scope.id, disclosure);
};

Angularjs service to manage dataset across multiple controllers

Basically the core of my app centers around a set of data retrieved from the server via a $http request. Once the data is available to the client (as an array of objects) I require it for multiple views and would like to maintain it's state between them, for example, if it has been filtered I would like only the filtered data to be available in the other views.
Currently I have a basic service retrieving the data and am then managing the state of the data (array) in an app-wide controller (see below). This works Ok but it is beginning to become a mess as I try to maintain the array length, filtered status, visible / hidden objects across controllers for each view as I have to keep a track of currentVenue etc in the app-wide controller. Note: I am using ng-repeat in each view to show and filter the data (another reason I would like to just have it filtered in a central spot).
Obviously this is not optimal. I assume I should be using a service to maintain the array of venue objects, so it would contain the current venue, current page, be responsible for filtering the array etc. and just inject it into each controller. My question is, how can I set up a service to have this functionality (including loading the data from the server on start; this would be a good start tbh) such that I can achieve this an then bind the results to the scope. ie: something $scope.venues = venues.getVenues and $scope.current = venues.currentVenue in each views controller.
services.factory('venues', function ($http, $q) {
var getVenues = function() {
var delay = $q.defer();
$http.get('/api/venues', {
cache: true
}).success(function (venues) {
delay.resolve(venues);
});
return delay.promise;
}
return {
getVenues: getVenues
}
});
controllers.controller('AppCtrl', function (venues, $scope) {
$scope.venuesPerPage = 3;
venues.getVenues().then(function (venues) {
$scope.venues = venues;
$scope.numVenues = $scope.venues.length;
$scope.currentPage = 0;
$scope.currentVenue = 0;
$scope.numPages = Math.ceil($scope.numVenues / $scope.venuesPerPage) - 1;
}
});
Sorry for the long wording, not sure how to specify it exactly. Thanks in advance.
The tactic is to take advantage of object references. If you move your shared data to an object, then set that object to $scope, any change on $scope is directly changing the service object since they are the same thing ($scope is referencing the service).
Here's a live sample demonstrating this technique (click).
<div ng-controller="controller-one">
<h3>Controller One</h3>
<input type="text" ng-model="serv.foo">
<input type="text" ng-model="serv.bar">
</div>
<div ng-controller="controller-two">
<h3>Controller Two</h3>
<input type="text" ng-model="serv.foo">
<input type="text" ng-model="serv.bar">
</div>
js:
var app = angular.module('myApp', []);
app.factory('myService', function() {
var myService = {
foo: 'abc',
bar: '123'
};
return myService;
});
app.controller('controller-one', function($scope, myService) {
$scope.serv = myService;
});
app.controller('controller-two', function($scope, myService) {
$scope.serv = myService;
});
I threw this together quickly as a starting point. You can restructure factory any way you want. The general idea is all data in scope has now been moved to an object in factory service.
Instead of resolving the $http with just the response array, resolve it with a much bigger object that includes the array from server. Since all data is now in an object it can be updated from any controller
services.factory('venues', function ($http, $q) {
var getVenues = function(callback) {
var delay = $q.defer();
$http.get('/api/venues', {
cache: true
}).then(function (response) {
/* update data object*/
venueData.venues=response.data;
venueData.processVenueData();
/* resolve with data object*/
delay.resolve(venueData);
}).then(callback);
return delay.promise;
}
var processVenueData=function(){
/* do some data manipulation here*/
venueData.updateNumPages();
}
var venueData={
venuesPerPage : 3,
numVenues:null,
currentVenue:0,
numPages:null,
venues:[],
updateNumPages:function(){
venueData.numPages = Math.ceil(venueData.numVenues / venueData.venuesPerPage) - 1;
},
/* create some common methods used by all controllers*/
addVenue: function( newVenue){
venueData.venues.push( newVenue)
}
}
return {
getVenues: getVenues
}
});
controllers.controller('AppCtrl', function (venues, $scope) {
venues.getVenues(function (venueData) {
/* now have much bigger object instead of multiple variables in each controller*/
$scope.venueData=venueData;
})
});
Now in markup reference venueData.venues or venueData.numPages
By sharing methods across controllers you can now simply bind a form object with ng-model's to a button that has ng-click="venueData.addVenue( formModel)" (or use ng-submit) and you can add a new venue from any controller/directive without adding a bit of code to the controller

AngularJS: Handling Model data injection in Controller

I have two controllers, to add Item and to delete Item, and a Model to show all items.
This model is injected into the controller ( on working on same template).
Whenever an item is added, I broadcast a message, which is listened by Model and it reloads the data from server.
Code:
ItemModule.factory('ItemListModal', function ($resource, $rootScope){
var allItem = $resource('item/page/:pageId.json', {'pageId': pageId });
var items = allItem.query();
$rootScope.$on('ItemAdded',function(){
items = allItem.query();
});
return items;
});
//Item is another Model, used to send data on server.
function CreateItemCtrl($scope, $rootScope, Item) {
$scope.save = function() {
Item.save($scope.item, function(data) {
$scope.result = data;
$rootScope.$broadcast('ItemAdded');
}, function(data) {
$scope.result = data.data;
});
}
}
function ListItemCtrl($scope, ItemListModal) {
$scope.allItems = ItemListModal;
}
Issue: Now since the dependency on ListItemCtrl is already resolved when template was first loaded, on adding Item it only changes the Model, but this is not re-injected into the ListItemCtrl. And due to this, the list on template do not change.
Is there any way to tell AngularJS to re-resolve the controller's dependency?
I really don't want to listen for event in Controllers and re-query data there, as there are other controllers which also needs same data from server.
Add another level of indirection on what you return from your service.
ItemModule.factory('ItemListModal', function ($resource, $rootScope){
var allItem = $resource('item/page/:pageId.json', {'pageId': pageId });
var data = {items:allItem.query()};
$rootScope.$on('ItemAdded',function(){
data.items = allItem.query();
});
return data;
});
function ListItemCtrl($scope, ItemListModal) {
$scope.allItems = ItemListModal;
// use as $scope.allItems.items wherever you need it. It will update when changes occur.
}
But it might be better to have a canonical representation of the item list on the client, and work to keep that current when you add things (just saving it to the server quietly).
The issue seems to be that while item is getting updated (have you tried console.log in the $on?) it's not an object and so hasn't been passed by reference. If you switch around your service to this:
ItemModule.factory('ItemListModal', function ($resource, $rootScope){
var ItemListModalScope = this;
var allItem = $resource('item/page/:pageId.json', {'pageId': pageId });
ItemListModalScope.items = allItem.query();
$rootScope.$on('ItemAdded',function(){
ItemListModalScope.items = allItem.query();
});
return ItemListModalScope;
});
And then wherever you use your allItems in your dome, you would do
{{ allItems.items }}

Resources