Now we have everything set up to start working on our application, there are three important steps for coding our app. They are as follows:
Defining our main
app.ts
fileCreating providers/services for various functionalities
Creating Ionic pages for various views
This is the root of our application and it defines the root
component using the @App
decorator. This is where we inject all of our dependencies. The following should be present in app.ts
:
/* /app/app.ts */ import {NavController, Platform, ionicBootstrap} from 'ionic-angular'; import {StatusBar} from 'ionic-native'; import {Component, Inject} from '@angular/core'; import {LoginPage} from './pages/login/login'; import {TabsPage} from './pages/tabs/tabs'; import {AuthProvider} from './providers/auth-provider/auth-provider'; import {ChatsProvider} from './providers/chats-provider/chats-provider'; import {UserProvider} from './providers/user-provider/user-provider'; import {UtilProvider} from './providers/utils'; import { FIREBASE_PROVIDERS, defaultFirebase, firebaseAuthConfig, FirebaseRef, AngularFire, AuthProviders, AuthMethods } from 'angularfire2'; @Component({ template: '<ion-nav id="nav" [root]="rootPage" #content></ion-nav>' }) class MyApp { message: string; rootPage: any; constructor(public authProvider:AuthProvider, public platform:Platform) { let auth = authProvider.getAuth(); auth.onAuthStateChanged(user => { if(user) { this.rootPage = TabsPage; } else { this.rootPage = LoginPage; } }); } } ionicBootstrap(MyApp, [FIREBASE_PROVIDERS,defaultFirebase({ apiKey: "AIzaSyC2gX3jlrBugfnBPugX2p0U1XiSqXhrRgQ", authDomain: "chat-app-1e137.firebaseapp.com", databaseURL: "https://chat-app-1e137.firebaseio.com", storageBucket: "chat-app-1e137.appspot.com", }), firebaseAuthConfig({ provider: AuthProviders.Password, method: AuthMethods.Password, remember: 'default', scope: ['email'] }), AuthProvider, ChatsProvider, UserProvider, UtilProvider] )
This is the entry point of our application. In it, we are initializing Firebase and configuring it to use password authentication. We are also checking that, if a user is authenticated, it goes to the TabsPage
; otherwise, it will go to the LoginPage
. We are also providing all of our dependencies. We haven't yet created our dependencies, so TypeScript will show errors. Just ignore it.
ionicBootstrap is an alternative to Angular's bootstrap method for Ionic 2 applications.
Note that we have written a template inside this file. We have used ion-nav
to create a navigation stack, where we will push our views.
Note
To read more about what is going on under the hood inside IonicBootstrap, check this blog post at http://inders.in/blog/2015/10/28/introduction-to-ionic-2/.
We will have the following providers for our application:
AuthProvider
UserProvider
ChatsProvider
UtilProvider
AuthProvider
is the provider that we will use for authentication purposes in our app. With the Ionic CLI, we can now generate pages and providers with the command line.
The following command will create auth-provider.js
files in the app/provider/auth-provider
directory, with some default content:
ionic g provider AuthProvider
Now, we have to change the extension of the file to .ts
for using TypeScript in our code.
This is the module where we handle all of our authentication work. The following code should be present in auth-provider.ts
:
/* /app/provider/auth-provider/auth-provider.ts */ import {Injectable, Inject} from '@angular/core'; import {FirebaseAuth, FirebaseRef, AngularFire} from 'angularfire2'; import {LocalStorage, Storage} from 'ionic-angular'; @Injectable() export class AuthProvider { local = new Storage(LocalStorage); constructor(public af:AngularFire) {} getAuth() { return firebase.auth(); }; signin(credentails) { return this.af.auth.login(credentails); } createAccount(credentails) { return this.af.auth.createUser(credentails); }; logout() { var auth = firebase.auth(); auth.signOut(); } }
Let's understand the preceding code:
getAuth()
returns the Firebase SDK'sfirebase.auth()
method.signin()
does the login process for us. Since we will be using Firebase password authentication. We are using AngularFire2'sauth.login
method by passing it login credentials.createAccount()
takes an object with e-mail and password values and creates a Firebase account for the user. Again, we are using AngularFire2'sauth.createUser
with credentials to create a Firebase user account.logout()
logs the user out. We are using Firebase SDK'sauth.signOut()
function here.
First we will generate our provider using the following command:
ionic g generate UserProvider
Then, change the extension of user-provider.js
to user-provider.ts
.
UserProvider
is for doing user-related work in our application, such as creating a user, getting a list of users, updating the profile of a user, and other things. The following code should be present in user-provider.ts
:
/* /app/providers/user-provider/user-provider.ts */ import {Injectable, Inject} from '@angular/core'; import {FirebaseRef, AngularFire} from 'angularfire2'; import {LocalStorage, Storage} from 'ionic-angular'; import {Camera} from 'ionic-native'; @Injectable() export class UserProvider { local = new Storage(LocalStorage); constructor(public af:AngularFire) { } // Get Current User's UID getUid() { return this.local.get('userInfo') .then(value => { let newValue = JSON.parse(value); return newValue.uid; }); } // Create User in Firebase createUser(userCredentails) { this.getUid().then(uid => { let currentUserRef = this.af.database.object(`/users/${uid}`); currentUserRef.set({email: userCredentails.email}); }); } // Get Info of Single User getUser() { // Getting UID of Logged In User return this.getUid().then(uid => { return this.af.database.object(`/users/${uid}`); }); } // Get All Users of App getAllUsers() { return this.af.database.list('/users'); } // Get base64 Picture of User getPicture() { let base64Picture; let options = { destinationType: 0, sourceType: 0, encodingType:0 }; let promise = new Promise((resolve, reject) => { Camera.getPicture(options).then((imageData) => { base64Picture = "data:image/jpeg;base64," + imageData; resolve(base64Picture); }, (error) => { reject(error); }); }); return promise; } // Update Provide Picture of User updatePicture() { this.getUid().then(uid => { let pictureRef = this.af.database.object(`/users/${uid}/picture`); this.getPicture() .then((image) => { pictureRef.set(image); }); }); } }
Let's understand the preceding code:
getUid()
returns apromise
, which resolves intotheuid
of the logged-in user. It gets theuid
from theuserInfo
key ofLocalStorage
.createUser()
creates a new user in theusers
endpoint in the Firebase database.getUser()
returns theObservable
, which has the information of the logged-in user from the Firebase database.getAllUsers()
returns an AngularFire2 listObservable
, which lists all the users of our application.getPicture()
gets a picture from the user's mobile device and returns apromise
. This promise resolves into a base64 Encoded JPEG Image.updatePicture()
updates the user's profile picture. It takes the picture using thegetPicture
method and sets thepicture
key of the logged-in user.
Note
In this app, we are storing images as a base64 string. It is not a very efficient method. Instead, Firebase provides us with a storage bucket to store binary data such as images, videos, and other binary data. We have used the Firebase storage-bucket approach in Chapter 7, Social App with Firebase of this book.
First, we need to generate our provider using the following command:
ionic g provider ChatsProvider
Then, change the extension of the file chats-provider.js
to chats-provider.ts
.
ChatsProvider
is used to get a list of previous chats and check if a chat already exists between two users. The following code should be present in chats-provider.ts
:
/* /app/providers/chats-provider/chats-provider.ts */ import {Injectable, Inject} from '@angular/core'; import {AngularFire, FirebaseRef} from 'angularfire2'; import {Observable} from 'rxjs/Observable'; import {UserProvider} from '../user-provider/user-provider'; @Injectable() export class ChatsProvider { constructor(public af: AngularFire, public up: UserProvider) {} // get list of Chats of a Logged In User getChats() { return this.up.getUid().then(uid => { let chats = this.af.database.list(`/users/${uid}/chats`); return chats; }); } // Add Chat References to Both users addChats(uid,interlocutor) { // First User let otherUid = interlocutor; let endpoint = this.af.database.object(`/users/${uid}/chats/${interlocutor}`); endpoint.set(true); // Second User let endpoint2 = this.af.database.object(`/users/${interlocutor}/chats/${uid}`); endpoint2.set(true); } getChatRef(uid, interlocutor) { let firstRef = this.af.database.object(`/chats/${uid},${interlocutor}`, {preserveSnapshot:true}); let promise = new Promise((resolve, reject) => { firstRef.subscribe(snapshot => { let a = snapshot.exists(); if(a) { resolve(`/chats/${uid},${interlocutor}`); } else { let secondRef = this.af.database.object(`/chats/${interlocutor}, ${uid}`, {preserveSnapshot:true}); secondRef.subscribe(snapshot => { let b = snapshot.exists(); if(!b) { this.addChats(uid,interlocutor); } }); resolve(`/chats/${interlocutor},${uid}`); } }); }); return promise; } }
Let's understand the preceding code:
getChats()
gets a list of chats of the logged-in user-chats that a user has already initiated.addChats()
takes two input values. The first is theuid
of the logged-in user, and the second is theuid
of the other user (interlocutor). This function adds chat references (uid
of the other user) to both users' information in the Firebase database.getChatRef()
takes two arguments. One is theuid
of the logged-in user and the other is theuid
of the other user. It returns apromise
, which resolves to the Firebase database URL of the chats between these two users. If this is the first time these two users are chatting, it creates a URL in the form of/chats/${interlocutor},${uid}
, where${interlocutor}
is theuid
of the other user and${uid}
is theuid
of the logged-in user.
UtilProvider
is a module for abstracting some functionalities that we will use repeatedly, such as the Ionic alert. The following code should be present in utils.ts
:
/* /app/providers/utils.ts */ import {Injectable, Inject} from '@angular/core'; import {Alert} from 'ionic-angular'; @Injectable() export class UtilProvider { doAlert(title, message, buttonText) { console.log(message); let alert = Alert.create({ title: title, subTitle: message, buttons: [buttonText] }); return alert; } }
In UtilProvider
, we have created a doAlert
function that takes a title
, message
, and buttonText
as an input, and creates an Ionic alert box for us. This function becomes very useful for displaying alert messages without writing alert code again and again.
Now we have to define all the providers and services for our application. First let's define the pages of our application.
We will have the following pages in our application:
LoginPage
TabsPage
UsersPage
ChatsPage
AccountPage
ChatViewPage
Each page has two or three files. One is the .ts
file, which controls the page. The other is the .html
file, which is the template of the page and, if present, the last is the .scss
file, which is the styling file for the page.
The LoginPage
includes the login template and a controller to handle that template. This is the page where the user creates an account or logs in. The following code should be present in login.ts
:
/* /app/pages/login/login.ts*/ import {Component} from '@angular/core'; import {NavController, Storage, LocalStorage} from 'ionic-angular'; import {TabsPage} from '../tabs/tabs'; import {FormBuilder, Validators} from '@angular/common'; import {validateEmail} from '../../validators/email'; import {AuthProvider} from '../../providers/auth-provider/auth-provider'; import {UserProvider} from '../../providers/user-provider/user-provider'; import {UtilProvider} from '../../providers/utils'; import {FirebaseAuth} from 'angularfire2'; @Component({ templateUrl: 'build/pages/login/login.html' }) export class LoginPage { loginForm:any; storage = new Storage(LocalStorage); constructor(public nav:NavController, form:FormBuilder, public auth: AuthProvider, public userProvider: UserProvider, public util: UtilProvider) { this.loginForm = form.group({ email: ["",Validators.compose([Validators.required, validateEmail])], password:["",Validators.required] }); } signin() { this.auth.signin(this.loginForm.value) .then((data) => { this.storage.set('userInfo', JSON.stringify(data)); this.nav.push(TabsPage); }, (error) => { let errorMessage = "Enter Correct Email and Password"; let alert = this.util.doAlert("Error",errorMessage,"Ok"); this.nav.present(alert); }); }; createAccount() { let credentails = this.loginForm.value; this.auth.createAccount(credentails) .then((data) => { this.storage.set('userInfo', JSON.stringify(data)); this.userProvider.createUser(credentails); }, (error) => { let errorMessage = "Account Already Exists"; let alert = this.util.doAlert("Error",errorMessage,"Ok"); this.nav.present(alert); }); }; }
In the constructor, we have created a login form using Angular2's form builder, and we are using a custom validator that we have defined in the following code to validate the e-mail ID, since Angular2 doesn't have a validator for e-mail ID.
The signin
function takes the user's e-mail and password, and authenticates the user, using the signin
member function of AuthProvider
. If the user is authenticated successfully they navigate to the TabsPage
. Otherwise, it shows an error message using the doAlert
member function of UtilProvider
.
Similarly, the createAccount
function takes the user's e-mail and password and creates a new user account. It also adds the user's information to the users
key in the Firebase database. If the user already exists it shows an error message.
The following code should be present in login.html
:
<!-- /app/pages/login/login.html --> <ion-header> <ion-navbar primary> <ion-title>Login</ion-title> </ion-navbar> </ion-header> <ion-content class="padding"> <form [ngFormModel]="loginForm"> <ion-list> <ion-item> <ion-label floating>Email</ion-label> <ion-input type="text" ngControl="email"></ion-input> </ion-item> <ion-item> <ion-label floating>Password</ion-label> <ion-input type="password" ngControl="password"></ion- input> </ion-item> </ion-list> <div padding> <button primary block (click)="signin()" [disabled]="!loginForm.valid">Sign In</button> </div> <div padding> <button full clear favorite (click)="createAccount()" [disabled]="!loginForm.valid"> <ion-icon name="person"></ion-icon> Create an Account</button> </div> </form> </ion-content>
Both the Sign In and Create Account buttons will be enabled only when the form is valid.
We need to define a custom validator, which validates the e-mail input from users because Angular 2 doesn't have a default e-mail validator. The following code should be present in email.ts
:
/* /app/validators/email.ts */ import {Control} from '@angular/common'; export function validateEmail(c: Control) { let EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0- 9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; return EMAIL_REGEXP.test(c.value) ? null : { validateEmail: { valid: false } }; }
This just takes the control's value and validates it against the regular expression provided using EMAIL_REGEXP.
The TabsPage
handles the tabs of our app. This is the place where we define the root page of each tab. The following code should be present in tabs.ts
:
/* /app/pages/tabs/tabs.ts */ import {Component} from '@angular/core'; import {NavController} from 'ionic-angular'; import {ChatsPage} from '../chats/chats'; import {AccountPage} from '../account/account'; import {UsersPage} from '../users/users'; @Component({ templateUrl: 'build/pages/tabs/tabs.html' }) export class TabsPage { chats = ChatsPage; users = UsersPage; profile = AccountPage; }
The following code should be present in tabs.html
:
<!-- /app/pages/tabs/tabs.html --> <ion-tabs light> <ion-tab [root]="users" tabTitle="Users" tabIcon="people"></ion- tab> <ion-tab [root]="chats" tabTitle="Chats" tabIcon="chatboxes"></ion- tab> <ion-tab [root]="profile" tabTitle="Account" tabIcon="person"></ion-tab> </ion-tabs>
In the TabsPage
, we have just defined the root pages of all our tabs. The [root]
property is used to set the root page of a tab. By default, the first tab is opened when the TabsPage is pushed into the navigation stack.
The UserPage
is the page where we will list all of our users. The following code should be present in users.ts
:
/* /app/pages/users/users.ts */ import {Component} from '@angular/core'; import {NavController} from 'ionic-angular'; import {Observable} from 'rxjs/Observable'; import {UserProvider} from '../../providers/user-provider/user-provider'; import {ChatViewPage} from '../chat-view/chat-view'; @Component({ templateUrl: 'build/pages/users/users.html' }) export class UsersPage { users:Observable<any[]>; uid:string; constructor(public nav: NavController, public userProvider: UserProvider) { userProvider.getUid() .then(uid => { this.uid = uid; this.users = this.userProvider.getAllUsers(); }); } openChat(key) { let param = {uid: this.uid, interlocutor: key}; this.nav.push(ChatViewPage,param); } }
In the constructor, we got the uid
of the logged-in user and a list of all users of the app as an Observable
.
The openChat
function opens the ChatViewPage
by passing the uid
of both users as navigation parameters.
The following code should be present in user.html
:
<!-- /app/pages/users/users.html --> <ion-header> <ion-navbar primary> <ion-title>Users</ion-title> <ion-buttons end> <ion-spinner *ngIf="!(users | async)"></ion-spinner> </ion-buttons> </ion-navbar> </ion-header> <ion-content> <ion-list> <span *ngFor="let user of users | async"> <a ion-item (click)="openChat(user.$key)" *ngIf="user.$key !== uid"> <ion-avatar item-left> <img *ngIf="!user.picture" src="img/default.jpg"> <img *ngIf="user.picture" src="{{user.picture}}"> </ion-avatar> <h2>{{user.email}}</h2> </a> </span> </ion-list> </ion-content>
We have used ngFor
to iterate over the users list and we have excluded the logged-in user from the list using ngIf
. Basically, we check if the uid
of the logged-in user and the user in the iteration is the same, then exclude it from the list. We are also showing the avatar of each user. If the user has uploaded his picture, we show it from Firebase; otherwise, we show a default image.
We are also showing a loading spinner in the navbar
until we get the list of users.
The ChatsPage
lists all previous chats. The following code should be present in chats.ts
:
/* /app/pages/chats/chats.ts */ import {Component} from '@angular/core'; import {NavController, NavParams} from 'ionic-angular'; import {Observable} from 'rxjs/Rx'; import {UserProvider} from '../../providers/user-provider/user-provider'; import {ChatsProvider} from '../../providers/chats-provider/chats-provider'; import {AngularFire} from 'angularfire2'; import 'rxjs/add/operator/map'; import {ChatViewPage} from '../chat-view/chat-view'; @Component({ templateUrl: 'build/pages/chats/chats.html' }) export class ChatsPage { chats:Observable<any[]>; constructor(public chatsProvider: ChatsProvider, public userProvider: UserProvider, public af:AngularFire, public nav: NavController) { this.chatsProvider.getChats() .then(chats => { this.chats = chats.map(users => { return users.map(user => { user.info = this.af.database.object(`/users/${user.$key}`); return user; }); }); }); } openChat(key) { this.userProvider.getUid() .then(uid => { let param = {uid: uid, interlocutor: key}; this.nav.push(ChatViewPage,param); }); } }
The ChatsPage
is similar to the UsersPage
. The openChat
function does exactly the same thing as it does in the UsersPage
. The only difference is that instead of showing all the users of the application, we show only those users who have already had a conversation with the logged-in user. First we get all the references of his previous chats from his Firebase endpoint, which contains all the uid
of other people. Then we map all those uid
to keys inside the users endpoint to get the e-mails of those users. It is like a join. This is an asynchronous process, and this is what Observables
are capable of. You can filter, map, search, and do lots of other stuff on Observables
.
The following code should be present in chats.html
:
<!-- /app/pages/chats/chats.html --> <ion-header> <ion-navbar primary> <ion-title>Chats</ion-title> <ion-buttons end> <ion-spinner primary *ngIf="!(chats | async)"></ion- spinner> </ion-buttons> </ion-navbar> </ion-header> <ion-content> <a ion-item *ngFor="let chat of chats | async" (click)="openChat(chat.$key)"> <ion-avatar item-left> <img *ngIf="!(chat.info | async).picture" src="img/default.jpg"> <img *ngIf="(chat.info | async).picture" src=" {{(chat.info | async).picture}}"> </ion-avatar> <span>{{(chat.info | async).email}}</span> </a> </ion-content>
The ChatViewPage
is the place where the actual chatting takes place. The following code should be present in chat-view.ts
:
/* /app/pages/chat-view/chat-view.ts */ import {Component, ViewChild} from '@angular/core'; import {NavController, NavParams, Content} from 'ionic-angular'; import {Observable} from 'rxjs/Observable'; import {AngularFire, FirebaseListObservable} from 'angularfire2'; import {ChatsProvider} from '../../providers/chats-provider/chats-provider'; import {UserProvider} from '../../providers/user-provider/user-provider'; @Component({ templateUrl: 'build/pages/chat-view/chat-view.html', }) export class ChatViewPage { message: string; uid:string; interlocutor:string; chats:FirebaseListObservable<any>; @ViewChild(Content) content: Content; constructor(public nav:NavController, params:NavParams, public chatsProvider:ChatsProvider, public af:AngularFire, public userProvider:UserProvider) { this.uid = params.data.uid; this.interlocutor = params.data.interlocutor; // Get Chat Reference chatsProvider.getChatRef(this.uid, this.interlocutor) .then((chatRef:any) => { this.chats = this.af.database.list(chatRef); }); } ionViewDidEnter() { this.content.scrollToBottom(); } sendMessage() { if(this.message) { let chat = { from: this.uid, message: this.message, type: 'message' }; this.chats.push(chat); this.message = ""; } }; sendPicture() { let chat = {from: this.uid, type: 'picture', picture:null}; this.userProvider.getPicture() .then((image) => { chat.picture = image; this.chats.push(chat); }); } }
In the constructor, we get the uid
of both users and then we get the Firebase URL of their chat's endpoint, which is something like this: /chats/uid1,uid2
. With this URL, we get a list of all the messages between these two users' AngularFire2 lists.
In the sendMessage
function, we send chat messages using the push function of the AngularFire2 list. Similarly, in the sendPicture
function, we get a picture from the user's gallery and send it as a base64 encoded string.
The ionViewDidEnter()
function is an Ionic page hook. It fires each time a page is pushed in the navigation stack. We scroll to the bottom in this function using the scrollToBottom()
method of ion-content
.
It is important to note that we are using a @ViewChild
decorator to get hold of ion-content
.
Note
For further information about Ionic page life cycle hooks, take a look at http://ionicframework.com/docs/v2/api/components/nav/NavController/.
The following code should be present in chat-view.html
:
<!-- /app/pages/chat-view/chat-view.html --> <ion-header> <ion-navbar primary> <ion-title>Chat</ion-title> <ion-buttons end> <button (click)="sendPicture()"><ion-icon name="image" ></ion- icon>Send Image</button> </ion-buttons> </ion-navbar> </ion-header> <ion-content padding class="chat-view" id="chat-view"> <div class="messages"> <div class="message" *ngFor="let chat of chats | async" [ngClass]="{'me': uid === chat.from}"> <span *ngIf="chat.message">{{chat.message}}</span> <img *ngIf="chat.picture" src="{{chat.picture}}" class="picture"> </div> </div> </ion-content> <ion-footer> <ion-toolbar> <ion-row> <ion-col width-10> <ion-spinner *ngIf="!(chats)"></ion-spinner> </ion-col> <ion-col width-70 [hidden]="!chats"> <ion-input type="text" placeholder="Enter Message" [(ngModel)]="message"> </ion-input> </ion-col> <ion-col width-20 [hidden]="!chats"> <button full (click)="sendMessage()"><ion-icon name="send"> </ion-icon></button> </ion-col> </ion-row> </ion-toolbar> </ion-footer>
The following code should be present in chat-view.scss:
/* /app/pages/chat-view/chat-view.scss */ .chat-view { .messages { width: 100%; position: absolute; .message { width: 70%; padding: 5px 10px; background: #3F51B5; color: #fff; border-radius: 5px; margin: 5px; float: left; } .message.me { float: right; background: #fff; border: 1px solid #3F51B5; color: #222; text-align: right; } } }
The AccountPage
is the place where the user updates their profile picture and logs out. The following code should be present in accounts.ts
:
/* /app/pages/account/account.ts */ import {Component} from '@angular/core'; import {NavController, LocalStorage, Storage} from 'ionic-angular'; import {AuthProvider} from '../../providers/auth-provider/auth-provider'; import {UserProvider} from '../../providers/user-provider/user-provider'; @Component({ templateUrl: 'build/pages/account/account.html' }) export class AccountPage { rootNav; user = {}; local = new Storage(LocalStorage); constructor(public nav: NavController, public auth: AuthProvider, public userProvider: UserProvider) { this.userProvider.getUser() .then(userObservable => { userObservable.subscribe(user => { this.user = user; }); }); } //save user info updatePicture() { this.userProvider.updatePicture(); }; logout() { this.local.remove('userInfo'); this.auth.logout(); } }
In the AccountPage
, we are just updating the user's profile picture using the updatePicture
function, and we are logging out the user using the logout
function. When the user logs out, we also remove the value of the userInfo
key from LocalStorage
.
The following code should present in accounts.html
:
<!-- /app/pages/account/account.html --> <ion-header> <ion-navbar primary> <ion-title>Account</ion-title> </ion-navbar> </ion-header> <ion-content> <ion-list> <ion-card> <img *ngIf="!user.picture" src="img/default.jpg"/> <img *ngIf="user.picture" src="{{user.picture}}"/> </ion-card> <ion-item> <button full (click)="updatePicture()">Change Picture</button> </ion-item> <a ion-item primary (click)="logout()"> Logout </a> </ion-list> </ion-content>