Now that we've finished making our initial setup, let's dive into the code of our app.
First, we will need some helpers which will help us write some AngularJS
code using es6's class system. For this purpose we will use angular-ecmascript npm package. Let's install it:
$ npm install angular-ecmascript --save
angular-ecmascript
is a utility library which will help us write an AngularJS
app using es6's class system. In addition, angular-ecmascript
provides us with some very handy features, like auto-injection without using any pre-processors like ng-annotate, or setting our controller as the view model any time it is created (See referene). The API shouldn't be too complicated to understand, and we will get familiar with it as we make progress with this tutorial.
NOTE: As for now there is no best pratice for writing
AngularJS
es6 code, this is one method we recommend out of many possible other options.
Now that everything is set, let's create the RoutesConfig
using the Config
module-helper:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Config } from 'angular-ecmascript/module-helpers';
export default class RoutesConfig extends Config {
static $inject = ['$stateProvider']
configure() {
this.$stateProvider
.state('tab', {
url: '/tab',
abstract: true,
templateUrl: 'templates/tabs.html'
});
}
}
This will be our main app router which is implemented using angular-ui-router, and anytime we would like to add some new routes and configure them, this is where we do so.
After we define a helper, we shall always load it in the main app file. Let's do so:
2
3
4
5
6
7
8
9
10
12
13
14
15
16
17
18
19
20
import Ionic from 'ionic';
import Keyboard from 'cordova/keyboard';
import StatusBar from 'cordova/status-bar';
import Loader from 'angular-ecmascript/module-loader';
import RoutesConfig from './routes';
const App = 'whatsapp';
...some lines skipped...
'ionic'
]);
new Loader(App)
.load(RoutesConfig);
Ionic.Platform.ready(() => {
if (Keyboard) {
Keyboard.hideKeyboardAccessoryBar(true);
As you can see there is only one route state defined as for now, called tabs
, which is connected to the tabs
view. Let's add it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ion-tabs class="tabs-stable tabs-icon-top tabs-color-positive" ng-cloak>
<ion-tab title="Favorites" icon-on="ion-ios-star" icon-off="ion-ios-star-outline" href="#/tab/favorites">
<ion-nav-view name="tab-favorites"></ion-nav-view>
</ion-tab>
<ion-tab title="Recents" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline" href="#/tab/recents">
<ion-nav-view name="tab-recents"></ion-nav-view>
</ion-tab>
<ion-tab title="Contacts" icon-on="ion-ios-person" icon-off="ion-ios-person-outline" href="#/tab/contacts">
<ion-nav-view name="tab-contacts"></ion-nav-view>
</ion-tab>
<ion-tab title="Chats" icon-on="ion-ios-chatbubble" icon-off="ion-ios-chatbubble-outline" href="#/tab/chats">
<ion-nav-view name="tab-chats"></ion-nav-view>
</ion-tab>
<ion-tab title="Settings" icon-on="ion-ios-cog" icon-off="ion-ios-cog-outline" href="#/tab/settings">
<ion-nav-view name="tab-settings"></ion-nav-view>
</ion-tab>
</ion-tabs>
In our app we will have 5 tabs: Favorites
, Recents
, Contacts
, Chats
, and Settings
. In this tutorial we will only focus on implementing the Chats
and the Settings
tabs, but your'e more than free to continue on with this tutorial and implement the rest of the tabs.
Let's create Chats
view which will appear one we click on the Chats
tab. But first, let's install an npm package called Moment
which is a utility library for manipulating date object. It will soon come in handy:
$ npm install moment --save
Our package.json
should look like so:
19
20
21
22
23
24
25
"lodash.camelcase": "^4.1.1",
"lodash.upperfirst": "^4.2.0",
"script-loader": "^0.7.0",
"moment": "^2.13.0",
"webpack": "^1.13.0"
},
"devDependencies": {
Now that we have installed Moment
, we need to expose it to our environment, since some libraries we load which are not using es6's module system rely on it being defined as a global variable. For these purposes we shall use the expose-loader
. Simply, add to our index.js
file:
1
2
3
4
5
// modules
import 'expose?moment!moment';
// libs
import 'script!lib/angular/angular';
import 'script!lib/angular-animate/angular-animate';
After the ?
comes the variable name which shuold be defined on the global scope, and after the !
comes the library we would like to load. In this case we load the Moment
library and we would like to expose it as window.global
.
NOTE: Altough
Moment
is defined on the global scope, we will keep importing it in every module we wanna use it, since it's more declerative and clearer.
Now that we have Moment
lock and loaded, we will create our Chats
controller and we will use it to create some data stubs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import Moment from 'moment';
import { Controller } from 'angular-ecmascript/module-helpers';
export default class ChatsCtrl extends Controller {
constructor() {
super(...arguments);
this.data = [
{
_id: 0,
name: 'Ethan Gonzalez',
picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg',
lastMessage: {
text: 'You on your way?',
timestamp: Moment().subtract(1, 'hours').toDate()
}
},
{
_id: 1,
name: 'Bryan Wallace',
picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg',
lastMessage: {
text: 'Hey, it\'s me',
timestamp: Moment().subtract(2, 'hours').toDate()
}
},
{
_id: 2,
name: 'Avery Stewart',
picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg',
lastMessage: {
text: 'I should buy a boat',
timestamp: Moment().subtract(1, 'days').toDate()
}
},
{
_id: 3,
name: 'Katie Peterson',
picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg',
lastMessage: {
text: 'Look at my mukluks!',
timestamp: Moment().subtract(4, 'days').toDate()
}
},
{
_id: 4,
name: 'Ray Edwards',
picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg',
lastMessage: {
text: 'This is wicked good ice cream.',
timestamp: Moment().subtract(2, 'weeks').toDate()
}
}
];
}
}
ChatsCtrl.$name = 'ChatsCtrl';
And we will load it:
4
5
6
7
8
9
10
14
15
16
17
18
19
20
import StatusBar from 'cordova/status-bar';
import Loader from 'angular-ecmascript/module-loader';
import ChatsCtrl from './controllers/chats.controller';
import RoutesConfig from './routes';
const App = 'whatsapp';
...some lines skipped...
]);
new Loader(App)
.load(ChatsCtrl)
.load(RoutesConfig);
Ionic.Platform.ready(() => {
NOTE: From now on any component we create we will also load it right after, without any further explenations.
The data stubs are just a temporary fabricated data which will be used to test our application and how it reacts with it. You can also look at our scheme and figure out how our application is gonna look like.
Now that we have the controller with the data, we need a view to present it. We will use ion-list
and ion-item
directives, which provides us a list layout, and we will iterate our static data using ng-repeat
and we will display the chat's name, image and timestamp.
Let's create it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ion-view view-title="Chats">
<ion-content>
<ion-list>
<ion-item ng-repeat="chat in chats.data | orderBy:'-lastMessage.timestamp'"
class="item-chat item-remove-animate item-avatar item-icon-right"
type="item-text-wrap">
<img ng-src="{{ chat.picture }}">
<h2>{{ chat.name }}</h2>
<p>{{ chat.lastMessage.text }}</p>
<span class="last-message-timestamp">{{ chat.lastMessage.timestamp }}</span>
<i class="icon ion-chevron-right icon-accessory"></i>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
We also need to define the appropriate route state which will be navigated any time we press the Chats
tab. Let's do so:
1
2
3
4
5
6
7
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Config } from 'angular-ecmascript/module-helpers';
export default class RoutesConfig extends Config {
static $inject = ['$stateProvider', '$urlRouterProvider']
configure() {
this.$stateProvider
...some lines skipped...
url: '/tab',
abstract: true,
templateUrl: 'templates/tabs.html'
})
.state('tab.chats', {
url: '/chats',
views: {
'tab-chats': {
templateUrl: 'templates/chats.html',
controller: 'ChatsCtrl as chats'
}
}
});
this.$urlRouterProvider.otherwise('tab/chats');
}
}
If you look closely we used the controllerAs
syntax, which means that our data models should be stored on the controller and not on the scope.
We also used the $urlRouterProvider.otherwise()
which defines our Chats
state as the default one, so any unrecognized route state we navigate to our router will automatically redirect us to this state.
As for now, our chats' dates are presented in a very messy format which is not very informative for the every-day user. We wanna present it in a calendar format. Inorder to do that we need to define a Filter
, which is provided by Angular
and responsibe for projecting our data presented in the view. Let's add the CalendarFilter
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Moment from 'moment';
import { Filter } from 'angular-ecmascript/module-helpers';
export default class CalendarFilter extends Filter {
static $name = 'calendar'
filter(time) {
if (!time) return;
return Moment(time).calendar(null, {
lastDay : '[Yesterday]',
sameDay : 'LT',
lastWeek : 'dddd',
sameElse : 'DD/MM/YY'
});
}
}
5
6
7
8
9
10
11
16
17
18
19
20
21
22
import Loader from 'angular-ecmascript/module-loader';
import ChatsCtrl from './controllers/chats.controller';
import CalendarFilter from './filters/calendar.filter';
import RoutesConfig from './routes';
const App = 'whatsapp';
...some lines skipped...
new Loader(App)
.load(ChatsCtrl)
.load(CalendarFilter)
.load(RoutesConfig);
Ionic.Platform.ready(() => {
And now let's apply it to the view:
7
8
9
10
11
12
13
<img ng-src="{{ chat.picture }}">
<h2>{{ chat.name }}</h2>
<p>{{ chat.lastMessage.text }}</p>
<span class="last-message-timestamp">{{ chat.lastMessage.timestamp | calendar }}</span>
<i class="icon ion-chevron-right icon-accessory"></i>
</ion-item>
</ion-list>
As you can see, inorder to apply a filter in the view we simply pipe it next to our data model.
We would also like to be able to remove a chat, let's add a delete button for each chat:
9
10
11
12
13
14
15
16
17
<p>{{ chat.lastMessage.text }}</p>
<span class="last-message-timestamp">{{ chat.lastMessage.timestamp | calendar }}</span>
<i class="icon ion-chevron-right icon-accessory"></i>
<ion-option-button class="button-assertive" ng-click="chats.remove(chat)">
Delete
</ion-option-button>
</ion-item>
</ion-list>
</ion-content>
And implement its logic in the controller:
53
54
55
56
57
58
59
60
61
62
}
];
}
remove(chat) {
this.data.splice(this.data.indexOf(chat), 1);
}
}
ChatsCtrl.$name = 'ChatsCtrl';
Now everything is ready, but it looks a bit dull. Let's add some style to it:
1
2
3
4
5
6
7
8
9
.item-chat {
.last-message-timestamp {
position: absolute;
top: 16px;
right: 38px;
font-size: 14px;
color: #9A9898;
}
}
Since the stylesheet was written in SASS
, we need to import it into our main scss
file:
21
22
23
24
// Include all of Ionic
@import "www/lib/ionic/scss/ionic";
@import "chats";
NOTE: From now on every
scss
file we write will be imported right after without any further explenations.
Our Chats
tab is now ready. You can run it inside a browser, or if you prefer to see it in a mobile layout, you should use Ionic
's simulator. Just follow the following instructions:
$ npm install -g ios-sim
$ cordova platform add i The API shouldn't be too complicated to understand, and we will get familiar with it as we make progress with this tutorial.
{{tutorialImage 'ionic' '1.png' 500}}
And if you swipe a menu item to the left:
{{tutorialImage 'ionic' '2.png' 400}}