In a previous post, I wrote about using generator-ionic
to create the scaffolding for a mobile-optimised Administration UI for the Vardyger publishing platform. In this post, we'll look at how the Admin UI uses the Angular UI router for handling routes, defining states and sharing data between views.
Note: I've updated Vardyger's project structure to include directories for controllers
, directives
and services
:
├── /app
└── /scripts
└── /controllers
├── editor-controller.js
├── main-controller.js
├── preview-controller.js
├── side-menu-controller.js
└── /directives
└── /services
├── posts-service.js
├── app.js
└── /styles
├── main.scss
└── /templates
├── editor-template.html
├── main-template.html
├── preview-template.html
├── side-menu-template.html
├── index.html
...
The Angular UI router
The Ionic framework uses the angular-ui-router module for handling routes and defining states (including nested states and nested views).
Let's start by taking a look at the the Admin UI's app.js
where the ui-router's $stateProvider
and $urlRouterProvider
are used to set up the application’s states and routing logic:
First, we define the root state:
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: '/app',
templateUrl: 'templates/side-menu-template.html',
controller: 'SideMenuController'
abstract: true,
})
By providing a name ('app'
) and populating a configuration object with keys & values for the state's url, templateUrl and controller name.
We have also defined this state as abstract (abstract: true
), which means the side-menu can only be displayed when one of its nested states (e.g., app.main
, app.preview
or app.editor
) is active:
Next, we define a nested state:
.state('app.main', {
url: '/main',
views: {
'menuContent': {
templateUrl: 'templates/main-template.html',
controller: 'MainController',
resolve: {
posts: function(PostsService) {
return PostsService.findPosts();
}
}
}
}
})
The app.main
state defines a named view ('menuContent'
) that is referenced in the root state's template (templates/side-menu-template.html
):
<ion-side-menus enable-menu-with-back-views="false">
<ion-side-menu-content>
<ion-nav-bar class="bar-dark">
<ion-nav-back-button>
</ion-nav-back-button>
<!-- ion-nav-buttons must be immediate descendants of the ion-view
or ion-nav-bar element (basically, don't wrap it in a div). -->
<ion-nav-buttons side="primary">
<button class="button button-icon button-clear ion-navicon"
menu-toggle="primary"></button>
</ion-nav-buttons>
</ion-nav-bar>
<ion-nav-view name="menuContent"></ion-nav-view>
</ion-side-menu-content>
<ion-side-menu side="left">
...
</ion-side-menu>
</ion-side-menus>
The Angular UI router will load the named view's template (templates/main-template.html
) into the <ion-nav-view> directive with the name menuContent
.
The app.main
state also uses resolve and the PostsService
to pass data to the MainController
(scripts/controllers/main-controller.js):
angular.module('vardyger')
.controller('MainController',
function(
$scope, // inject the $scope service
posts // inject the resolved posts data
) {
$scope.listItems = posts;
});
The MainController
inject's the $scope
service and the resolved posts
data, the posts
data is assigned to the $scope.listItems
object.
Note: Values on $scope
are called models and are available in views.
The app.main
state's template (templates/main-template.html
):
<ion-view title="Content">
...
<ion-content class="has-header has-subheader">
<ion-list>
<ion-item
ng-repeat="listItem in listItems"
ui-sref='app.preview({postId: listItem.post.id})'>
{{listItem.post.title}}
...
</ion-item>
</ion-list>
</ion-content>
</ion-view>
Uses the ng-repeat directive to iterate over the listItems
model in order to display a list of posts:
The ui-sref directive specifies the state (app.preview
) we want to transition to:
.state('app.preview', {
url: /preview/{postId}',
views: {
'menuContent': {
templateUrl: 'templates/preview-template.html',
controller: 'PreviewController',
resolve: {
post: function($stateParams, PostsService) {
return PostsService.findPostById($stateParams.postId);
}
}
}
}
})
Most states in your application will have a url
associated with them and URLs often have dynamic parts called parameters. You can send parameters to a state, using either:
<a ui-sref="app.editor({postId: item.post.id})">Go!</a>
Or:
$state.go('app.editor', {postId: item.post.id});
You can learn more about URL parameters in the Angular UI router's wiki and in the $stateProvider documentation.
The app.preview
state, like the app.main
state defines a named view ('menuContent'
) that is referenced in the root state's template (templates/side-menu-template.html
). And, like the app.main
state the Angular UI router will load the app.preview
state's view template (templates/preview-template.html
) into the <ion-nav-view name="menuContent"> directive.
The app.preview
state also uses resolve, the $stateParams
service and the PostsService
to pass data to the PreviewController
(scripts/controllers/preview-controller.js):
angular.module('vardyger')
.controller('PreviewController',
function(
$scope, // inject the $scope service
post // inject the resolved post data
) {
$scope.item = posts;
});
The PreviewController
inject's the $scope
service and the resolved post
data, the post
data is assigned to the item
model.
Now, let's take a look at the PostsService's findPostById()
:
this.findPostById = function(id) {
var deferred = $q.defer();
model.forEach(function(item) {
if (item.post.id === id) {
deferred.resolve(item);
}
});
return deferred.promise;
};
findPostById()
returns a promise that will be resolved and converted into a value before the PreviewController
is instantiated and the $stateChangeSuccess event is fired.
The app.preview
state's template (templates/preview-template.html
):
<ion-view title="Preview">
<ion-nav-buttons side="right">
<button class="button button-outline"
ui-sref="app.editor({postId: item.post.id})">EDIT</button>
</ion-nav-buttons>
<ion-content class="has-header">
<div class="wrapper">
<h1 class="padding-top">{{item.post.title}}</h1>
<div ng-bind-html="item.post.html"></div>
</div>
</ion-content>
</ion-view>
Uses the ng-bind-html directive to display the post's content:
And, the ui-sref directive to transition to:
The Admin UI's Markdown editor.
References:
- GitHub: ui-router
- ui-router: Nested States and Nested Views
- AngularJS API $sce: Strict Contextual Escaping
- The Ionic Framework: Sharing Data Between Views
- GitHub: The Vardyger publishing platform