Fork me on GitHub

Ionic

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

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!

Adding Ionic

Using ionic is pretty simple - first, we need to install it:

$ meteor npm install ionic-sdk --save

To use ionic in our app we have to install angular-sanitize:

$ meteor npm install angular-sanitize --save

Now we've got all of modules. Let's add the first module to Socially:

22.3 NgSanitize as a dependency imports/ui/components/socially/socially.js
1
2
3
4
5
6
7
 
18
19
20
21
22
23
24
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngMaterial from 'angular-material';
import ngSanitize from 'angular-sanitize';
import uiRouter from 'angular-ui-router';
 
import template from './socially.html';
...some lines skipped...
export default angular.module(name, [
  angularMeteor,
  ngMaterial,
  ngSanitize,
  uiRouter,
  PartiesList,
  PartyDetails,

The second one is Ionic. We need to import not one but two files. It should look like this:

22.4 Import ionic and add as a dependency imports/ui/components/socially/socially.js
3
4
5
6
7
8
9
10
 
26
27
28
29
30
31
32
33
import ngMaterial from 'angular-material';
import ngSanitize from 'angular-sanitize';
import uiRouter from 'angular-ui-router';
import 'ionic-sdk/release/js/ionic';
import 'ionic-sdk/release/js/ionic-angular';
 
import template from './socially.html';
import { name as PartiesList } from '../partiesList/partiesList';
...some lines skipped...
  PartyDetails,
  Navigation,
  Auth,
  'accounts.ui',
  'ionic'
]).component(name, {
  template,
  controllerAs: name,

Now's the time to add some style:

5
6
7
8
9
10
11
import uiRouter from 'angular-ui-router';
import 'ionic-sdk/release/js/ionic';
import 'ionic-sdk/release/js/ionic-angular';
import 'ionic-sdk/release/css/ionic.css';
 
import template from './socially.html';
import { name as PartiesList } from '../partiesList/partiesList';

Separate the Socially view

We will do the same thing as we did in previous chapter with Login component.

Let's create a view for the web. We can achieve this by copying the content of socially.html:

22.6 Create view of Socially for web imports/ui/components/socially/web.html
1
2
3
<navigation></navigation>
 
<div ui-view=""></div>

Now let's take care of mobile view:

1
2
3
<ion-nav-bar class="bar-positive" align-title="center">
</ion-nav-bar>
<ion-nav-view></ion-nav-view>

This is a simple navigation layout. As you can see it is pretty similar to the web view.

The ion-nav-view tag is similar to the ui-view tag

Last thing to do is to implement these views:

22.8 Implement web and mobile views imports/ui/components/socially/socially.js
7
8
9
10
11
12
13
14
15
16
 
19
20
21
22
23
24
25
import 'ionic-sdk/release/js/ionic-angular';
import 'ionic-sdk/release/css/ionic.css';
 
import { Meteor } from 'meteor/meteor';
 
import webTemplate from './web.html';
import mobileTemplate from './mobile.html';
import { name as PartiesList } from '../partiesList/partiesList';
import { name as PartyDetails } from '../partyDetails/partyDetails';
import { name as Navigation } from '../navigation/navigation';
...some lines skipped...
class Socially {}
 
const name = 'socially';
const template = Meteor.isCordova ? mobileTemplate : webTemplate;
 
// create a module
export default angular.module(name, [

We will no longer use socially.html, so let's remove it!

By now, the navigation bar is empty, we can change this by adding ionNavTitle:

1
2
3
4
5
6
<ion-nav-bar class="bar-positive" align-title="center">
  <ion-nav-title>
    Socially
  </ion-nav-title>
</ion-nav-bar>
<ion-nav-view></ion-nav-view>

Running mobile app

We will use the techniques we learned in Step 21 of the tutorial and run the project in our favorite emulator, I used Android so

$ meteor run android

Socially is working but the list of parties looks terrible!

Use Ionic in list of parties

The web view stays the same so let's just copy partiesList.html to web.html:

22.11 Create web view of PartiesList imports/ui/components/partiesList/web.html
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
<div layout="column" layout-padding>
  <party-add-button ng-show="partiesList.isLoggedIn"></party-add-button>
 
  <div>
    <h2 class="md-display-1">List of the parties:</h2>
  </div>
 
  <div layout="row" layout-padding>
    <md-input-container>
      <label>Search</label>
      <input ng-model="partiesList.searchText">
    </md-input-container>
 
    <parties-sort on-change="partiesList.sortChanged(sort)" property="name" order="1"></parties-sort>
  </div>
 
  <div layout="column" layout-gt-sm="row">
    <div class="list list-web" flex="50">
      <md-card dir-paginate="party in partiesList.parties | itemsPerPage: partiesList.perPage" total-items="partiesList.partiesCount">
        <md-card-title>
          <md-card-title-text>
            <span class="md-headline" ui-sref="partyDetails({ partyId: party._id })">
              {{party.name}}
              <party-remove party="party"></party-remove>
            </span>
            <span class="md-subhead">{{party.description}}</span>
          </md-card-title-text>
          <md-card-title-media ng-if="party.images">
            <div class="md-media-lg card-media">
              <party-image images="party.images"></party-image>
            </div>
          </md-card-title-media>
        </md-card-title>
        <md-card-content>
          <party-rsvps-list rsvps="party.rsvps"></party-rsvps-list>
 
          <party-unanswered party="party" ng-if="!party.public"></party-unanswered>
          <div ng-if="party.public">
            Everyone is invited
          </div>
 
          <party-creator party="party"></party-creator>
        </md-card-content>
        <md-card-actions>
          <party-rsvp party="party" ng-show="partiesList.isLoggedIn"></party-rsvp>
          <div ng-hide="partiesList.isLoggedIn">
            <i>Sign in to RSVP for this party.</i>
          </div>
        </md-card-actions>
      </md-card>
      <dir-pagination-controls on-page-change="partiesList.pageChanged(newPageNumber)"></dir-pagination-controls>
    </div>
    <div flex="50">
      <parties-map parties="partiesList.parties"></parties-map>
    </div>
  </div>
</div>

For the purpose of tutorial we want to keep mobile version of Socially as simple as possible.

Let's display only name, description, image and list of RSVPs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<ion-view hide-back-button="true">
  <ion-content padding="true" class="has-header">
    <div class="list list-mobile">
      <div class="card" ng-repeat="party in partiesList.parties">
        <div class="item">
          <h2>
            {{party.name}}
          </h2>
          <p>
            {{party.description}}
          </p>
        </div>
 
        <div class="item item-image">
          <party-image images="party.images"></party-image>
        </div>
 
        <party-rsvps-list class="item" rsvps="party.rsvps"></party-rsvps-list>
      </div>
    </div>
  </ion-content>
</ion-view>

Ok! We have both views. It's time to implement them and remove the old partiesList.html file:

4
5
6
7
8
9
10
11
12
13
 
73
74
75
76
77
78
79
import utilsPagination from 'angular-utils-pagination';
 
import { Counts } from 'meteor/tmeasday:publish-counts';
import { Meteor } from 'meteor/meteor';
 
import webTemplate from './web.html';
import mobileTemplate from './mobile.html';
import { Parties } from '../../../api/parties';
import { name as PartiesSort } from '../partiesSort/partiesSort';
import { name as PartiesMap } from '../partiesMap/partiesMap';
...some lines skipped...
}
 
const name = 'partiesList';
const template = Meteor.isCordova ? mobileTemplate : webTemplate;
 
// create a module
export default angular.module(name, [

Since the layout of the view has changed, it's a bit broken and not functioning well. For example, in the mobile view we have a terribly looking margin at the top of the first party item, and in the web view we can't scroll the parties list. Here are some few adjustments which will make the layout work properly again:

22.15 Fix layout to match both mobile and web imports/ui/components/partiesList/partiesList.less
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
parties-list {
  > div:first-child {
    height: calc(~"100vh - 64px");
  }
 
  [ui-sref] {
    cursor: pointer;
  }
 
  .list-web {
    overflow-y: visible;
    overflow-x: hidden;
  }
 
  .list-mobile > .card:first-child {
    margin-top: 0;
  }
}
 
@import "../partiesMap/partiesMap.less";

You've probably noticed an issue with images. It happens because jalik:ufs package saves an absolute path of a file. So if you uploaded an image in the browser its path will contain http://localhost:3000.

But hey! We're on the mobile app so the port is different.

We can fix it by creating a filter. Let's call it DisplayImageFilter:

22.16 Create DisplayImageFilter imports/ui/filters/displayImageFilter.js
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
import angular from 'angular';
 
import { Meteor } from 'meteor/meteor';
 
const name = 'displayImageFilter';
 
function DisplayImageFilter(image) {
  if (!image) {
    return image;
  }
 
  // leave it as it is for the web view
  if (!Meteor.isCordova) {
    return image.url;
  }
 
  // create new path of an image
  const path = `ufs/${image.store}/${image._id}/${image.name}`;
  return Meteor.absoluteUrl(path);
}
 
// create a module
export default angular.module(name, [])
  .filter(name, () => {
    return DisplayImageFilter;
  });

Done, we have it! Now we want to use it:

22.17 Add new filter to PartyImage imports/ui/components/partyImage/partyImage.js
3
4
5
6
7
8
9
 
27
28
29
30
31
32
33
34
 
import template from './partyImage.html';
import { Images } from '../../../api/images';
import { name as DisplayImageFilter } from '../../filters/displayImageFilter';
 
class PartyImage {
  constructor($scope, $reactive) {
...some lines skipped...
 
// create a module
export default angular.module(name, [
  angularMeteor,
  DisplayImageFilter
]).component(name, {
  template,
  bindings: {
22.18 Implement DisplayImageFilter imports/ui/components/partyImage/partyImage.html
1
<img ng-src="{{partyImage.mainImage | displayImageFilter}}"/>

And... we're done!

Summary

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.