Ionic is a CSS and JavaScript framework. It is highly recommended that before starting this step you will get yourself familiar with its documentation.
In this step we will learn how to add Ionic library into our project, and use its powerful directives to create cross platform mobile (Android & iOS) applications.
We will achieve this by creating separate views for web and for mobile so be creating a separate view for the mobile applications, but we will keep the shared code parts as common code!
Using ionic is pretty simple - first, we need to install it:
$ meteor npm install ionic-angular --save
We also have to install missing packages that required by Ionic:
$ meteor npm install @angular/http @angular/platform-server --save
We are going to have two different NgModule
s, because of the differences in the imports and declarations between the two platforms (web and Ionic).
We will also separate the main Component
that in use.
So let's start with the AppComponent
that needed to be change to app.component.web.ts
, and it's template that ness to be called app.component.web.html
.
Now update the usage of the template in the Component:
1
2
3
4
5
6
7
import { Component } from '@angular/core';
import style from './app.component.scss';
import template from './app.component.web.html';
import {InjectUser} from "angular2-meteor-accounts-ui";
@Component({
And modify the import path in the module file:
6
7
8
9
10
11
12
import { Ng2PaginationModule } from 'ng2-pagination';
import { AgmCoreModule } from 'angular2-google-maps/core';
import { AppComponent } from "./app.component.web";
import { routes, ROUTES_PROVIDERS } from './app.routes';
import { PARTIES_DECLARATIONS } from './parties';
import { SHARED_DECLARATIONS } from './shared';
Now let's take back the code we modified in the previous step (#21) and use only the original version of the Login component, because we do not want to have login in our Ionic version (it will be read only):
5
6
7
8
9
10
11
12
13
14
15
16
import { PartyDetailsComponent } from './parties/party-details.component';
import {SignupComponent} from "./auth/signup.component";
import {RecoverComponent} from "./auth/recover.component";
import {LoginComponent} from "./auth/login.component.web";
export const routes: Route[] = [
{ path: '', component: PartiesListComponent },
{ path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] },
{ path: 'login', component: LoginComponent },
{ path: 'signup', component: SignupComponent },
{ path: 'recover', component: RecoverComponent }
];
Create a root Component for the mobile, and call it AppMobileComponent
:
1
2
3
4
5
6
7
8
9
10
11
12
13
import {Component} from "@angular/core";
import template from "./app.component.mobile.html";
import {MenuController, Platform, App} from "ionic-angular";
@Component({
selector: "app",
template
})
export class AppMobileComponent {
constructor(app: App, platform: Platform, menu: MenuController) {
}
}
And let's create it's view:
1
<ion-nav [root]="rootPage" swipe-back-enabled="true"></ion-nav>
We used ion-nav
which is the navigation bar of Ionic, we also declared that our root page is rootPage
which we will add later.
Now let's create an index file for the ionic component declarations:
1
2
3
4
5
import {AppMobileComponent} from "./app.component.mobile";
export const MOBILE_DECLARATIONS = [
AppMobileComponent
];
In order to create two different versions of NgModule
- one for each platform, we need to identify which platform are we running now - we already know how to do this from the previous step - we will use Meteor.isCordova
.
We will have a single NgModule
called AppModule
, but it's declaration will be different according to the platform.
So we already know how the web module looks like, we just need to understand how mobile module defined when working with Ionic.
First - we need to import IonicModule
and declare our root Component there.
We also need to declare IonicApp
as our bootstrap
Component, and add every Ionic page
to the entryComponents
.
So let's create it and differ the platform:
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
66
67
68
69
70
71
72
import { MaterialModule } from "@angular/material";
import { AUTH_DECLARATIONS } from "./auth/index";
import { FileDropModule } from "angular2-file-drop";
import { MOBILE_DECLARATIONS } from "./mobile/index";
import { AppMobileComponent } from "./mobile/app.component.mobile";
import { IonicModule, IonicApp } from "ionic-angular";
let moduleDefinition;
if (Meteor.isCordova) {
moduleDefinition = {
imports: [
IonicModule.forRoot(AppMobileComponent)
],
declarations: [
...SHARED_DECLARATIONS,
...MOBILE_DECLARATIONS
],
providers: [
],
bootstrap: [
IonicApp
],
entryComponents: [
AppMobileComponent
]
}
}
else {
moduleDefinition = {
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot(routes),
AccountsModule,
Ng2PaginationModule,
AgmCoreModule.forRoot({
apiKey: 'AIzaSyAWoBdZHCNh5R-hB5S5ZZ2oeoYyfdDgniA'
}),
MaterialModule.forRoot(),
FileDropModule
],
declarations: [
AppComponent,
...PARTIES_DECLARATIONS,
...SHARED_DECLARATIONS,
...AUTH_DECLARATIONS
],
providers: [
...ROUTES_PROVIDERS
],
bootstrap: [
AppComponent
]
}
}
@NgModule(moduleDefinition)
export class AppModule {}
Our next step is to change our selector of the root Component.
As we already know, the root Component of the web platform uses <app>
tag as the selector, but in our case the root Component has to be IonicApp
that uses <ion-app>
tag.
So we need to have the ability to switch <app>
to <ion-app>
when using mobile platform.
There is a package called ionic-selector
we can use in order to get this done, so let's add it:
$ meteor npm install --save ionic-selector
Now let's use in before bootstrapping our module:
6
7
8
9
10
11
12
13
14
15
16
17
18
import '../both/methods/parties.methods';
import ionicSelector from 'ionic-selector';
Meteor.startup(() => {
if (Meteor.isCordova) {
ionicSelector("app");
}
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
});
What it does? It's changing tag name of the main component (app
by default but you can specify any selector you want) to ion-app
.
An example:
<body>
<app class="main"></app>
</body>
will be changed to:
<body>
<ion-app class="main"></ion-app>
</body>
Our next step is to load Ionic style and icons (called ionicons
) only to the mobile platform.
Start by adding the icons package:
$ meteor npm install --save ionicons
Also, let's create a style file for the mobile and Ionic styles, and load the icons package to it:
1
@import "{}/node_modules/ionicons/dist/scss/ionicons";
And let's imports this file into our main styles file:
1
2
3
4
5
@import '../node_modules/@angular/material/core/theming/all-theme';
@import "imports/app/mobile/ionic.scss";
@include md-core();
$app-primary: md-palette($md-light-blue, 500, 100, 700);
Now we need to load Ionic stylesheet into our project - but we need to load it only to the mobile platform, without loading it to the web platform (otherwise, it will override our styles):
2
3
4
5
6
7
8
9
10
11
import template from "./app.component.mobile.html";
import {MenuController, Platform, App} from "ionic-angular";
if (Meteor.isCordova) {
require("ionic-angular/css/ionic.css");
}
@Component({
selector: "app",
template
We also need to add some CSS classes in order to get a good result:
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
margin: 0;
}
body.mobile {
overflow: hidden;
}
body.web {
overflow: visible;
position: initial;
}
.sebm-google-map-container {
width: 450px;
height: 450px;
And let's add the correct class to the body
:
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import ionicSelector from 'ionic-selector';
function setClass(css) {
if (!document.body.className) {
document.body.className = "";
}
document.body.className += " " + css;
}
Meteor.startup(() => {
if (Meteor.isCordova) {
ionicSelector("app");
setClass('mobile');
}
else {
setClass('web');
}
const platform = platformBrowserDynamic();
We created a mechanism that adds
web
ormobile
class to<body/>
element depends on environment.
We want to share the logic of PartiesListComponent
without sharing it's styles and template - because we want a different looks between the platforms.
In order to do so, let's take all of the logic we have in PartiesListComponent
and take it to an external file that won't contain the Component decorator:
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import {OnDestroy, OnInit} from "@angular/core";
import {Observable, Subscription, Subject} from "rxjs";
import {Party} from "../../../../both/models/party.model";
import {PaginationService} from "ng2-pagination";
import {MeteorObservable} from "meteor-rxjs";
import {Parties} from "../../../../both/collections/parties.collection";
import {Counts} from "meteor/tmeasday:publish-counts";
import {InjectUser} from "angular2-meteor-accounts-ui";
interface Pagination {
limit: number;
skip: number;
}
interface Options extends Pagination {
[key: string]: any
}
@InjectUser('user')
export class PartiesList implements OnInit, OnDestroy {
parties: Observable<Party[]>;
partiesSub: Subscription;
pageSize: Subject<number> = new Subject<number>();
curPage: Subject<number> = new Subject<number>();
nameOrder: Subject<number> = new Subject<number>();
optionsSub: Subscription;
partiesSize: number = 0;
autorunSub: Subscription;
location: Subject<string> = new Subject<string>();
user: Meteor.User;
imagesSubs: Subscription;
constructor(private paginationService: PaginationService) {
}
ngOnInit() {
this.imagesSubs = MeteorObservable.subscribe('images').subscribe();
this.optionsSub = Observable.combineLatest(
this.pageSize,
this.curPage,
this.nameOrder,
this.location
).subscribe(([pageSize, curPage, nameOrder, location]) => {
const options: Options = {
limit: pageSize as number,
skip: ((curPage as number) - 1) * (pageSize as number),
sort: { name: nameOrder as number }
};
this.paginationService.setCurrentPage(this.paginationService.defaultId, curPage as number);
if (this.partiesSub) {
this.partiesSub.unsubscribe();
}
this.partiesSub = MeteorObservable.subscribe('parties', options, location).subscribe(() => {
this.parties = Parties.find({}, {
sort: {
name: nameOrder
}
}).zone();
});
});
this.paginationService.register({
id: this.paginationService.defaultId,
itemsPerPage: 10,
currentPage: 1,
totalItems: this.partiesSize
});
this.pageSize.next(10);
this.curPage.next(1);
this.nameOrder.next(1);
this.location.next('');
this.autorunSub = MeteorObservable.autorun().subscribe(() => {
this.partiesSize = Counts.get('numberOfParties');
this.paginationService.setTotalItems(this.paginationService.defaultId, this.partiesSize);
});
}
removeParty(party: Party): void {
Parties.remove(party._id);
}
search(value: string): void {
this.curPage.next(1);
this.location.next(value);
}
onPageChanged(page: number): void {
this.curPage.next(page);
}
changeSortOrder(nameOrder: string): void {
this.nameOrder.next(parseInt(nameOrder));
}
isOwner(party: Party): boolean {
return this.user && this.user._id === party.owner;
}
ngOnDestroy() {
this.partiesSub.unsubscribe();
this.optionsSub.unsubscribe();
this.autorunSub.unsubscribe();
this.imagesSubs.unsubscribe();
}
}
And let's clean up the PartiesListComponent
, and use the new class PartiesList
as base class for this Component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
import { PaginationService } from 'ng2-pagination';
import { PartiesList } from "../shared-components/parties-list.class";
import template from './parties-list.component.html';
import style from './parties-list.component.scss';
@Component({
selector: 'parties-list',
template,
styles: [ style ]
})
export class PartiesListComponent extends PartiesList {
constructor(paginationService: PaginationService) {
super(paginationService);
}
}
Now let's create a basic view and layout for the mobile platform, be creating a new Component called PartiesListMobile
, starting with the view:
1
2
3
4
5
6
7
8
<ion-header>
<ion-navbar>
<ion-title>Socially</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
Parties!
</ion-content>
And it's Component, which is very similar to the web version, only it uses different template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component } from '@angular/core';
import { PaginationService } from 'ng2-pagination';
import { PartiesList } from "../shared-components/parties-list.class";
import template from './parties-list.component.mobile.html';
@Component({
selector: 'parties-list',
template
})
export class PartiesListMobileComponent extends PartiesList {
constructor(paginationService: PaginationService) {
super(paginationService);
}
}
Now let's add the mobile Component of the parties list to the index file:
1
2
3
4
5
6
7
import {AppMobileComponent} from "./app.component.mobile";
import {PartiesListMobileComponent} from "./parties-list.component.mobile";
export const MOBILE_DECLARATIONS = [
AppMobileComponent,
PartiesListMobileComponent
];
And let's add the Component we just created as rootPage
for our Ionic application:
1
2
3
4
5
6
7
12
13
14
15
16
17
18
19
20
import {Component} from "@angular/core";
import template from "./app.component.mobile.html";
import {MenuController, Platform, App} from "ionic-angular";
import {PartiesListMobileComponent} from "./parties-list.component.mobile";
if (Meteor.isCordova) {
require("ionic-angular/css/ionic.css");
...some lines skipped...
template
})
export class AppMobileComponent {
rootPage: any;
constructor(app: App, platform: Platform, menu: MenuController) {
this.rootPage = PartiesListMobileComponent;
}
}
Now we just need declare this Component as entryComponents
in the NgModule
definition, and make sure we have all the required external modules in the NgModule
that loaded for the mobile:
16
17
18
19
20
21
22
23
24
25
26
27
28
29
36
37
38
39
40
41
42
import { MOBILE_DECLARATIONS } from "./mobile/index";
import { AppMobileComponent } from "./mobile/app.component.mobile";
import { IonicModule, IonicApp } from "ionic-angular";
import { PartiesListMobileComponent } from "./mobile/parties-list.component.mobile";
let moduleDefinition;
if (Meteor.isCordova) {
moduleDefinition = {
imports: [
Ng2PaginationModule,
IonicModule.forRoot(AppMobileComponent)
],
declarations: [
...some lines skipped...
IonicApp
],
entryComponents: [
PartiesListMobileComponent
]
}
}
Now we want to add the actual view to the mobile Component, so let's do it:
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
</ion-navbar>
</ion-header>
<ion-content>
<ion-card *ngFor="let party of parties | async">
<ion-card-content>
<ion-card-title>
{{party.name}}
</ion-card-title>
<p>
{{party.description}}
</p>
</ion-card-content>
<ion-row no-padding>
<ion-col text-right>
<ion-badge>
yes {{party | rsvp:'yes'}}
</ion-badge>
<ion-badge item-center dark>
maybe {{party | rsvp:'maybe'}}
</ion-badge>
<ion-badge item-center danger>
no {{party | rsvp:'no'}}
</ion-badge>
</ion-col>
</ion-row>
</ion-card>
</ion-content>
We used
ion-card
which is an Ionic Component.
And in order to have the ability to load images in the mobile platform, we need to add some logic to the displayMainImage
Pipe, because Meteor's absolute URL is not the same in mobile:
1
2
3
4
5
6
7
18
19
20
21
22
23
24
25
26
27
28
29
import {Pipe, PipeTransform} from '@angular/core';
import { Images } from '../../../../both/collections/images.collection';
import { Party } from '../../../../both/models/party.model';
import { Meteor } from "meteor/meteor";
@Pipe({
name: 'displayMainImage'
...some lines skipped...
const found = Images.findOne(imageId);
if (found) {
if (!Meteor.isCordova) {
imageUrl = found.url;
} else {
const path = `ufs/${found.store}/${found._id}/${found.name}`;
imageUrl = Meteor.absoluteUrl(path);
}
}
return imageUrl;
And let's add the image to the view:
5
6
7
8
9
10
11
</ion-header>
<ion-content>
<ion-card *ngFor="let party of parties | async">
<img *ngIf="party.images" [src]="party | displayMainImage">
<ion-card-content>
<ion-card-title>
{{party.name}}
As you probably notice, there are many warnings about missing fonts. We can easily fix it with the help of a package called mys:fonts
.
$ meteor add mys:fonts
That plugin needs to know which font we want to use and where it should be available.
Configuration is pretty easy, you will catch it by just looking on an example:
1
2
3
4
5
6
7
8
{
"map": {
"node_modules/ionic-angular/fonts/roboto-medium.ttf": "fonts/roboto-medium.ttf",
"node_modules/ionic-angular/fonts/roboto-regular.ttf": "fonts/roboto-regular.ttf",
"node_modules/ionic-angular/fonts/roboto-medium.woff": "fonts/roboto-medium.woff",
"node_modules/ionic-angular/fonts/roboto-regular.woff": "fonts/roboto-regular.woff"
}
}
Now roboto-regular.ttf
is available under http://localhost:3000/fonts/roboto-regular.ttf
.
And... You have an app that works with Ionic!
In this tutorial we showed how to use Ionic and how to separate the whole view for both, web and mobile.
We also learned how to share component between platforms, and change the view only!
We also used Ionic directives in order to provide user-experience of mobile platform instead of regular responsive layout of website.