Menu Module
1
2
3
4
lib/
├── menu/
│ └── menu.module.js
│ └── menu.html
The Menu module is very simple. Its purpose is to add a menu inside <ion-side-menu>
. Without this module, the side menu would be blank. The menu module declares only a config file, it has ionic
and ui.router
as dependencies.
01
02
03
04
05
06
07
08
09
10
import
modConfig from
'./menu.config'
;
let
mod = angular.module(
'prototype.menu'
, [
'ionic'
,
'ui.router'
]);
mod.config(modConfig);
export
default
mod = mod.name;
The most interesting part is the configuration. We do not want to create a state for the Menu module as it is available everywhere. Instead, we decorate the root
state with the menu content. With the ui-view="menu"
being defined in the root
state, we need to use menu@root
to refer to it.
01
02
03
04
05
06
07
08
09
10
11
12
export
default
function
($stateProvider) {
'ngInject'
;
$stateProvider.decorator(
'views'
, (state, parent) => {
let
views = parent(state);
if
(state.name ===
'root'
) {
views[
'menu@root'
] = {
template: require(
"./menu.html"
)
};
}
return
views;
});
}
1
2
3
4
| lib/ ├── menu/ │ └── menu.module.js │ └── menu.html |
<ion-side-menu>
. Without this module, the side menu would be blank. The menu module declares only a config file, it has ionic
and ui.router
as dependencies.
01
02
03
04
05
06
07
08
09
10
| import modConfig from './menu.config' ; let mod = angular.module( 'prototype.menu' , [ 'ionic' , 'ui.router' ]); mod.config(modConfig); export default mod = mod.name; |
root
state with the menu content. With the ui-view="menu"
being defined in the root
state, we need to use menu@root
to refer to it.
01
02
03
04
05
06
07
08
09
10
11
12
| export default function ($stateProvider) { 'ngInject' ; $stateProvider.decorator( 'views' , (state, parent) => { let views = parent(state); if (state.name === 'root' ) { views[ 'menu@root' ] = { template: require( "./menu.html" ) }; } return views; }); } |
Home Module
1
2
3
4
5
6
lib/
├── home/
│ └── home.module.js
│ └── home.config.js
│ └── home.controller.js
│ └── home.html
1
2
3
4
5
6
| lib/ ├── home/ │ └── home.module.js │ └── home.config.js │ └── home.controller.js │ └── home.html |
home.module.js
The Home module displays the latests posts of your WordPress website. It has a config file, a controller, and it depends on the following libraries:
ionic
ui.router
wp-api-angularjs
01
02
03
04
05
06
07
08
09
10
11
12
13
import
modConfig from
'./home.config'
;
import
modController from
'./home.controller'
;
let
mod = angular.module(
'prototype.home'
, [
'ionic'
,
'ui.router'
,
'wp-api-angularjs'
]);
mod.config(modConfig);
mod.controller(
'HomeController'
, modController);
export
default
mod = mod.name
ionic
ui.router
wp-api-angularjs
01
02
03
04
05
06
07
08
09
10
11
12
13
| import modConfig from './home.config' ; import modController from './home.controller' ; let mod = angular.module( 'prototype.home' , [ 'ionic' , 'ui.router' , 'wp-api-angularjs' ]); mod.config(modConfig); mod.controller( 'HomeController' , modController); export default mod = mod.name |
home.config.js
The config adds a new state, root.home
, with the /home
URL that has a template and a controller (both living within the module).
01
02
03
04
05
06
07
08
09
10
11
12
export
default
function
($stateProvider) {
'ngInject'
;
$stateProvider.state(
'root.home'
, {
url:
"/home"
,
views: {
'content@root'
: {
template: require(
"./home.html"
),
controller:
"HomeController as homeCtrl"
}
}
});
}
root.home
, with the /home
URL that has a template and a controller (both living within the module).
01
02
03
04
05
06
07
08
09
10
11
12
| export default function ($stateProvider) { 'ngInject' ; $stateProvider.state( 'root.home' , { url: "/home" , views: { 'content@root' : { template: require( "./home.html" ), controller: "HomeController as homeCtrl" } } }); } |
home.controller.js
This is a simplified version of the Home controller logic. It contains two functions:
loadMore
: This function populates vm.posts
. It uses the $wpApiPosts
service, which is part of the wp-api-angularjs library.
refresh
: This function removes posts and calls loadMore
again.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
export
default
function
($scope, $log, $q, $wpApiPosts) {
'ngInject'
;
var
vm =
this
;
vm.posts = [];
vm.loadMore = loadMore;
vm.refresh = refresh;
function
refresh() {
vm.posts =
null
;
loadMore().finally(() => $scope.$broadcast(
'scroll.refreshComplete'
));
}
function
loadMore() {
return
$wpApiPosts.$getList().then((response) => {
vm.posts = (vm.posts) ? vm.posts.concat(response.data) : response.data;
$scope.$broadcast(
'scroll.infiniteScrollComplete'
);
});
}
}
loadMore
: This function populates vm.posts
. It uses the $wpApiPosts
service, which is part of the wp-api-angularjs library.refresh
: This function removes posts and calls loadMore
again.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
| export default function ($scope, $log, $q, $wpApiPosts) { 'ngInject' ; var vm = this ; vm.posts = []; vm.loadMore = loadMore; vm.refresh = refresh; function refresh() { vm.posts = null ; loadMore().finally(() => $scope.$broadcast( 'scroll.refreshComplete' )); } function loadMore() { return $wpApiPosts.$getList().then((response) => { vm.posts = (vm.posts) ? vm.posts.concat(response.data) : response.data; $scope.$broadcast( 'scroll.infiniteScrollComplete' ); }); } } |
home.html
The template has a ion-refresher
directive that allows users to reload the page by pulling the page down. It also has a ion-infinite-scroll
directive that calls the loadMore
function when reached. Posts are displayed using the ng-repeat
directive.
Tip: Use the track by
expression for better performance. It minimizes DOM manipulation when a post is updated.
01
02
03
04
05
06
07
08
09
10
<
ion-view
>
<
ion-nav-title
>Home</
ion-nav-title
>
<
ion-content
>
<
ion-refresher
pulling-text
=
"Pull to refresh"
on-refresh
=
"homeCtrl.refresh()"
></
ion-refresher
>
<
div
class
=
"list card"
ng-repeat
=
"post in homeCtrl.posts track by post.ID"
>
<!-- THE POST DETAILS -->
</
div
>
<
ion-infinite-scroll
immediate-check
=
"true"
on-infinite
=
"homeCtrl.loadMore()"
></
ion-infinite-scroll
>
</
ion-content
>
</
ion-view
>
ion-refresher
directive that allows users to reload the page by pulling the page down. It also has a ion-infinite-scroll
directive that calls the loadMore
function when reached. Posts are displayed using the ng-repeat
directive.track by
expression for better performance. It minimizes DOM manipulation when a post is updated.
01
02
03
04
05
06
07
08
09
10
| < ion-view > < ion-nav-title >Home</ ion-nav-title > < ion-content > < ion-refresher pulling-text = "Pull to refresh" on-refresh = "homeCtrl.refresh()" ></ ion-refresher > < div class = "list card" ng-repeat = "post in homeCtrl.posts track by post.ID" > <!-- THE POST DETAILS --> </ div > < ion-infinite-scroll immediate-check = "true" on-infinite = "homeCtrl.loadMore()" ></ ion-infinite-scroll > </ ion-content > </ ion-view > |
Post Module
1
2
3
4
5
6
lib/
├── post/
│ └── post.module.js
│ └── post.config.js
│ └── post.controller.js
│ └── post.html
The Post module displays only one post. It has a config file, a controller, and it depends on the same librairies as the Home module.
1
2
3
4
5
6
| lib/ ├── post/ │ └── post.module.js │ └── post.config.js │ └── post.controller.js │ └── post.html |
post.module.js
01
02
03
04
05
06
07
08
09
10
11
12
13
import
modConfig from
'./post.config'
;
import
modController from
'./post.controller'
;
let
mod = angular.module(
'prototype.post'
, [
'ionic'
,
'ui.router'
,
'wp-api-angularjs'
]);
mod.config(modConfig);
mod.controller(
'PostController'
, modController);
export
default
mod = mod.name
Similar to the Home module, the config adds a new state, root.post
, with the /post/:id
URL. It also registers a view and a controller.
01
02
03
04
05
06
07
08
09
10
11
12
13
| import modConfig from './post.config' ; import modController from './post.controller' ; let mod = angular.module( 'prototype.post' , [ 'ionic' , 'ui.router' , 'wp-api-angularjs' ]); mod.config(modConfig); mod.controller( 'PostController' , modController); export default mod = mod.name |
root.post
, with the /post/:id
URL. It also registers a view and a controller.post.config.js
01
02
03
04
05
06
07
08
09
10
11
12
export
default
function
($stateProvider) {
'ngInject'
;
$stateProvider.state(
'root.post'
, {
url:
"/post/:id"
,
views: {
'content@root'
: {
template: require(
"./post.html"
),
controller:
"PostController as postCtrl"
}
}
});
}
01
02
03
04
05
06
07
08
09
10
11
12
| export default function ($stateProvider) { 'ngInject' ; $stateProvider.state( 'root.post' , { url: "/post/:id" , views: { 'content@root' : { template: require( "./post.html" ), controller: "PostController as postCtrl" } } }); } |
post.controller.js
The controller retrieves the post specified in the url /post/:id
via the $stateParams
service (UI router service).
01
02
03
04
05
06
07
08
09
10
11
12
export
default
function
($scope, $log, $wpApiPosts, $stateParams) {
'ngInject'
;
var
vm =
this
;
vm.post =
null
;
$scope.$on(
'$ionicView.loaded'
, init);
function
init() {
$wpApiPosts.$get($stateParams.id).then((response) => {
vm.post = response.data;
});
}
}
/post/:id
via the $stateParams
service (UI router service).
01
02
03
04
05
06
07
08
09
10
11
12
| export default function ($scope, $log, $wpApiPosts, $stateParams) { 'ngInject' ; var vm = this ; vm.post = null ; $scope.$on( '$ionicView.loaded' , init); function init() { $wpApiPosts.$get($stateParams.id).then((response) => { vm.post = response.data; }); } } |
post.html
The template has a ion-spinner
directive that displays a loader while the data is being fetched from the WordPress REST API. When the post is loaded, we use an Ionic card to render the author avatar, the post title, and the post content.
Tip: Use the bindOnce
expression, ::
, (introduced in Angular 1.3) to avoid watching data that will not change over time.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<ion-view>
<ion-nav-title>{{postCtrl.post.title}}</ion-nav-title>
<ion-content>
<ion-spinner ng-
if
=
"!postCtrl.post"
></ion-spinner>
<div
class
=
"list card"
ng-
if
=
"postCtrl.post"
>
<div
class
=
"item item-avatar"
>
<img ng-src=
"{{::postCtrl.post.author.avatar}}"
>
<h2>{{::postCtrl.post.author.name}}</h2>
<p>{{::postCtrl.post.date | date:
'medium'
}}</p>
</div>
<div
class
=
"item item-body"
>
<img
class
=
"full-image"
ng-src=
"{{::postCtrl.post.featured_image.attachment_meta.sizes.medium.url}}"
>
<h2>{{::postCtrl.post.title}}</h2>
<p ng-bind-html=
"::postCtrl.post.content"
></p>
</div>
</div>
</ion-content>
</ion-view>
ion-spinner
directive that displays a loader while the data is being fetched from the WordPress REST API. When the post is loaded, we use an Ionic card to render the author avatar, the post title, and the post content.bindOnce
expression, ::
, (introduced in Angular 1.3) to avoid watching data that will not change over time.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
| <ion-view> <ion-nav-title>{{postCtrl.post.title}}</ion-nav-title> <ion-content> <ion-spinner ng- if = "!postCtrl.post" ></ion-spinner> <div class = "list card" ng- if = "postCtrl.post" > <div class = "item item-avatar" > <img ng-src= "{{::postCtrl.post.author.avatar}}" > <h2>{{::postCtrl.post.author.name}}</h2> <p>{{::postCtrl.post.date | date: 'medium' }}</p> </div> <div class = "item item-body" > <img class = "full-image" ng-src= "{{::postCtrl.post.featured_image.attachment_meta.sizes.medium.url}}" > <h2>{{::postCtrl.post.title}}</h2> <p ng-bind-html= "::postCtrl.post.content" ></p> </div> </div> </ion-content> </ion-view> |
Style (Sass)
1
2
3
4
lib/
├── scss/
│ └── _variables.scss
│ └── bootstrap.scss
The bootstrap.scss
file that we imported in our entry point is as simple as this:
1
2
@import
"./variables"
;
@import
"~ionic-sdk/scss/ionic"
;
First, we import our variables. We then import the Ionic styles. Importing our variables before Ionic allows us to overwrite whatever Sass variables Ionic has declared.
For example, if you want the positive color to be red instead of blue, you can overwrite it like this:
1
$positive:
red
!
default
;
1
2
3
4
| lib/ ├── scss/ │ └── _variables.scss │ └── bootstrap.scss |
bootstrap.scss
file that we imported in our entry point is as simple as this:
1
2
| @import "./variables" ; @import "~ionic-sdk/scss/ionic" ; |
1
| $positive: red ! default ; |
6. Android and iOS
Installation
Run the following commands inside the project folder and chose the platform you want to build for.
1
2
3
4
$
cp
config.dist.xml config.xml
$ npm run installCordova
Which platforms
do
you want to build? (android ios):
In addition to installing platforms within the /platforms
folder, the script will install one plugin. For the demo, we need the cordova-plugin-whitelist
plugin. It is necessary to allow the application to query the WordPress REST API we created earlier.
If you open config.xml, you will see that we allow access to any kind of origin (<access origin="*" />
). Of course, this is only for demo purposes. If you deploy your application to production, then make sure you restrict access like this:
1
1
2
3
4
| $ cp config.dist.xml config.xml $ npm run installCordova Which platforms do you want to build? (android ios): |
/platforms
folder, the script will install one plugin. For the demo, we need the cordova-plugin-whitelist
plugin. It is necessary to allow the application to query the WordPress REST API we created earlier.<access origin="*" />
). Of course, this is only for demo purposes. If you deploy your application to production, then make sure you restrict access like this:
1
|
Android
Prerequisites
- Android SDK
- Ant
Running the npm run runAndroid
command is a shortcut for rm -rf www/* && webpack && cordova run android
. This removes everything within the www folder, dumps a non-minified version of the app in it, and runs the android
command. If an Android device is connected (run adb devices
to make sure), the command will load the app on the device, otherwise it will use the Android emulator.
1
2
# Run Android
$ npm run runAndroid
npm run runAndroid
command is a shortcut for rm -rf www/* && webpack && cordova run android
. This removes everything within the www folder, dumps a non-minified version of the app in it, and runs the android
command. If an Android device is connected (run adb devices
to make sure), the command will load the app on the device, otherwise it will use the Android emulator.
1
2
| # Run Android $ npm run runAndroid |
iOS
Prerequisites
- OS X
- Xcode
If you do not have an Apple device, you should install the iOS Simulator. It's really good and better than the Android emulator.
1
$
sudo
npm
install
-g ios-sim
Running npm run runIosEmulator
is a shortcut for rm -rf www/* && webpack && cordova run ios
. The npm run runIosDevice
command is a shortcut for rm -rf www/* && webpack && cordova run ios --device
.
1
2
3
# Run iOS
$ npm run runIosEmulator
$ npm run runIosDevice
1
| $ sudo npm install -g ios-sim |
npm run runIosEmulator
is a shortcut for rm -rf www/* && webpack && cordova run ios
. The npm run runIosDevice
command is a shortcut for rm -rf www/* && webpack && cordova run ios --device
.
1
2
3
| # Run iOS $ npm run runIosEmulator $ npm run runIosDevice |
Conclusion
With this tutorial, I've tried to show you how easy it is to create a hybrid, mobile application for your WordPress website. You should now be able to:
- create loosely coupled modules that respect CommonJS specs
- import CommonJS modules with ECMAScript 6
- use the WordPress REST API client side (with wp-api-angularjs)
- leverage Ionic Framework to create a great user interface
- use webpack to bundle your application
- use Cordova to run the application on iOS and Android
If you want to go further, then take a look at a project I created few months ago,WordPress Hybrid Client.
WordPress Hybrid Client
WordPress Hybrid Client (WPHC) is an open-source project available on GitHub that helps you to create iOS and Android versions of your WordPress website for free. WPHC is based on the same technology stack that we used in this tutorial.
WPHC includes the following features:
- push notifications
- bookmarks (offline mode)
- Google Analytics support
- automatic content updates
- social buttons
- accessibility (post font size)
- multiple languages (English, French, Chinese)
- infinite scroll
- syntax highlighter for tech blogs
- image caching
- app rating
Sign up here with your email
ConversionConversion EmoticonEmoticon