Ionicハンズオン資料

# はじめに

こんにちは！！

では、今からIonicを用いてWebアプリケーションを作成していこうと思います。
時間がないので、サクサクと進めていきますね！

---

もっとじっくり学びたい？
そういう方にはオススメの本がありますので、こちらでじっくり学んでくださいね :-p

<a target="_blank" href="https://www.amazon.co.jp/gp/product/4863542364/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4863542364&linkCode=as2&tag=kozake0a-22&linkId=442f907d8f963b003783b727762ba90b">Ionicで作る モバイルアプリ制作入門〈Web/iPhone/Android対応〉</a><img src="//ir-jp.amazon-adsystem.com/e/ir?t=kozake0a-22&l=am2&o=9&a=4863542364" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

<a target="_blank"  href="https://www.amazon.co.jp/gp/product/4863542364/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4863542364&linkCode=as2&tag=kozake0a-22&linkId=e0599655f1fd6adf88b3c3b3e8fa0bf9"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=JP&ASIN=4863542364&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL250_&tag=kozake0a-22" ></a><img src="//ir-jp.amazon-adsystem.com/e/ir?t=kozake0a-22&l=am2&o=9&a=4863542364" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

---

2019年11月27日にIonicの第2版が発売されました🎉！！
本記事はIonic v3の内容に基づいています。
より洗練されたIonic v4に触れたい、学びたい方はこちらでどうぞ！

<a target="_blank" href="https://www.amazon.co.jp/gp/product/4863542925/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4863542925&linkCode=as2&tag=kozake0a-22&linkId=c2f5c151c7ae91c142e1ee610e3af7ac">Ionicで作る モバイルアプリ制作入門[Angular版]&lt;Web/iPhone/Android対応&gt;</a><img src="//ir-jp.amazon-adsystem.com/e/ir?t=kozake0a-22&l=am2&o=9&a=4863542925" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

<a target="_blank"  href="https://www.amazon.co.jp/gp/product/4863542925/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4863542925&linkCode=as2&tag=kozake0a-22&linkId=6ab43f6eb9c53e5f9500c0f21e439da8"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=JP&ASIN=4863542925&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL250_&tag=kozake0a-22" ></a><img src="//ir-jp.amazon-adsystem.com/e/ir?t=kozake0a-22&l=am2&o=9&a=4863542925" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />



---
## 自己紹介

- こざけ （Twitter: @s_kozake）
- システムアーキテクト
- Java屋
- 🍻(´∀｀*v)

---
## 今から作るもの

「イベントさん」という名前のWebアプリケーションです。
connpassで公開しているイベントを任意のキーワードで検索したり、お気に入りしたりすることができるWebアプリケーションです。

---

アプリケーションの構成イメージです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/101517/92c68763-c688-cc6c-87ad-b75e4f23e926.png)

---

本日作るWebアプリケーションの出来上がりとは少し違いますが、このアプリを作ろうとした時に作成したイメージ図がこちらです。

---
**イベント検索画面**

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/b3290e8e-d60d-ebee-bf96-ea9f25712d2c.png" width=70%>

---
**イベント検索画面**

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/2af0879a-635a-1975-e973-26af7423749c.png" width=70%>

---
**イベント検索画面**

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/53d9a79c-d771-b5cc-f8f3-01a8bf7ab130.png" width=70%>

---
**ブックマーク画面**

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/028e2006-7042-cbc1-07e4-0f13f14f23eb.png" width=70%>

---
**ブックマーク画面**

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/efc74e98-e1ab-2fb4-a00b-c27922d80e0b.png" width=70%>

---
**Gitリポジトリ**

出来上がりのソースコードはここにあります。

https://bitbucket.org/kozake/event-san

---
# プロジェクト作成 - Ionic Start

では、プロジェクトを作成していきましょう！
ターミナル、もしくはコマンドプロンプトを起動して、任意のディレクトリで次のコマンドを実行してください。

```
$ ionic start event-san
```

---

コマンドを実行すると、   
「どの雛形を使うか」   
「iOSやAndroidなどのネイティブをターゲットにするか」   
を聞かれますので、次のとおり選択してください。

- What starter would you like to use: `blank`
- Would you like to integrate your new app with Cordova to target native iOS and Android? (y/N) `N`

これでプロジェクトの雛形が出来ました！

`ionic start`コマンドで、このように手軽にプロジェクトの雛形を作成することが出来ます。

---

では、画面を立ち上げて見ましょう。
event-sanディレクトリに移って、`ionic serve`コマンドを実行してください。

```
$ cd ./event-san
$ ionic serve
```

---

次の画面が立ち上げれば、成功です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/5f0b2571-9aa9-fbf9-0c4a-0bd594233f67.png" width=50%>

---

# タブの作成

では、画面の雛形を作成していきます。

---

## タブの生成

次のコマンドを実行して、タブを生成します。

```
$ ionic g tabs event
```

---

作成するタブの数とタブの名前を聞かれますので、次のとおり入力してください。

- How many tabs? `2`
- Name of this tab: `search`
- Name of this tab: `bookmark`

---

成功したら、コミットしましょう。

```
$ git add .
$ git commit -m "add tabs"
```

---

## ホーム画面の変更

起動時に表示されるホーム画面を、先ほど作成したタブ画面と差し替えます。
MyAppクラスのrootPageを、HomePageからEventPageに変更してください。

```typescript:src/app/app.component.ts
export class MyApp {
  rootPage:any = "EventPage";

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) {
```

---

修正後に`ionic serve`コマンドを実行して、次の画面が表示されれば修正成功です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/7e99012e-43f7-00f8-b1a3-3b2aa58efd2f.png" width=50%>

---

成功したら、コミットしましょう。

```
$ git add .
$ git commit -m "change homepage"
```
---

## change icon & title

画面のアイコンとタイトルを変更します。
まずはタイトルから変更します。   
ソースコードに次の修正を加えてください。

```html:src/pages/bookmark/bookmark.html
  <ion-navbar>
    <ion-title>ブックマーク</ion-title>
  </ion-navbar>
```

```html:src/pages/search/search.html
  <ion-navbar>
    <ion-title>検索</ion-title>
  </ion-navbar>
```

---

次に画面のアイコンを変更します。   
デフォルトでは、tabTitleの値が画面のURLとしても使用されます。
tabTitleを日本語にしたことで、URLがわかりづらいものになるので、明示的にtabUrlPathを指定してください。

```html:src/pages/event/event.html
<ion-tabs>
    <ion-tab [root]="searchRoot" tabTitle="検索" tabIcon="search" tabUrlPath="search-root"></ion-tab>
    <ion-tab [root]="bookmarkRoot" tabTitle="ブックマーク" tabIcon="bookmark" tabUrlPath="bookmark-root"></ion-tab>
</ion-tabs>
```

---

修正された画面が正しく表示されたら、コミットしましょう。

```
$ git add .
$ git commit -m "change icon & title"
```
---

# イベント検索画面の作成

では、イベント検索画面を作成していきましょう！

---

## 検索バーを追加

イベント検索画面に検索バーを追加します。
次の修正を加えてください。

```html:src/pages/search/search.html
<ion-content padding>
  <div class="searchbar" ion-fixed>
    <form action="" (keydown.enter)="getEvents($event)">
        <ion-searchbar name="keywords" [(ngModel)]="keywords" placeholder="キーワード検索"></ion-searchbar>
    </form>
  </div>

</ion-content>
```

`ion-content`は、画面のメインコンテンツを配置する場所です。   
ここに配置した内容は、通常画面サイズに応じてスクロールするのですが、ion-fixedをつけることで、画面の固定位置にコンポーネントを配置することが出来ます。
検索バーは常に同じ位置に表示したいので、この指定をしています。

---

画面個別のスタイル定義は、その画面のscssに記述すると、他の画面に影響しないので便利です。
ここでは、検索バーのスタイルを定義します。

```scss:src/pages/search/search.scss
 page-search {
    .searchbar {
        width: 100%;
        height: 50px;
    }
 }
```

---

検索バーの入力値を保持する`keywords`プロパティと、検索バーで検索された場合に動作する`getEvents`メソッドを追加しています。
今はまだ、検索された場合の動作はログ出力するだけです。

```typescript:src/pages/search/search.ts
export class SearchPage {

  keywords: string = "";

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad SearchPage');
  }

  getEvents(ev) {
    console.log(this.keywords);
  }
```

---


次のように画面が表示されれば修正成功です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/8027a9e8-a3f0-2c99-c976-cd4b2be5a3ce.png" width=50%>

---

成功したら、コミットしましょう。

```
$ git add .
$ git commit -m "add search bar"
```

---

## イベントプロバイダーの作成

次にイベントプロバイダーを作成します。
イベントプロバイダーは、connpassのAPIを用いて、任意のキーワードでイベントを検索します。
このハンズオンアプリの中核とも言える機能です。

---

では、Ionicのコマンドを用いてイベントプロバイダーの雛形を生成します。
次のコマンドを実行してください。

```
ionic g provider event
```

---

成功したら、コミットしましょう。

```
$ git add .
$ git commit -m "create EventProvider"
```

---

## 検索処理の実装

では、イベントの検索処理を実装していきます。
イベントプロパイダーはHTTP通信するため、`HttpClientModule`のインポートが必要です。

`app.module.ts`にモジュールを追加してください。

```typescript:src/app/app.module.ts
import { HttpClientModule } from '@angular/common/http';
```

```typescript:src/app/app.module.ts
  imports: [
    BrowserModule,
    HttpClientModule,
    IonicModule.forRoot(MyApp)
  ],
```

---

イベントプロバイダーを修正します。
HTTPパラメータをconnpassのAPIに指定するので、`HttpParams`と、その戻り値を受け取る`Observable`をimport定義しています。

```typescript:src/providers/event/event.ts
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';

/*
  Generated class for the EventProvider provider.
```

---

searchメソッドでは、

`order`、`count`、`keyword` をHTTPパラメータに指定しています。
それぞれの詳細は、connpassのAPIリファレンスをご覧ください。

https://connpass.com/about/api/


```typescript:src/providers/event/event.ts
    console.log('Hello EventProvider Provider');
  }

  search(keywords: string[]): Observable<Object> {

    let params = new HttpParams()
      .append('order', '2')
      .append('count', '100');
    keywords.forEach(kwd => params = params.append('keyword', `${kwd}`));

    return this.http.get("/connpass/api", { params: params });
  }
}
```

---

## プロキシ設定

イベントプロバイダーは、`/connpass/api`にアクセスしています。
connpassのAPIリファレンスでは、リクエスト先URLとして、
https://connpass.com/api/v1/event/
となっているので、これはおかしいですね。

実は、このハンズオンアプリから直接このリクエスト先URLにアクセスすると、クロスオリジン制約に引っかかってしまいエラーとなります。
そこで、Ionicの開発用サーバに備わっているプロキシ機能を用いて、クロスオリジン制約を回避します。

---
イメージは次の通りです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/a1158557-40ad-90eb-6d91-622225ed202a.png" width=100%>

---

`ionic.config.json`に次の設定を追加してください。

```json:ionic.config.json
  "name": "event-san",
  "app_id": "",
  "type": "ionic-angular",
  "integrations": {},
  "proxies": [
    {
      "path": "/connpass/api",
      "proxyUrl": "https://connpass.com/api/v1/event"
    }  
  ]
 }
```
この設定により、`/connpass/api`へのリクエストを`https://connpass.com/api/v1/event`にプロキシすることが出来ます。

---

ここまで出来たら、コミットしましょう。

```
$ git add .
$ git commit -m "implements search method"
```

---

## connpassイベントの取得とイベントのリスト表示

イベントプロバイダーの実装が出来たので、実際に検索したイベントを画面に表示してみましょう！

検索画面のソースコードは、大部分の箇所が修正となります。

---

検索結果のイベントを保持する、`events`プロパティを追加しています。

```typescript:src/pages/search/search.ts
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, LoadingController } from 'ionic-angular';
import { EventProvider } from '../../providers/event/event';


@IonicPage()
@Component({
  selector: 'page-search',
  templateUrl: 'search.html',
})
export class SearchPage {

  keywords: string = "";
  events: any[] = [];

```

---

**注意事項**

今回は時間の制約上、eventの型をanyにしていますが、本格的な開発ではany型の使用は避けてください。
どのように型定義するかについては、
「Ionicで作る モバイルアプリ制作入門」の5章を参考にしてください。

---

また、通信中の画面ロックをする`LoadingController`と、作成した`EventProvider`をDIを経由して取得するために、コンストラクタの引数に追加します。

```typescript:src/pages/search/search.ts

  constructor(
      public navCtrl: NavController,
      public navParams: NavParams,
      public loadingCtrl: LoadingController,
      public eventProvider: EventProvider,
    ) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad SearchPage');
  }

```

---

`getEvents`メソッドでは、入力されたキーワードでイベントプロバイダーを呼び出しています。
また、それ以外にも検索中のローディング画面の表示処理もここで行なっています。

```typescript:src/pages/search/search.ts
  getEvents(ev) {
    const searchKeywords:string = this.keywords.trim();
    
    if (!searchKeywords) return;

    const loader = this.loadingCtrl.create({
      content: "Please wait..."
    });
    loader.present();

    const kwds = searchKeywords.split(' ').filter(v => v !== "");
    this.eventProvider.search(kwds).subscribe((body: any) => {
      if (body && body.events) {
        if (this.keywords === searchKeywords) {
          this.events = body.events;
        }
      }
      loader.dismiss();
    }, (error: any) => {
      loader.dismiss();
    })
  }
```

---

検索画面の全ソースコードを次に示します。

```typescript:src/pages/search/search.ts
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, LoadingController } from 'ionic-angular';
import { EventProvider } from '../../providers/event/event';

/**
 * Generated class for the SearchPage page.
 *
 * See https://ionicframework.com/docs/components/#navigation for more info on
 * Ionic pages and navigation.
 */

@IonicPage()
@Component({
  selector: 'page-search',
  templateUrl: 'search.html',
})
export class SearchPage {

  keywords: string = "";
  events: any[] = [];

  constructor(
      public navCtrl: NavController,
      public navParams: NavParams,
      public loadingCtrl: LoadingController,
      public eventProvider: EventProvider,
    ) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad SearchPage');
  }

  getEvents(ev) {
    const searchKeywords:string = this.keywords.trim();
    
    if (!searchKeywords) return;

    const loader = this.loadingCtrl.create({
      content: "Please wait..."
    });
    loader.present();

    const kwds = searchKeywords.split(' ').filter(v => v !== "");
    this.eventProvider.search(kwds).subscribe((body: any) => {
      if (body && body.events) {
        if (this.keywords === searchKeywords) {
          this.events = body.events;
        }
      }
      loader.dismiss();
    }, (error: any) => {
      loader.dismiss();
    })
  }
}

```

---

検索されたイベントをリスト表示するように、`ion-list`を追加します。

```html:src/pages/search/search.html
        <ion-searchbar name="keywords" [(ngModel)]="keywords" placeholder="キーワード検索"></ion-searchbar>
    </form>
  </div>
  <ion-list class="event-list">
    <ng-container *ngFor="let event of events">
      <button ion-item>
        <h2>{{event.title}}</h2>
        <h3>{{event.catch}}</h3>
        <h4>開催場所:</h4>
        <p>{{event.address}} {{event.place}}</p>
        <h4>イベント日時:</h4>
        <p>{{event.started_at}}〜{{event.ended_at}}</p>
      </button>
    </ng-container>
  </ion-list>
```

---

修正が終われば、`ionic serve`コマンドを実行して、イベントを検索してみましょう！

修正が成功していると、次のような画面となります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/2bf4aa82-45fd-236f-c9e8-d43bfac9435a.png" width=50%>

---

ここまでの修正をコミットしましょう。

```
$ git add .
$ git commit -m "get event & search list"
```

---

## 見た目の修正

現時点で、検索バーの後ろにイベントが隠れていたり、微妙にイベントタイトルの上部が切れてたりします。
見た目を修正します。

---

`app.scss`に次の修正を加えてください。

```scss:/app/app.scss
// To declare rules for a specific mode, create a child rule
// for the .md, .ios, or .wp mode classes. The mode class is
// automatically applied to the <body> element in the app.

[padding] h1:first-child, [padding] h2:first-child, [padding] h3:first-child, 
[padding] h4:first-child, [padding] h5:first-child, [padding] h6:first-child { 
    margin-top: 0;
}

ion-label {
    white-space: normal;
}
```

アプリケーション全体に対する見た目の修正は、`app.scss`で行えばいいかと思います。

---


検索バーの表示分、イベント一覧のマージンをとります。
検索画面のみのスタイル変更のため、`search.scss`に次の修正を加えます。

```scss:src/pages/search/search.scss
        width: 100%;
        height: 50px;
    }

    .event-list {
        margin-top: 30px;
    }
}
```

---

画面の見た目が綺麗に表示できることが確認できたら、コミットしましょう。

```
$ git add .
$ git commit -m "modify css"
```

---

# イベント詳細画面の作成

では、イベントの詳細を表示する、イベント詳細画面を作成していきます。

---

## イベント詳細画面の作成

イベント詳細画面の雛形を作成します。
次のコマンドを実行してください。

```
$ ionic g page EventDetail
```

---

イベント詳細画面の雛形が作成できたら、コミットしましょう。

```
$ git add .
$ git commit -m "add event-detail page"
```

---

## イベント詳細画面の実装

イベント詳細画面を実装していきます。
まずはタイトルの修正。

```html:src/pages/event-detail/event-detail.html

<ion-header>

  <ion-navbar>
    <ion-title>イベント詳細</ion-title>
  </ion-navbar>

</ion-header>
```

---
次にコンテンツエリアの修正です。

```html:src/pages/event-detail/event-detail.html
<ion-content padding>

  <ng-container *ngIf="event">
    <h1><a [href]="event.event_url" target="_blank">{{event.title}}</a></h1>
    <p>{{event.catch}}</p>
    <h1 class="event-header">開催場所:</h1>
    <p>{{event.address}} {{event.place}}</p>
    <h1 class="event-header">イベント日時:</h1>
    <p>{{event.started_at}}〜{{event.ended_at}}</p>
    <h1 class="event-header">概要:</h1>
    <div [innerHTML]="event.description"></div>
  </ng-container>

</ion-content>
```

`event.description`には、HTMLが設定されていますので、少し特殊な記述をしています。
詳細を知りたい方は、こちらの記事などで確認してください。

https://www.buildinsider.net/web/angulartips/007

---
イベント詳細画面の見た目を修正します。

```scss:src/pages/event-detail/event-detail.scss
page-event-detail {
    .event-header { 
        border-left: solid 7px color($colors, primary);
        border-bottom: solid 1px color($colors, primary);
        padding-left: 5px;
    }
}
```

---

イベント詳細画面はイベント検索画面から呼ばれます。
その際、イベント検索画面から、`NavParams`経由で選択されたイベントオブジェクトを受け取ることが出来ます。

次のコードでは、`ionViewDidLoad`メソッドでイベントオブジェクトを受け取っています。
`ionViewDidLoad`はIonicのページのライフサイクルで呼ばれるメソッドの一つで、画面初期時に呼ばれます。
詳細は、「Ionicで作る モバイルアプリ制作入門」の「イベントとライフサイクル」を確認してくださいね。

```typescript:src/pages/event-detail/event-detail.ts
export class EventDetailPage {

  event: any;

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad EventDetailPage');
    this.event = this.navParams.data.event;
  }

}
```

---

では、イベント検索画面のリストをクリック時に、`openEvent`メソッド経由でイベント詳細画面を呼び出す処理を記述します。

```html:src/pages/search/search.html
  </div>
  <ion-list class="event-list">
    <ng-container *ngFor="let event of events">
      <button ion-item (click)="openEvent(event)">
        <h2>{{event.title}}</h2>

```

```typescript:src/pages/search/search.ts

  openEvent(event) {
    this.navCtrl.push('EventDetailPage', {
      event: event
    });
  }
}
```

`NavController`の`push`メソッドを用いて、イベント詳細画面を呼び出すことが出来ます。
その際に、選択されたイベントオブジェクトを渡しています。

---

では、`ionic serve`コマンドを実行して、検イベント詳細画面を表示できるか確認しましょう。
正しく動作すれば、次の画面が表示されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/aad6b82f-3a23-4ba2-2dd8-ef508196baa1.png" width=50%>

---

画面が表示出来たら、修正をコミットしましょう。

```
$ git add .
$ git commit -m "implements event-detail page"
```

---

## イベント詳細ページをブックマーカブルに

ブックマーカブルとは、ある画面をブックマークして、URLで直接呼び出せる画面特性のことです。
今のイベント詳細ページは、表示するイベント情報をイベント検索画面からオンメモリで受けてますので、直接URLで画面遷移すると、なにも表示されません。

そこで、イベント詳細ページをブックマーカブルにします。

---

まずはイベントプロバイダを修正します。
イベントプロバイダにイベントIDを指定してイベント情報を取得するAPI呼び出しを追加します。

```typescript:src/providers/event/event.ts

    return this.http.get("/connpass/api", { params: params });
  }

  get(eventId: string): Observable<Object> {

    let params = new HttpParams()
      .append('event_id', `${eventId}`);

    return this.http.get("/connpass/api", { params: params });
  }
}
```

---

次に、イベント検索画面からイベント詳細画面の呼び出しの際に、イベントIDもパラメータに追加します。

```typescript:src/pages/search/search.ts
  openEvent(event) {
    this.navCtrl.push('EventDetailPage', {
      eventId: event.event_id,
      event: event
    });
  }
```

---

最後に、イベント詳細画面の修正です。
画面のURLに指定されたイベントIDが含まれるよう、IonicPageでデコレータで指定します。

```typescript:src/pages/event-detail/event-detail.ts
 * Ionic pages and navigation.
 */

@IonicPage({
  segment: 'event-detail/:eventId'
})
@Component({
  selector: 'page-event-detail',
  templateUrl: 'event-detail.html',
```

---

最後にイベント詳細画面を修正します。
イベント情報が`navParams`に含まれない場合（URLで直接画面遷移した場合）、URLに指定されたイベントIDからイベント情報を取得するよう修正します。

```typescript:src/pages/event-detail/event-detail.ts
import { EventProvider } from '../../providers/event/event';

/**
 * Generated class for the EventDetailPage page.
```

```typescript:src/pages/event-detail/event-detail.ts

  event: any;

  constructor(
    public navCtrl: NavController, 
    public navParams: NavParams,
    public eventProvider: EventProvider,
  ) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad EventDetailPage');
    this.event = this.navParams.data.event;
    if (!this.event) {
      this.eventProvider.get(this.navParams.data.eventId).subscribe((body: any) => {
        if (body && body.events && body.events.length > 0) this.event = body.events[0];
      });
    }
  }

}
```

---

URL指定で直接画面遷移できたら、ここまでの修正をコミットしましょう。

```
$ git add .
$ git commit -m "bookmarkable event-detail page"
```

---

# ブックマーク画面の作成

検索したイベントをブックマークして、後からブックマーク一覧を確認できる画面を作成していきます。

---

## ブックマークプロバイダの作成

ブックマークはブラウザの`Web Storage`に保存します。
`Web Storage`とのやりとりを行うブックマークプロバイダを作成しましょう。

次のコマンドを実行してください。

```
ionic g provider bookmark
```

---

無事生成出来たら、コミットしましょう。

```
$ git add .
$ git commit -m "create BookmarkProvider
```

---

## ブックマーク機能の実装

`Web Storage`を用いるために、IonicStorageModuleを組み込みます。
次の通り修正してください。

```typescript:src/app/app.module.ts
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { IonicStorageModule } from '@ionic/storage';
import { SplashScreen } from '@ionic-native/splash-screen';
```

```typescript:src/app/app.module.ts
  imports: [
    BrowserModule,
    HttpClientModule,
    IonicModule.forRoot(MyApp),
    IonicStorageModule.forRoot(),
  ],
  bootstrap: [IonicApp],
  entryComponents: [
```

---

ブックマークプロバイダはほぼ作り変えなので、ソースコードをいかに配置しました。
置き換えてください。
ローカルストレージの取得(get)・追加/更新(put)・削除(delete)を実装しています。

```typescript:src/providers/bookmark/bookmark.ts
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';

@Injectable()
export class BookmarkProvider {

  constructor(public storage: Storage) {}

  get() {
    return this.storage.get("bookmark.events").then(events => {
      return events ? events : {};
    });
  }

  put(event: any) {
    return this.get().then(events => {
      events[event.event_id] = event;
      return this.storage.set("bookmark.events", events);
    })
  }

  delete(event: any) {
    return this.get().then(events => {
      delete events[event.event_id];
      return this.storage.set("bookmark.events", events);
    })
  }
}
```

---

では、イベント詳細画面にブックマーク機能を追加します。
イベント詳細画面のヘッダ部にブックマークボタンを設けます。

```html:src/pages/event-detail/event-detail.html

  <ion-navbar>
    <ion-title>イベント詳細</ion-title>
    <ion-buttons end>
      <button ion-button icon-start (click)="doBookmark()">
        <ion-icon name="bookmark"></ion-icon>
        ブックマーク
      </button>
    </ion-buttons>
  </ion-navbar>

</ion-header>
```

---

ブックマークボタンが押された時の処理を、イベント詳細画面に実装します。
ブックマークプされると、トーストメッセージを表示する機能も追加するので、　`BookmarkProvider`の他に`ToastController`も取得します。

```typescript:src/pages/event-detail/event-detail.ts
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, ToastController } from 'ionic-angular';
import { EventProvider } from '../../providers/event/event';
import { BookmarkProvider } from '../../providers/bookmark/bookmark';
 ```

```typescript:src/pages/event-detail/event-detail.ts
    public navCtrl: NavController, 
    public navParams: NavParams,
    public eventProvider: EventProvider,
    public toastCtrl: ToastController,
    public bookmarkProvider: BookmarkProvider
  ) {
  }
```

---

ブックマークボタンが押されると、イベントを`Web Storage`に保存すると共に、トーストメッセージを表示します。

```typescript:src/pages/event-detail/event-detail.ts

  doBookmark() {
    this.bookmarkProvider.put(this.event).then(() => {
      const toast = this.toastCtrl.create({
        message: 'イベントをブックマークしました。',
        duration: 1500
      });
      toast.present();
    })
  }
}
```

---

ここまでの変更をコミットしましょう。

```
$ git add .
$ git commit -m "implements bookmark fuction"
```

---

## ブックマーク画面の実装

ブックマーク画面を実装します。
イベントの一覧をブックマークプロバイダから取得する以外は、ほぼイベント検索画面と同じです。

ただし、イベント検索画面とは異なり、`ion-item-sliding`を用いて、右スライドでブックマーク削除できる機能を追加しています。

---

```html:src/pages/bookmark/bookmark.html
<ion-content padding>
  <ion-list>
    <ion-item-sliding #itemSliding *ngFor="let event of events">
      <button ion-item (click)="openEvent(event)">
        <h2>{{event.title}}</h2>
        <h3>{{event.catch}}</h3>
        <h4>開催場所:</h4>
        <p>{{event.address}} {{event.place}}</p>
        <h4>イベント日時:</h4>
        <p>{{event.started_at}}〜{{event.ended_at}}</p>
      </button>
      <ion-item-options side="right">
        <button ion-button color="danger" icon-start (click)="doDelete(event, itemSliding)">
          <ion-icon name="trash"></ion-icon>
          削除
        </button>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
</ion-content>
```

---

ブックマーク画面は他の画面でブックマーク情報が更新されるため、画面が表示されるたびブックマーク情報を再取得します。
そのため、再取得は`ionViewWillEnter`メソッドで実装しています。
イベント検索画面とは、この部分が異なるのでご注意ください。
詳細は、「Ionicで作る モバイルアプリ制作入門」の「イベントとライフサイクル」を確認してください。

---

```typescript:src/pages/bookmark/bookmark.ts
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, ItemSliding, ToastController } from 'ionic-angular';
import { BookmarkProvider } from '../../providers/bookmark/bookmark';

@IonicPage()
@Component({
  selector: 'page-bookmark',
  templateUrl: 'bookmark.html',
})
export class BookmarkPage {

  events: any[] = [];

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    public toastCtrl: ToastController,
    public bookmarkProvider: BookmarkProvider
){}

  ionViewWillEnter() {
    console.log('ionViewWillEnter BookmarkPage');
    this.bookmarkProvider.get().then(events => {
      this.events = this.toEventArray(events);
    });
  }

  private toEventArray(events): Array<any> {
    const eventArray = [];
    Object.keys(events).forEach(key => {
      eventArray.push(events[key]);
    })
    eventArray.sort((ev1, ev2) => {
      let ret = ev2.started_at.localeCompare(ev1.started_at);
      if (ret !== 0) return ret;
      return ev2.event_id - ev1.event_id;
    })
    return eventArray;
  }

  openEvent(event) {
    this.navCtrl.push('EventDetailPage', {
      eventId: event.event_id,
      event: event
    });
  }

  doDelete(event:any, itemSliding: ItemSliding) {
    this.bookmarkProvider.delete(event).then(events => {
      this.events = this.toEventArray(events);
      const toast = this.toastCtrl.create({
        message: 'ブックマークを削除しました。',
        duration: 1500
      });
      toast.present();
    })
    itemSliding.close();
  }
}
```

---

`ionic serve`コマンドを実行して、期待通りにブックマーク画面が動作することを確認してください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/08bc9ce5-89f6-76af-1452-6008f35fcfd3.png" width=50%>

---

確認が出来たら、変更をコミットしましょう。

```
$ git add .
$ git commit -m "implements bookmark page"
```

---

# 仕上げ

さて、アプリケーションの仕上げにかかります。

---

## タイトルの修正

ずっと放置していましたが、タイトルを修正しましょう。

```html:src/index.html
  <meta charset="UTF-8">
  <title>イベントさん</title>
```

---

また、iPhoneのSafariでは、ホーム画面にWebページのリンクを追加することが出来ます。
ホーム画面に追加するアイコンとタイトルを、`apple-touch-icon`と`apple-mobile-web-app-title`で指定できます。

```html:src/index.html
  <meta name="apple-mobile-web-app-status-bar-style" content="black">

  <!-- ホーム画面追加時のアイコン -->
  <link rel="apple-touch-icon" href="assets/imgs/logo.png">
  
  <!-- ホーム画面追加時のタイトル -->
  <meta name="apple-mobile-web-app-title" content="イベントさん" />

  <!-- cordova.js required for cordova apps (remove if not needed) -->

```

---

修正が完了したら、コミットしましょう。

```
$ git add .
$ git commit -m "modify title and more"
```

---

## タイトルカラーの変更

画面の色がすこし寂しいので、タイトルに色をつけましょう！
Ionicではアプリケーションのテーマカラーを`variables.scss`で変数定義しています。
この定義をアプリケーション内で用いることが出来ます。

ここでは新たに、`search`と`bookmark`のテーマカラーを用意しました。

```scss:src/theme/variables.scss
$colors: (
  primary:    #488aff,
  secondary:  #32db64,
  danger:     #f53d3d,
  light:      #f4f4f4,
  dark:       #222,
  search:     #4682B4,
  bookmark:   #FFF59D
);
```

---

定義したテーマ色を、各画面のタイトルバーに指定します。

```html:src/pages/bookmark/bookmark.html
<ion-header>

  <ion-navbar>
  <ion-navbar color="bookmark">
    <ion-title>ブックマーク</ion-title>
  </ion-navbar>
```

```html:src/pages/search/search.html
<ion-header>

  <ion-navbar>
  <ion-navbar color="search">
    <ion-title>検索</ion-title>
  </ion-navbar>
```

---

`ionic serve`コマンドを実行して、期待通りにタイトル色が変更されたか確認してください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/bd228aa0-2b97-492b-83a1-cab1ccd91770.png" width=50%>

---

確認を終えたら、変更をコミットしましょう。

```
$ git add .
$ git commit -m "title color"
```

---

## 日付表示の修正

日付の表示形式が、`2018-03-20T13:00:00+09:00`とISO8601形式になっています。
このままでは見にくいので、Angularの`DatePipe`を用いて、見やすい形式に変更します。

```html:src/pages/search/search.html
        <h4>イベント日時:</h4>
        <p>
          {{event.started_at | date: 'longDate'}}{{event.started_at | date: 'shortTime'}}
          〜
          {{event.ended_at | date: 'longDate'}}{{event.ended_at | date: 'shortTime'}}
        </p>
      </button>
```

---

```html:src/pages/bookmark/bookmark.html
        <h4>イベント日時:</h4>
        <p>
          {{event.started_at | date: 'longDate'}}{{event.started_at | date: 'shortTime'}}
          〜
          {{event.ended_at | date: 'longDate'}}{{event.ended_at | date: 'shortTime'}}
        </p>
      </button>
```

```html:src/pages/event-detail/event-detail.html
    <h1 class="event-header">イベント日時:</h1>
    <p>
      {{event.started_at | date: 'longDate'}}{{event.started_at | date: 'shortTime'}}
      〜
      {{event.ended_at | date: 'longDate'}}{{event.ended_at | date: 'shortTime'}}
    </p>
    <h1 class="event-header">概要:</h1>
```

---

この修正のみだと、`March 20, 20181:00 PM`と英語圏の日付形式で表示されます。
ロケールを日本に変更して日本語表記にしましょう！
以下の修正を加えます。

```typescript:src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule, LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
```

```typescript:src/app/app.module.ts
import localeJa from '@angular/common/locales/ja';

registerLocaleData(localeJa);
```

```typescript:src/app/app.module.ts
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    {provide: LOCALE_ID, useValue: navigator.language},
    EventProvider,
```

---

日付表記が`2018年3月20日13:00`と日本語表記になったことを確認出来たら、コミットしてください。

```
$ git add .
$ git commit -m "use DatePipe"
```

---

# リリース

やったぜ！アプリが完成しました！！   
では、世の中にリリースしてIonicの実力を思い知らせてやりましょうwww！

Netlifyへ作成したアプリのリリースをします。

---

## NetlifyのProxying機能

その前に、NetlifyのProxying機能を使って、connpassへのアクセスをプロキシできるようにします。   
Netlifyでは、HTTPリクエストをリダイレクトしたり、プロキシする機能が用意されています。   

詳細は次のページで確認してください。   
https://www.netlify.com/docs/redirects/

```:src/_redirects
/connpass/api/*  https://connpass.com/api/v1/event/:splat  200
```

上記ファイルをsrc直下に用意します。

---

先ほど追加した`_redirects`ファイルをビルドに組み込みます。   
Ionicでは、ビルド処理を拡張するための仕組みが用意されています。   
今回は、wwwディレクトリに`_redirects`ファイルをコピーする追加処理を組み込みます。

`package.json`を以下のとおり修正し、新たに`custom.copy.config.js`ファイルを`config`ディレクトリの下に用意してください。

```json:package.json
  "description": "An Ionic project",
  "config": {
    "ionic_copy": "./config/custom.copy.config.js"
  }
```

```javascript:config/custom.copy.config.js
module.exports = {
    copyRedirects: {
        src: ['{{SRC}}/_redirects'],
        dest: '{{WWW}}'
    }
}
```

---

## GithubもしくはBitbucketにPush

Netlifyへのリリースの準備が出来ました！   
Netlifyは、GithubやBitbucketと連携し、自動でデプロイ・公開する機能が用意されています。   
便利ですね！

ということで、今まで作成したソースコードをGithubにPushします。

GithubへのPush方法は割愛します。


---

## NetlifyへのDeploy

では、Netlifyへデプロイします。
次のサイトへアクセスしてください。

https://app.netlify.com/
<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/0c9b2d57-de06-46fa-0094-62c0cb358127.png" width=30%>

---

自分のGithubもしくはBitbucketのアカウントを用いて、ログインしましょう。
その後、「New site from Git」ボタンをクリックしてください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/e5775db3-6165-f6c7-e44e-db812131461d.png" width=30%>

---

Github、GitLab、Bitbucketのいずれかを選択してください。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/f19e4a46-d49b-0386-1695-f67da24db4ae.png" width=30%>

---

デプロイするリポジトリを選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/101517/c016773c-ca6a-16f1-b648-f4397d550847.png" width=30%>

---

次のビルド設定をして、「Deploy site」ボタンを押下してください。   

`Branch to deploy:` master
`Build command:` npm run build --prod
`Publish directory:` www/

これでリリース完了です！

---

かなり早足のハンズオンでしたが、如何だったでしょうか？
今回はWebアプリケーションを作成しましたが、Ionicを用いるとAndroidやiPhoneのネイティブアプリを作成することも可能です。
Web技術を用いて、ネイティブさながらのハイブリッドアプリケションを短時間に作成できるのは、とても魅力的だと考えております。
今回のハンズオンを通して「面白いな！」と感じて頂けたら、どんどんチャレンジして、かっこいいアプリを作成していってくださいね！

---

# おつかれさまでした！！ 🍻(´∀｀*v)