multistep form and sharing data with angular-ui-router - angularjs

I am trying to create a multistep form and am having trouble sharing data between views/states. I'm not sure that the way I'm even going about this is accurate as I am new to angular-ui-router. Here are my routes:
.state('tab.newEventCategory', {
url: '/activities/new-event-category',
views: {
'tab-patient': {
templateUrl: 'templates/new-event-category.html',
controller: 'ActivityDashboardCtrl'
}
}
})
.state('tab.newEventSubCategory', {
url: '/activities/new-event-sub-category',
views: {
'tab-patient': {
templateUrl: 'templates/new-event-sub-category.html'
}
}
})
I am trying to use the routes above so that once someone chooses a category, they then go to a page where they choose a subcategory. Here is the new-event-category page:
<div ng-repeat="event_category in event_categories" class="padding">
<a class="button button-block button-positive button-large" ng-click="moveToEventSubCategory(event_category)">
{{event_category}}
</a>
</div>
and here is the controller for the page:
.controller('ActivityDashboardCtrl', function($scope, $stateParams, EventCategory, $state) {
$scope.formData = {};
$scope.event_categories = EventCategory.query();
$scope.moveToEventSubCategory = function(event_category){
$scope.formData.category = event_category;
$state.go('tab.newEventSubCategory');
}
})
My issue is that now I want the newEventSubCategory state to have access to the same formData object so it can add subcategories. There will be more pages to this multistep form after this and I want them to all have access to the same formData variable. How do I do this?

If you want to communicate between controllers/states you best option it what #ronnie mentioned; creating a service or factory.
Have a look at this if you want to work out what services are and how to user them: AngularJS Step-by-Step: Services

Related

Master detail in AngularJS

I try to write a small app that get a list from REST API and displays details for each selected item. I was looking for a simple tutorial on how to transfer the selected id from the list view to the detail view, but couldn't figure out. I assume I need to use the Master-detail technique.
Here's the code:
The list view:
<ul>
<li ng-repeat="gadgetDetails in gadgettDetails">
<div>
<p>{{gadgetDetails.type}}</p>
<p>{{gadgetDetails.manufacturer}}</p>
</div>
<hr />
</li>
</ul>
And the detail view
<div>
<p>Go back to Gadgets list</p>
<p>{{gadgetDetails[2].type}}</p>
<p>{{gadgetDetails[2].color}}</p>
<p>{{gadgetDetails[2].manufcturer}}</p>
</div>
So just for the question I've put 2 (the second object in the JSON list that I get from the REST API , but it actually should be gadgetDetails[i].type
but I couldn't figure out how to get the index (for example i) for each item in the list
This the controller:
GadgetListCtrl.controller('gdgtList', function($scope, $http, $filter) {
$http.get('https:.../gadgets.json').
then(function(response) {
$scope.gadgettDetails = response.data;
});
});
So the question is how to display for each item its details by the id.
Thanks
you can achive this by angular ui router params
below is dummy code
in .config block
$stateProvider
.state('gadgets', {
url: "/gadgets/:id",
templateUrl: 'gadgetDetail.html',
controller: 'gadgetController',
controllerAs: 'vm',
params: {
id: null
}
});
and in controller file, you can access the id as below
.controller('GadgetController', function($state, $stateParams) {
var selectedID = $state.params.id;
//alternatively you can get using $stateParams service
// $stateParams.id
if(selectedID) {
// get detail of gadget
}
});

How to run controller each time angularjs ui-router named view is activated?

In regular angularjs ui-router state the controller runs each time the state is activated. I found this not to be the case when dealing with named views in an ionic application. The named view controller runs only once per element(game). Eg. user displays a game -> controller runs. Then user goes to another state and upon their return to displaying the same game controller does not run again - the view uses an already generated scope from before. I would like reinitialise that games scope with controller.
My code:
//app.js
.state('app.game', {
url: "/x/{gameID:int}",
views: {
'menuContent': {
templateUrl: "templates/html/my-game.html",
controller: 'GameCtrl'
}
}
})
...
//controllers.js
controllers.controller('GameCtrl', function ($scope, $stateParams, Game) {
//alert("running controller");
Game.get({id: $stateParams.gameID}, function(res){
$scope.game = res;
});
})
Edit: I actually use this code to go to the view of game (currently playing with guid idea from Steven Wexler)
<ion-view view-title="Games">
<ion-content>
<h1>Games</h1>
<ion-list>
<ion-item nav-clear menu-close href="#/app/x/{{game.id}}?guid= {{guid()}}" ng-repeat="game in games">
{{game.id}}
</ion-item>
</ion-list>
</ion-content>
</ion-view>
I Think your problem is already solved in this post Angular / Ionic - controller only runs once
brandyshea says that you need to add the code you want to run in the $ionicView.enter event
controller : function( $scope, $cordovaSQLite ){
$scope.$on('$ionicView.enter', function() {
// code to run each time view is entered
});
...
});
You probably are navigating to a state with the same url and params that was previously loaded. One way to force a reload is to pass in a guid as a parameter with your state and set reloadOnSearch=true (it's true by default). The second way to force a reload is to use the reload option added in version 0.2.5.
Option 1
.state('app.game', {
url: "/x/{gameID:int}/{guid}",
views: {
'menuContent': {
templateUrl: "templates/html/my-game.html",
controller: 'GameCtrl'
}
}
})
$state.go("app.game", { gameId: id, guid: guid });
function guid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
Option 2 (if you have version >= 0.2.5)
.state('app.game', {
url: "/x/{gameID:int}",
views: {
'menuContent': {
templateUrl: "templates/html/my-game.html",
controller: 'GameCtrl'
}
},
reload: true
})
Note: Awesome guid function copied from Create GUID / UUID in JavaScript?

Binding to data when changing the URL

I am building an application that uses ui-router. On my /teams route I have a form where a user can add a team name which then pushes the name to my MongoDB and binds to the view and displays the team name with several options, one of those being a button that links to a page where more specific information can be added for the team. I have the routing working on this and the url appears like /#/teams/oklahoma or /#/teams/washington for example. Here is what my routing looks like:
app.config(
['$stateProvider','$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
.state('teams', {
url: '/teams',
templateUrl: './templates/main/league.html',
controller: 'leagueCtrl'
})
.state('teams/:title', {
url: '/teams/:title',
templateUrl: './templates/main/team.html',
controller: 'teamCtrl'
})
Here is my link to the /teams/:title route:
<a href="#subjects/{{ team.title | lowercase }}">
<button ng-click="viewTeam(team)" class="team-button">View</button>
</a>
Currently I do not have anything in my viewTeam() function. My question is how do I bind to my {{ team.title }} and other related information in the new view with the new URL? I know a factory must be somehow involved and I have tried implementing the solution described at http://onehungrymind.com/angularjs-communicating-between-controllers/ without success. Any additional guidance would be very much appreciated.
The URL should probably contain the team ID. I'm going to assume your 'teams' array is loaded using $http from some backend API.
app.config(
['$stateProvider','$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
.state('teams', {
url: '/teams',
// the `teams` state will wait to load until $http.get() call is completed.
// this resolve declaration will provide the resulting teams array as an injectable 'teams' object.
resolve: { teams: function($http) { return $http.get("/teams.json"); },
templateUrl: './templates/main/league.html',
// This simplified controller injects the 'teams' resolve and simply sticks the list of teams on the scope to ng-repeat etc.
controller: function($scope, teams) { $scope.teams = teams; }
})
// This is a nested state, called `teams.team`. It requires a `teamId` parameter.
// The parameter can be provided by a URL: http://localhost/app#/teams/9237594827
// The parameter can be provided by $state.go: $state.go('teams.team', { teamId: 9237594827 });
// The parameter can be provided by uiSref: <a ui-sref="teams.team({ teamId: repeatedTeam.id })">{{repeatedTeam.title}}</a>
.state('teams.team', {
// `teams.team` state declares a `teamId` state parameter in the URL
url: '/teams/:teamId',
// This resolve function uses $stateParams to locate the correct team from the list.
// `team` will be made available for injection
resolve: {
team: function($stateParams, teams) {
// Find the correct team from the `teams` array, by ID.
return teams.filter(function(team) { return team.id === $stateParams.teamId; })[0];
}
},
templateUrl: './templates/main/team.html',
// Inject the `team` resolve and put it on $scope
controller: function($scope, team) { $scope.team = team; }
})
league.html:
<ul>
<li ng-repeat="team in teams">
<a ui-sref="teams.team({ teamId: team.id })">{{team.title}}</a>
</li>
</ul>

use angular-ui-router with bootstrap $modal to create a multi-step wizard

The FAQ for ui-router has a section about integration with bootstrap $modals, but it doesn't mention anything about abstract views. I have 3 views under a single abstract view, so something like the following.
$stateProvider
.state('setup', {
url: '/setup',
templateUrl: 'initialSetup.html',
controller: 'InitialSetupCtrl',
'abstract': true
})
// markup for the static view is
<div class="wizard">
<div ui-view></div>
</div>
.state('setup.stepOne', {
url: '/stepOne',
controller: 'SetupStepOneCtrl',
onEnter: function($stateParams, $state, $modal) {
$modal.open{
backdrop: 'static',
templateUrl: 'setup.stepOne.html',
controller: 'SetupStepOneCtrl'
})
}
})
.state('setup.stepTwo', {
url: '/stepTwo',
controller: 'SetupStepTwoCtrl',
onEnter: function($stateParams, $state, $modal) {
$modal.open({
backdrop: 'static',
templateUrl: 'setup.stepTwo.html',
controller: 'SetupStepTwoCtrl'
})
}
})
.state('setup.stepThree', {
url: '/stepThree',
templateUrl: 'setup.stepThree.html',
controller: 'SetupStepThreeCtrl'
...
});
}]);
I've also tried to only add the onEnter block to the abstract state, and removed onEnter from each of the 3 child states. This actually seems to me like the right approach. The abstract state initializes and opens the $modal and the subsequent states should interpolate into , but when I tried this the ui-view container was empty.
I can think of some other hacky ways to workaround this but thought I'd ask to see if there's a canonical way of handling this.
Alternative way is to use ng-switch with ng-include combination inside $modal controller to dynamically load wizard step templates, that is if you don't mind sharing the same controller for all wizard steps:
<div ng-switch="currentStep.number">
<div ng-switch-when="1">
<ng-include src="'wizardModalStep1.html'"></ng-include>
</div>
<div ng-switch-when="2">
<ng-include src="'wizardModalStep2.html'"></ng-include>
</div>
<div ng-switch-when="3">
<ng-include src="'wizardModalStep3.html'"></ng-include>
</div>
</div>
Here is Plunker with working example: http://plnkr.co/edit/Og2U2fZSc3VECtPdnhS1?p=preview
Hope that helps someone !!
I used following approach to develop a wizard. this might be help for you.
I used states like below sample with parent property.
var home = {
name: 'home',
url: '/home',
controller: 'MainController',
templateUrl: '/html/main.html'
},
sampleWizard = {
name: 'sampleWizard',
url: '/sampleWizard',
controller: 'sampleWizardController',
templateUrl: '/html/sd/sample/sampleWizard.html'
},
sampleSectionOne = {
name: 'sampleSectionOne',
url: '/sampleSectionOne',
parent: sampleWizard,
controller: 'sampleSectionOneController',
templateUrl: '/html/sd/sample/sampleSectionOne.html'
},
sampleSectionTwo = {
name: 'sampleSectionTwo',
url: '/sampleSectionTwo',
parent: sampleWizard,
controller: 'sampleSectionTwoController',
templateUrl: '/html/sd/sample/sampleSectionTwo.html'
};
$stateProvider.state(home);
$stateProvider.state(sampleWizard);
$stateProvider.state(sampleSectionOne);
$stateProvider.state(sampleSectionTwo);
I'm not sure you want to fire the modal every single time you go to the next step.
I think all you have to do is create a modal view () then each step has a modal a templateUrl assigned to it.
each template should look like:
<div class="modal fade in" id="whatever" style="display:block">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Modal title</h4>
</div>
<div class="modal-body">
<p>One fine body…</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<a ui-sref="next_page_route_id" type="button" class="btn btn-primary">Next</a>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<div class="modal-backdrop fade in"></div>
On the last screen you can add a data-dismiss="modal" to the submit and you are done
I have dealt with similar scenario, where I had to create a wizard (which allows you to go through steps and finally hit on summary and save).
For this, I had created different views but with one controller.
Since controller scope dies after each re-routing, I had to save the scope of controller(Basically the model object associated with the wizard) in a service object for each routing in the wizard (Captured through location.path()) and load this object back from service object on load of controller.Something like below:-
// Saving data on routing
$scope.nextPage = function () {
service.ModelForWizard = $scope.ModelForWizard;
switch ($location.path()) {
case RouteOfPage1:
//Do some stuff
break;
case RouteOfPage2:
//Do some stuff
break;
default:
}
Service or factory is persisted throughout life time of user session and is ideal to hold user data.
One more thing which was helpful , was use of 'Resolve' in the routes.
It ensures that next page in not rendered until the required data(generally lookup data) is not loaded. Code of resolve is something like this:
.when('/RouteForWizardPage1', {
templateUrl: '/templates/ViewPage1Wizard.html',
caseInsensitiveMatch: true,
controller: 'wizardController',
resolve: {
wizardLookupDataPage1: function (Service) {
return service.getwizardModelLookupDataPage1().$promise;
}
},
})
I was running into the same thing.. What worked for me was emptying the url property of the first sub state. Hence for the first sub state, your code should look as follows:
.state('setup.stepOne', {
url: '',
controller: 'SetupStepOneCtrl',
onEnter: function($stateParams, $state, $modal) {
$modal.open{
backdrop: 'static',
templateUrl: 'setup.stepOne.html',
controller: 'SetupStepOneCtrl'
})
}
})
Also, incase you're not using the url property to call the other 3 sub states, and are calling them using the state name only, you don't necessarily need to mention a url property for them.
If you want to show a wizard in a modal dialog and have a separate state for each of the wizard's steps, you need to keep in mind that the modal dialog is rendered completely outside your view hierarchy. Therefore, you cannot expect any interaction between ui-router's view rendering mechanisms and the dialog contents.
A robust solution is to put the wizard contents manipulation logic onto the parent state scope
$scope.wizard = {
scope: $scope.$new(),
show: function (template) {
// open the $modal if not open yet
// purge scope using angular.copy()
// return $scope.wizard.scope
},
// private
open: function () {
$modal.open({
scope: $scope.wizard.scope,
// ...
});
}
};
and then manually show the appropriate content from each of the sub-states and manipulate the wizard's scope as needed
$scope.wizard.show('someTemplate');
$scope.wizard.scope.user = ...;
When we faced this problem in our project, we decided after some discussion, that we didn't actually need separate ui-router states for the wizard steps. This allowed us to create a wizard directive used inside the dialog template to read wizard configuration from scope (using a format similar to ui-router state definition), provide methods to advance the wizard, and render appropriate view/controller inside the dialog.
To create multi-step wizards, you can use this module (I am the author): https://github.com/troch/angular-multi-step-form.
It allows you to create steps like views, and you can enable navigation (back / forward button, url with an URL search parameter). Examples are available here.

Conditional partials in Angular.js

Angular novice here. I'm trying to wrap my head around the right way to accomplish a basic template issue.
I have a header, which should read “click here to log in” when the user is not logged in, and “Welcome, Dudefellah” (and associated Settings links and whatnot) when a user is logged in.
I've written a Service that is able to return a JSON bundle including a login state and username, but I don't know what “The Angular Way” to express: “if(auth.loggedin), output partials/header.html; else output partials/header_login.html”.
I’m unclear as to whether this logic would belong in the controller, or some kind of “auth” model, or even in the view (that can't be right, right?). Any assistance would be greatly appreciated.
Within the controller once the login state is fetched create a scope variable headerTemplate and assign the name of the template depending on the login state
function MyCtrl($scope, loginService) {
$scope.auth = loginService.getLoginState();
$scope.headerTemplate = $scope.auth ? 'partials/header.html' : 'partials/header_login.html';
}
In in your markup
<div ng-include src="headerTemplate"></div>
There's a sample angular application called angular-app that does this really well. They have a security service, then a toolbar partial and directive that shows things depending on the state.
https://github.com/angular-app/angular-app/tree/master/client/src/common/security
from angular-app:
src/common/security/login/toolbar.tpl.html:
<ul class="nav pull-right">
<li class="divider-vertical"></li>
<li ng-show="isAuthenticated()">
{{currentUser.firstName}} {{currentUser.lastName}}
</li>
<li ng-show="isAuthenticated()" class="logout">
<form class="navbar-form">
<button class="btn logout" ng-click="logout()">Log out</button>
</form>
</li>
<li ng-hide="isAuthenticated()" class="login">
<form class="navbar-form">
<button class="btn login" ng-click="login()">Log in</button>
</form>
</li>
</ul>
src/common/security/login/toolbar.js:
angular.module('security.login.toolbar', [])
// The loginToolbar directive is a reusable widget that can show login or logout buttons
// and information the current authenticated user
.directive('loginToolbar', ['security', function(security) {
var directive = {
templateUrl: 'security/login/toolbar.tpl.html',
restrict: 'E',
replace: true,
scope: true,
link: function($scope, $element, $attrs, $controller) {
$scope.isAuthenticated = security.isAuthenticated;
$scope.login = security.showLogin;
$scope.logout = security.logout;
$scope.$watch(function() {
return security.currentUser;
}, function(currentUser) {
$scope.currentUser = currentUser;
});
}
};
return directive;
}]);
You could also use ui-router which does wonders for conditional routing and for good infrastructure in general. You'll need to define two states:
myapp.config(function($stateProvider, $urlRouterProvider){
...
// Now set up the states
$stateProvider
.state('login', {
parent: account,
url: "/login",
templateUrl: "partials/header_login.html"
})
.state('auth', {
parent: account,
url: "/authorized",
templateUrl: "partials/header.html"
})
})
when you are back from your query, change state by $state.transitionTo('login') or ('auth') and the router will load the right template for you (and also the URL). in general its much better to use a good router as the basis of your app and not give ad-hoc solutions per each case. you could also read a page (I wrote) about it here

Resources