IonicでContentfulのコンテンツを読み込んで表示してみるぞ！

# はじめに

この記事は [Ionic Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ionicframework) の第21日目の記事です！
今回は、Ionic4のプロジェクトでContentfulの記事を読み込んで、ブログっぽいものを表示してみたいと思います。
大筋はContentfulのサイトにある[Use Contentful in an Angular project](https://www.contentful.com/developers/docs/javascript/tutorials/using-contentful-in-an-angular-project/)というチュートリアルをなぞって進めていきます。

## Ionicのインストール

とりあえずここはNode.jsがインストールされていることが前提です。
もしインストールされていなかったら、[公式サイト](https://nodejs.org/ja/)からインストールしたり、[nodenv](https://qiita.com/kawa64372358/items/c55b527b523317d8758e)などのバージョン管理ツールを使ってインストールしてください。

`node`とか`npm`コマンドが使えるようになったら

```bash
$ npm install -g ionic
```

でIonic CLIをインストールしましょう。

できましたね。

```bash
$ ionic
```

してみてコマンドが動けばインストールできてます。おめでとうございます。

## Ionicのプロジェクトを作るよ

さっそく作ります

```bash
$ ionic start ionic-contentful blank --type=angular
```

これで`ionic-contentful`という名前のIonicプロジェクトが作成できます。ちなみに`--type=angular`と書くことで今のところIonic4でプロジェクトが作成できます。そして`blank`はテンプレートの名前です。1ページのシンプルなテンプレートになってます。

`? Install the free Ionic Appflow SDK and connect your app? (Y/n)`は`n`と入力してください。

プロジェクトの作成が完了したら`ionic-contentful`ディレクトリに移動してください。

## とりあえず動かす

んではさっそく動かします。

```bash
$ ionic sｅｒｖｅ
```

で、開発用のサーバーが立ち上がってブラウザが開きます。ちなみに`ionic s`でも同じ効果があります。割と最近省略できるようになりました。
このコマンドプロジェクトないのファイルを監視していますので、tsやらscssが変更されるとブラウザ側もリロードされます。
んでブラウザで開いたのが、`blank`テンプレートそのままのＩｏｎｉｃアプリです。こっから色々いじっていきます。

## Contentful用のサービスをつくる

Ionic CLIでContentful用のサービスをつくりますよ。

```bash
$ ionic g service contentful
```

これで`src/app/contentful.service.ts`が生成されました。
これを編集する前にcontentfulのSDKをインストールします。

```bash
$ npm i --save contentful
```

んではserviceを編集していきましょう。

```ts:src/app/contentful.service.ts
import { Injectable } from '@angular/core';
+import { createClient, Entry } from 'contentful';

+const CONFIG = {
    // 本当はここには自分のアカウントの情報を記載しますが、今回は公開されているテストアカウントを使用します
+    space: 'wl1z0pal05vy',
+    accessToken: '0e3ec801b5af550c8a1257e8623b1c77ac9b3d8fcfc1b2b7494e3cb77878f92a',
+    contentTypeIds: {
+        product: '2PqfXUJwE8qSYKuM0U6w8M'
+    }
+};

@Injectable({
  providedIn: 'root'
})
export class ContentfulService {

+    private cdaClient = createClient({
+        space: CONFIG.space,
+        accessToken: CONFIG.accessToken
+    });

    constructor() { }

+    getProducts(query?: object): Promise<Entry<any>[]> {
+        return this.cdaClient.getEntries(Object.assign({
+            content_type: CONFIG.contentTypeIds.product
+        }, query))
+            .then(res => res.items);
+    }
}
```

続いて`app.module.ts`に`ContentfulService`を追加していきます。

```ts:/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
+ import {　ContentfulService　} from './contentful.service';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
+   ContentfulService,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
```

##投稿の一覧ページを作るよ

ほいだら次は、HomePageコンポーネントでContentfulのコンテンツを読み込めるように`home.page.ts`を修正していきます。
HomePageコンポーネントが初期化されるときに、Contentfulからproductsのデータを取得してきます。

```ts:src/app/home/home.page.ts
import { Component, OnInit } from '@angular/core'; // OnInitを追加
+ import { ContentfulService } from '../contentful.service';
+ import { Entry } from 'contentful';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
// OnInitをimplementsしましょ
export class HomePage implements OnInit {
+   private products: Entry<any>[] = [];

+   constructor(private contentfulService: ContentfulService) {}

+    ngOnInit() {
+        this.contentfulService.getProducts()
+            .then(products => this.products = products);
+    }
}
```

htmlテンプレート側でも表示してみましょ。`<ion-content>`の中身を入れ替える感じです。

```html:src/app/home/home.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Contentful Data
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <ion-card *ngFor="let product of products">
    <ion-img [src]=product.fields.image[0].fields.file.url"></ion-img>
    <ion-card-header>
      <ion-card-title>{{ product.fields.productName }}</ion-card-title>
    </ion-card-header>
  </ion-card>

</ion-content>
```

画像がでかいのでスタイルもちょっと設定します。

```scss:/src/app/home/home.page.scss
ion-card {
  margin-bottom: 20px;
}

ion-img {
  width: 200px;
  margin: auto;
}
```

##投稿の詳細ページも作ってみるよ

これでとりあえずContentfulが持っているデータを一覧表示することができました。嬉しいです。
せっかくなので商品の個別ページも作ってみましょう。これもIonic CLIで作りますよ。

```bash
$ ionic g page details
```

はいできました。
そしたら一覧の各商品（記事）のIDを使ってルーティングを設定していきます。
このあとserviceで投稿の`id`を使って記事単品を取得するので`id`を使ってリンクするようにします。

```ts:src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
+  { path: 'details/:id', loadChildren: './details/details.module#DetailsPageModule' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```

んでは、一覧の表示にも詳細へのリンクを追加しましょう。

```html:src/app/home/home.page.ts
<ion-header>
  <ion-toolbar>
    <ion-title>
      Contentfulのデータをよみこむぞ
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <ion-card *ngFor="let product of products">
    <ion-img [src]=product.fields.image[0].fields.file.url"></ion-img>
    <ion-card-header>
      <ion-card-title>{{ product.fields.productName }}</ion-card-title>
    </ion-card-header>
+    <ion-card-content>
+        <ion-button routerLink="/details/{{ product.sys.id }}">Details</ion-button>
+    </ion-card-content>
  </ion-card>
</ion-content>
```

次は詳細ページで使うための、投稿データを1件だけ取得する処理をサービスに追加していきます。

```ts:/src/app/contentful.service.ts
import { Injectable } from '@angular/core';
import { createClient, Entry } from 'contentful';

const CONFIG = {
    space: 'wl1z0pal05vy',
    accessToken: '0e3ec801b5af550c8a1257e8623b1c77ac9b3d8fcfc1b2b7494e3cb77878f92a',
    contentTypeIds: {
        product: '2PqfXUJwE8qSYKuM0U6w8M'
    }
};

@Injectable({
  providedIn: 'root'
})
export class ContentfulService {

    private cdaClient = createClient({
        space: CONFIG.space,
        accessToken: CONFIG.accessToken
    });

  constructor() { }

    getProducts(query?: object): Promise<Entry<any>[]> {
        return this.cdaClient.getEntries(Object.assign({
            content_type: CONFIG.contentTypeIds.product
        }, query))
            .then(res => res.items);
    }

+    getProduct(id: any): Promise<Entry<any>> {
+        return this.cdaClient.getEntry(id)
+            .then(res => res);
+    }
}
```

今追加した関数を使って詳細ページの処理を追加します。

```ts:/src/app/details/details.page.ts
import { Component, OnInit } from '@angular/core';
+ import { Router, ActivatedRoute, ParamMap } from '@angular/router';
+ import { ContentfulService } from '../contentful.service';
+ import { Entry } from 'contentful';

@Component({
  selector: 'app-details',
  templateUrl: './details.page.html',
  styleUrls: ['./details.page.scss'],
})
export class DetailsPage implements OnInit {

+    private product: Entry<any> = null;
+    private id: string = null;

    constructor(
+        private contentfulService: ContentfulService,
+        private route: ActivatedRoute,
+        private router: Router
        ) {}

    ngOnInit() {
+        this.route.paramMap.subscribe((params: ParamMap) => {
+            this.id = params.get('id');
+            this.getProduct();
+        });
    }

+    getProduct() {
+        this.contentfulService.getProduct(this.id)
+            .then(product => {
+                this.product = product;
+            });
+    }

+    goBack() {
+      this.router.navigate(['/']);
+    }

}
```

htmlテンプレートも修正しますよ

```html:src/app/details/details.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>details</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
+  <ion-card *ngIf="product">
+    <ion-img [src]=product.fields.image[0].fields.file.url"></ion-img>
+    <ion-card-header>
+      <ion-card-title>{{ product.fields.productName }}</ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <p>Price: ${{ product.fields.price }}</p>
+      <p>Size: {{ product.fields.sizetypecolor }}</p>
+      <p>{{ product.fields.productDescription }}</p>
+    </ion-card-content>
+  </ion-card>
+  <ion-button target="_blank" (click)="goBack()">Back</ion-button>
</ion-content>
```

こちらもスタイルを設定します。

```scss:src/app/details/details.page.scss
ion-img {
  width: 200px;
  margin: auto;
}
```

##まとめ

こんな感じで簡単ではありますがContentfulのデータを読み込んで表示することができました。
デモは[こちら](https://friendly-panini-0c1f81.netlify.com/)に用意してありますので、試すのめんどい人は見てみてください。

###参考

[Use Contentful in an Angular project](https://www.contentful.com/developers/docs/javascript/tutorials/using-contentful-in-an-angular-project/)