Now that we're finished with the initial setup, we can start building our app.
Our app is gonna have a very clear methodology. It's gonna be made out of pages, and each page will be made out of 3 files:
.html
- A view template file written in HTML
based on Angular 2
's new template engine..scss
- A stylesheet file written in a CSS
pre-process language called SASS..ts
- A script file written in Typescript
.Following this pattern, we will create our first page, starting with its component - named ChatsPage
:
1
2
3
4
5
6
7
8
9
10
11
import { Component } from '@angular/core';
import template from './chats.html';
@Component({
template
})
export class ChatsPage {
constructor() {
}
}
Angular 2
uses decorators to declare Component
s, and we use ES2016
classes to create the actual component, and the template
declares the template file for the component. So now let's create this template file, next to the component file:
1
2
3
4
5
6
7
8
9
10
11
<ion-header>
<ion-navbar>
<ion-title>
Chats
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
Hello!
</ion-content>
Once creating an Ionic page it's recommended to use the following layout:
Now, we need to add a declaration for this new Component
in our NgModule
definition:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { ChatsPage } from '../pages/chats/chats'
import { MyApp } from './app.component';
@NgModule({
declarations: [
MyApp,
ChatsPage
],
imports: [
IonicModule.forRoot(MyApp),
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
ChatsPage
],
providers: [
{ provide: ErrorHandler, useClass: IonicErrorHandler }
You can read more about Angular 2 NgModule here.
We will define the ChatsPage
as the initial component of our app by setting the rootPage
property in the main app component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { ChatsPage } from '../pages/chats/chats';
import template from "./app.html";
@Component({
template
})
export class MyApp {
rootPage = ChatsPage;
constructor(platform: Platform) {
platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
To make the rootPage
visible, we will need to use the ion-nav
component in the application's view:
1
<ion-nav [root]="rootPage"></ion-nav>
Let's add some code to our Component
with a simple logic; Once the component is created we gonna define some dummy chats, using the Observable.of
, so we can have some data to test our view against:
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
59
60
61
62
63
64
65
import { Component } from '@angular/core';
import * as Moment from 'moment';
import { Observable } from 'rxjs';
import template from './chats.html';
@Component({
template
})
export class ChatsPage {
chats: Observable<any[]>;
constructor() {
this.chats = this.findChats();
}
private findChats(): Observable<any[]> {
return Observable.of([
{
_id: '0',
title: 'Ethan Gonzalez',
picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg',
lastMessage: {
content: 'You on your way?',
createdAt: Moment().subtract(1, 'hours').toDate()
}
},
{
_id: '1',
title: 'Bryan Wallace',
picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg',
lastMessage: {
content: 'Hey, it\'s me',
createdAt: Moment().subtract(2, 'hours').toDate()
}
},
{
_id: '2',
title: 'Avery Stewart',
picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg',
lastMessage: {
content: 'I should buy a boat',
createdAt: Moment().subtract(1, 'days').toDate()
}
},
{
_id: '3',
title: 'Katie Peterson',
picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg',
lastMessage: {
content: 'Look at my mukluks!',
createdAt: Moment().subtract(4, 'days').toDate()
}
},
{
_id: '4',
title: 'Ray Edwards',
picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg',
lastMessage: {
content: 'This is wicked good ice cream.',
createdAt: Moment().subtract(2, 'weeks').toDate()
}
}
]);
}
}
Further explanation regards
RxJS
can be found in step 3
moment
is an essential package for our data fabrication, which requires us to install it using the following command:
$ meteor npm install --save moment
Now, because we use TypeScript
, we can define our own data-types and use them in our app, which will give you a better auto-complete and developing experience in most IDEs. In our application, we have 2 models: a chat
model and a message
model. Since we will probably be using these models in both client and server, we will create a dir which is common for both, called imports
:
$ mkdir imports
Inside the imports
dir, we will define our initial models.ts
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export enum MessageType {
TEXT = <any>'text'
}
export interface Chat {
_id?: string;
title?: string;
picture?: string;
lastMessage?: Message;
}
export interface Message {
_id?: string;
chatId?: string;
content?: string;
createdAt?: Date;
type?: MessageType
}
Now that the models are up and set, we can use apply it to the ChatsPage
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
22
23
24
25
26
27
28
29
32
33
34
35
36
37
38
39
42
43
44
45
46
47
48
49
52
53
54
55
56
57
58
59
62
63
64
65
66
67
68
69
import { Component } from '@angular/core';
import * as Moment from 'moment';
import { Observable } from 'rxjs';
import { Chat, MessageType } from '../../../../imports/models';
import template from './chats.html';
@Component({
template
})
export class ChatsPage {
chats: Observable<Chat[]>;
constructor() {
this.chats = this.findChats();
}
private findChats(): Observable<Chat[]> {
return Observable.of([
{
_id: '0',
...some lines skipped...
picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg',
lastMessage: {
content: 'You on your way?',
createdAt: Moment().subtract(1, 'hours').toDate(),
type: MessageType.TEXT
}
},
{
...some lines skipped...
picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg',
lastMessage: {
content: 'Hey, it\'s me',
createdAt: Moment().subtract(2, 'hours').toDate(),
type: MessageType.TEXT
}
},
{
...some lines skipped...
picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg',
lastMessage: {
content: 'I should buy a boat',
createdAt: Moment().subtract(1, 'days').toDate(),
type: MessageType.TEXT
}
},
{
...some lines skipped...
picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg',
lastMessage: {
content: 'Look at my mukluks!',
createdAt: Moment().subtract(4, 'days').toDate(),
type: MessageType.TEXT
}
},
{
...some lines skipped...
picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg',
lastMessage: {
content: 'This is wicked good ice cream.',
createdAt: Moment().subtract(2, 'weeks').toDate(),
type: MessageType.TEXT
}
}
]);
Ionic 2
provides us with a comfortable theming system which is based on SASS
variables. The theme definition file is located in client/imports/theme/variable.scss
. Since we want our app to have a "Whatsappish" look, we will define a new SASS
variable called whatsapp
in the variables file:
11
12
13
14
15
16
17
18
secondary: #32db64,
danger: #f53d3d,
light: #f4f4f4,
dark: #222,
whatsapp: #075E54
);
// Components
The whatsapp
color can be used by adding an attribute called color
with a value whatsapp
to any Ionic component.
To begin with, we can start by implementing the ChatsView
and apply our newly defined theme into it. This view will contain a list representing all the available chats in the component's data-set:
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
<ion-header>
<ion-navbar color="whatsapp">
<ion-title>
Chats
</ion-title>
<ion-buttons end>
<button ion-button icon-only class="add-chat-button">
<ion-icon name="person-add"></ion-icon>
</button>
<button ion-button icon-only class="options-button">
<ion-icon name="more"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content class="chats-page-content">
<ion-list class="chats">
<ion-item-sliding *ngFor="let chat of chats | async">
<button ion-item class="chat">
<img class="chat-picture" [src]="chat.picture">
<div class="chat-info">
<h2 class="chat-title">{{chat.title}}</h2>
<span *ngIf="chat.lastMessage" class="last-message">
<p *ngIf="chat.lastMessage.type == 'text'" class="last-message-content last-message-content-text">{{chat.lastMessage.content}}</p>
<span class="last-message-timestamp">{{chat.lastMessage.createdAt }}</span>
</span>
</div>
</button>
<ion-item-options class="chat-options">
<button ion-button color="danger" class="option option-remove">Remove</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content>
We use ion-list
which Ionic translates into a list, and we use ion-item
to represent a single item in that list. A chat item includes an image, the receiver's name, and its recent message.
The
async
pipe is used to iterate through data which should be fetched asynchronously, in this case, observables.
Now, in order to finish our theming and styling, let's create a stylesheet file for our component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.chats-page-content {
.chat-picture {
border-radius: 50%;
width: 50px;
float: left;
}
.chat-info {
float: left;
margin: 10px 0 0 20px;
.last-message-timestamp {
position: absolute;
top: 10px;
right: 10px;
font-size: 14px;
color: #9A9898;
}
}
}
2
3
4
5
6
7
8
@import "imports/theme/variables";
// App
@import "imports/app/app";
// Pages
@import "imports/pages/chats/chats";
Ionic will load newly defined stylesheet files automatically, so you shouldn't be worried for importations.
Since Ionic 2
uses Angular 2
as the layer view, we can load Angular 2
modules just like any other plain Angular 2
application. One module that may come in our interest would be the angular2-moment
module, which will provide us with the ability to use moment
's utility functions in the view as pipes.
It requires us to install angular2-moment
module using the following command:
$ meteor npm install --save angular2-moment
Now we will need to declare this module in the app's main component:
1
2
3
4
5
11
12
13
14
15
16
17
import { NgModule, ErrorHandler } from '@angular/core';
import { MomentModule } from 'angular2-moment';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { ChatsPage } from '../pages/chats/chats'
import { MyApp } from './app.component';
...some lines skipped...
],
imports: [
IonicModule.forRoot(MyApp),
MomentModule
],
bootstrap: [IonicApp],
entryComponents: [
Which will make moment
available as a pack of pipes, as mentioned earlier:
24
25
26
27
28
29
30
<span *ngIf="chat.lastMessage" class="last-message">
<p *ngIf="chat.lastMessage.type == 'text'" class="last-message-content last-message-content-text">{{chat.lastMessage.content}}</p>
<span class="last-message-timestamp">{{ chat.lastMessage.createdAt | amCalendar }}</span>
</span>
</div>
</button>
Ionic provides us with components which can handle mobile events like: slide, tap and pinch. Since we're going to take advantage of the "sliding" event in the chats
view, we used the ion-item-sliding
component, so any time we will slide our item to the left, we should see a Remove
button.
Right now this button is not hooked to anything. It requires us to bind it into the component:
29
30
31
32
33
34
35
</div>
</button>
<ion-item-options class="chat-options">
<button ion-button color="danger" class="option option-remove" (click)="removeChat(chat)">Remove</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
And now that it is bound to the component we can safely implement its handler:
68
69
70
71
72
73
74
75
76
77
78
79
80
}
]);
}
removeChat(chat: Chat): void {
this.chats = this.chats.map<Chat[]>(chatsArray => {
const chatIndex = chatsArray.indexOf(chat);
chatsArray.splice(chatIndex, 1);
return chatsArray;
});
}
}
{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/setup" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/rxjs"}}}