中安拓也のブログ

プログラミングについて書くブログ

【JavaScript】Ionicで架空のECアプリを作成する #7 - 同じ商品を買った場合は、まとめて表示したい

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第7回目である今回は、同じ商品を二つ買った場合でも、まとめて表示されない問題について解決します。

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10
  • Google Chrome バージョン: 60.0.3112.113

解決したい問題

f:id:l08084:20180504161319p:plain
今回することのイメージ

現在のアプリ(AS IS)だと、同一の商品を3個購入した場合は、そのままレジ画面に3列分表示されてしまうという問題があります。それを、同一の商品を複数件購入した場合は、1列でまとめて表示するように、コードを改修します。

実装

複数個の商品オブジェクトをまとめて一つにする、groupByメソッドを以前の回で作ったサービスクラスProductProviderに追加します。

src/providers/product/product.ts:

  // ...長いので省略

  /**
   * 同一の商品(idが同じ)を一つのオブジェクトにまとめる
   *
   * @param {Product[]} productList カート内の商品リスト
   * @returns {Product[]} (同一の商品ついては、一つにまとめた状態の)カート内の商品リスト
   * @memberof ProductProvider
   */
  groupBy(productList: Product[]): Product[] {
    const group = productList.reduce((result: Product[], current: Product) => {
      const element = result.find(p => p.id === current.id);
      if (element) {
        // 同一の商品がある場合
        // 商品の数量をインクリメントする
        element.quantity += 1;
      } else {
        result.push(current);
      }
      return result;
    }, []);
    return group;
  }
}

上記のgroupByメソッドをカート画面の初期表示時に呼び出す

src/pages/cart/cart.ts:

// add this!
// 同一の商品(idが同じ)を一つのオブジェクトにまとめる
this.productList = this.productProvider.groupBy(result);

商品が複数個になった場合の価格の表示については、カート画面のテンプレートファイル側で「商品の1個あたりの値段 * 数量」の計算をすることで対応している(あまりテンプレート側で計算とかをするのはよくないと思うので、今後改善していきたい....)

src/pages/cart/cart.html:

<div class="price">¥{{product.price * product.quantity}}</div>

ここまで書くと最初の図のTO BEのイメージ通り、同じ商品を複数購入した場合は、まとまって表示されるはずである。

余談

今回のような、”あるプロパティが同じ複数のオブジェクトをまとめて一つにする処理”は、自分にとっては鬼門で、本件と同様の問題が発生した時にまずいコードを書いてしまったことがある(以前書いたまずいコードについては、こちら)。

今回は、下記のサイトを参考にメソッドを書いた。(「SQLのgroup byのように集計する」という的確な表現だったり、説明がわかりやすい記事でとても参考にさせていただいた)

参考サイト

JavaScript オブジェクト配列をsqlのgroup byのように集計する


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #6 - タブバーのアイコンにバッジをつける

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第6回目である今回は、カートアイコンにショッピングカート内の商品数を表示するバッジをつけるところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10
  • Google Chrome バージョン: 60.0.3112.113

タブバーにバッジを表示する

Ionicのタブには、インプットプロパティtabBadgeがあるため、タブバーのアイコンにバッジを表示するだけなら、簡単にすみます。

src/pages/tabs/tab.html:

<ion-tabs>
  <ion-tab [root]="tab1Root" tabTitle="商品" tabIcon="home"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="カート" tabIcon="cart" tabBadge="4" tabBadgeStyle="danger"></ion-tab>
</ion-tabs>

上記のコードでは、カートアイコン上にバッジで「4」を表示しています

f:id:l08084:20180503183243p:plain
動作確認イメージ

バッジ上の数値とカート内の商品数を同期させる

このままではバッチに固定の数値が表示されているだけで意味がないので、ショッピングカート内の商品数とバッジ上で表示されている数値が常に一致するようにしてあげる必要があります。

IonicのEventsを使う

カート内の商品数とバッジ上の数値を同期させる件ですが、IonicのEventsを使って実現します。Eventsはいわゆる出版-購読型モデルを実現するIonicのAPIになります。 今回は、商品をカートに入れるなど、カート内の商品数が更新されるタイミングでPublish(出版)を実施し、タブ側のコンポーネントではSubscribe(購読)を行うことによって、タブ側で常に最新のカート内の商品数を取得することができる...といった風に使います。

タブ側でSubscribeを行う

実際にコードを書いていきます。まずは、タブバーコンポーネントの方から。

src/pages/tabs/tab.ts:

import { Component } from "@angular/core";

import { HomePage } from "../home/home";
import { CartPage } from "../cart/cart";
import { Events } from "ionic-angular";

@Component({
  templateUrl: "tabs.html"
})
export class TabsPage {
  public tab1Root = HomePage;
  public tab2Root = CartPage;
  public itemCount = 0;

  constructor(public events: Events) {}

  ionViewDidLoad() {
    this.events.subscribe(
      "cart:updated",
      count => (this.itemCount = count ? count : 0)
    );
  }
}

上記のコードでは、タブバーコンポーネント初期作成時(ionViewDidLoad())に、トピック"cart:updated"とアロー関数を引数に、Subscribe(購読)を行なっています。こうすることによって、同一トピック(つまり"cart:updated")のPublish(出版)が行われる度に、アロー関数が実行されるため、バッジの値itemCountは常に最新の値になります。

ちなみにタブバーのテンプレートファイル

src/pages/tabs/tab.html:

<ion-tabs>
  <ion-tab [root]="tab1Root" tabTitle="商品" tabIcon="home"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="カート" tabIcon="cart" tabBadge="{{itemCount}}" tabBadgeStyle="danger"></ion-tab>
</ion-tabs>

カート内の商品数が変更されるタイミングでPublishを行う

あとは、ショッピングカート内の商品数が更新されるタイミングでPublish(出版)を行いさえすれば、カートアイコンのバッジ上に正しい商品数が表示される。

  • カート内の商品数が更新されるタイミング
    • アプリの初期表示時
    • カートに商品を追加した時
    • 注文を確定して、カートの中身が空になった時
アプリの初期表示時

前回起動時に、商品をカートに入れていた場合は、ストレージの機能により、カート内に商品が残ったままであるため、アプリ起動時には必ず商品数が0になる、というわけではない。

なので、アプリ起動時にも、ストレージの中身をチェックして、商品の数をPublishしてあげる必要がある。

タブ(src/pages/tabs/tab.ts):

import { Component } from "@angular/core";

import { HomePage } from "../home/home";
import { CartPage } from "../cart/cart";
import { Events } from "ionic-angular";
import { Storage } from "@ionic/storage";

@Component({
  templateUrl: "tabs.html"
})
export class TabsPage {
  public tab1Root = HomePage;
  public tab2Root = CartPage;
  // カート内の商品の数
  public itemCount = 0;

  constructor(private events: Events, private storage: Storage) {}

  /**
   * Ionicのライフサイクルメソッド
   * タブ画面の作成時に呼び出される
   *
   * @memberof TabsPage
   */
  ionViewDidLoad() {
    // トピック`cart:updated`を購読している
    // 最新のカート内の商品数を取得して、バッジの数値を更新する
    this.events.subscribe(
      "cart:updated",
      count => (this.itemCount = count ? count : 0)
    );

    // add this!
    // アプリの初期表示時のタイミング
    // 前回起動時にカート内に商品を入れた場合を考慮して、
    // ストレージの読み込みを実施している
    this.storage
      .length()
      .then(result => {
        if (result !== 0) {
          this.storage
            .get("items")
            .then(items => {
              // 現在のカート内の商品の数を取得
              const count = items.length;
              // トピック`cart:updated`で出版
              // eventDataとして、カート内の商品の数を渡している
              this.events.publish("cart:updated", count);
            })
            .catch(err => console.log(`storage error: ${err}`));
        }
      })
      .catch(err => console.log(`storage error: ${err}`));
  }
}
カートに商品を追加した時

ショッピングカートに商品を追加したタイミングでも、当然カート内の商品の数が変わるため、Publishしてあげる必要がある。

商品詳細画面(src/pages/detail/detail.ts):

import { Component } from "@angular/core";
import { NavController, NavParams, Events } from "ionic-angular";
import { Storage } from "@ionic/storage";
import { Product } from "../../model/product.model";

@Component({
  selector: "page-detail",
  templateUrl: "detail.html"
})
export class DetailPage {
  product: Product;

  constructor(
    private navCtrl: NavController,
    private navParams: NavParams,
    private storage: Storage,
    private events: Events
  ) {}

  ngOnInit() {
    // 商品リスト画面でタップした商品情報を受け取る
    this.product = this.navParams.get("product");
  }

  /**
   * ショッピングカートに商品を追加する
   *
   * @param {Product} product
   * @memberof DetailPage
   */
  addItem(product: Product) {
    let itemList = [];
    this.storage
      .length()
      .then(result => {
        // カート内の商品が0個だった場合
        if (result === 0) {
          itemList = [];
          itemList.push(product);
          // ストレージにボタンを押下した商品を追加する
          this.storage.set("items", itemList);
          // add this!
          // トピック`cart:updated`で出版
          // eventDataとして、カート内の商品の数を渡している
          this.events.publish("cart:updated", 1);
        } else {
          // カート内にすでに商品があった場合
          this.storage
            .get("items")
            .then(items => {
              items.push(product);
              const count = items.length;
              // ストレージにボタンを押下した商品を追加する
              this.storage.set("items", items);
              // add this!
              // トピック`cart:updated`で出版
              // eventDataとして、カート内の商品の数を渡している
              this.events.publish("cart:updated", count);
            })
            .catch(err => console.log(`storage error: ${err}`));
        }
      })
      .catch(err => console.log(`storage error: ${err}`));
    // 商品リスト画面に戻る
    this.navCtrl.pop();
  }
}
注文を確定して、カートの中身が空になった時

注文を確定して、未決済の商品(つまりカート内の商品)が0件になったときも、Publishして購読者に最新の商品数を送ってあげる必要がある。

カート画面(src/pages/cart/cart.ts):

// ...長いので省略

  /**
   * 「注文する」ボタン押下時に呼び出し
   *  Confirm Alertを表示し、OKを選択すると、
   *  注文を確定して、ストレージをクリアする
   *
   * @memberof CartPage
   */
  order() {
    const confirm = this.alertCtrl.create({
      title: "注文を確定しますか?",
      buttons: [
        {
          text: "キャンセル",
          handler: () => {}
        },
        {
          text: "OK",
          handler: () => {
            let alert = this.alertCtrl.create({
              title: "ご注文を受け付けました!",
              subTitle: "ご指定の住所までのお届け時間は、20-30分程です",
              buttons: ["OK"]
            });
            alert.present();
            this.storage.clear();
            this.productList = [];
            this.isEmpty = true;
            // add this!
            // トピック`cart:updated`で出版
            // eventDataとして、カート内の商品の数を渡している
            this.events.publish("cart:updated", 0);
          }
        }
      ]
    });
    confirm.present();
  }

// ...長いので省略

ここまで書くと、ショッピングカート内の商品数と、バッジの表示が同期されます。


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #5 - カート画面を作る(後編)

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第5回目である今回は、カート画面を作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10
  • Google Chrome バージョン: 60.0.3112.113

実装

カート画面の実装

カート画面の実装をしていく。

  • ポイント
    • 画面の初期表示以外のタイミングでも、ストレージを読み込む必要があるため、ionViewDidLoadではなく、ionViewDidEnterを使う
    • IonicのAlertControllerを使って、確認アラームを表示している
    • 確認アラームのOKを選択すると、注文が完了したことを知らせるアラームが表示される

src/pages/cart/cart.ts:

import { Component } from "@angular/core";
import { AlertController, NavController, Events } from "ionic-angular";
import { Product } from "../../model/product.model";
import { Storage } from "@ionic/storage";
import { ProductProvider } from "../../providers/product/product";

@Component({
  selector: "page-cart",
  templateUrl: "cart.html"
})
export class CartPage {
  // カート内の商品の一覧
  productList: Product[];
  // 小計
  subtotal: number;
  // 合計
  total: number;
  // カート内が空の時にtrue
  isEmpty: boolean;

  constructor(
    public navCtrl: NavController,
    public alertCtrl: AlertController,
    private storage: Storage,
    private productProvider: ProductProvider
  ) {}

  /**
   * Ionicのライフサイクルメソッドの一種
   * ページがアクティブになると呼び出される
   * ストレージ(カート)内に商品が格納されていれば、取得する
   *
   * @memberof CartPage
   */
  ionViewDidEnter() {
    this.productList = [];
    this.subtotal = 0;
    this.total = 0;
    this.isEmpty = false;
    this.storage
      .length()
      .then(result => {
        // カート内に商品がある場合
        if (result > 0) {
          this.storage
            .get("items")
            .then(result => {
              this.productList = result;
              this.productList.forEach(
                // 小計を計算
                product => (this.subtotal += product.price)
              );
              // 小計にデリバリー料を加算して、合計を計算している
              this.total = this.subtotal + 300;
            })
            .catch(err => {});
        } else {
          // カート内が空の場合
          this.isEmpty = true;
        }
      })
      .catch(err => {});
  }

  /**
   * 「注文する」ボタン押下時に呼び出し
   *  Confirm Alertを表示し、OKを選択すると、
   *  注文を確定して、ストレージをクリアする
   *
   * @memberof CartPage
   */
  order() {
    const confirm = this.alertCtrl.create({
      title: "注文を確定しますか?",
      buttons: [
        {
          text: "キャンセル",
          handler: () => {}
        },
        {
          text: "OK",
          handler: () => {
            let alert = this.alertCtrl.create({
              title: "ご注文を受け付けました!",
              subTitle: "ご指定の住所までのお届け時間は、20-30分程です",
              buttons: ["OK"]
            });
            alert.present();
            this.storage.clear();
            this.productList = [];
            this.isEmpty = true;
          }
        }
      ]
    });
    confirm.present();
  }
}
  • ポイント
    • AngularのngIfElseを使っている。ストレージの中身が空の時は、#elseBlock(お客様のショッピングカートに商品はありません。)の方が表示される
    • 注文するボタンを押下すると、order()メソッドを呼び出す

src/pages/cart/cart.html:

<ion-header>
  <ion-navbar>
    <ion-title>
      カート
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <div *ngIf="!isEmpty; else elseBlock">
    <ion-list>
      <ion-item *ngFor="let product of productList">
        <div class="quantity">{{product.quantity}}</div>
        <div class="product">{{product.name}}</div>
        <div class="price">¥{{product.price}}</div>
      </ion-item>
      <ion-item>
        <div class="check-detail">
          <p class="title">小計</p>
          <p class="price">¥{{subtotal}}</p>
        </div>
        <div class="check-detail">
          <p class="title">デリバリー料</p>
          <p class="price">¥300</p>
        </div>
      </ion-item>
      <ion-item>
        <div class="title emphasis">合計(税込み)</div>
        <div class="price emphasis">¥{{total}}</div>
      </ion-item>
      <ion-item no-lines>
      </ion-item>
    </ion-list>
    <button ion-button full color="secondary" class="order" (click)="order()">注文する</button>
  </div>
  <ng-template #elseBlock>
    <h2>お客様のショッピングカートに商品はありません。</h2>
  </ng-template>
</ion-content>
  • ポイント
    • floatの回り込み解除のために、overflow: hidden;を設定している

src/pages/cart/cart.scss:

page-cart {
  .quantity {
    float: left;
    margin: 0 10px 0 0;
  }
  .product {
    float: left;
  }
  .title {
    float: left;
    &.emphasis {
      font-weight: bold;
    }
  }
  .price {
    float: right;
    &.emphasis {
      font-weight: bold;
      color: #3d9970;
      font-size: 1.9rem;
    }
  }
  .check-detail {
    // floatの回り込み解除
    overflow: hidden;
  }
  .order {
    position: fixed;
    bottom: 60px;
    left: 0;
    margin: 0;
  }
}

動作確認

上記のコードを書いた段階で、ionic serveコマンドを実行して、ブラウザ上で確認してみる

カート画面表示

ストレージ(ショッピングカート)内の商品情報がリスト形式で表示されている

f:id:l08084:20180502162652p:plain
カート画面初期表示

注文ボタン押下

「注文する」ボタンを押下すると、Ionicのアラートが表示される

f:id:l08084:20180502163101p:plain
注文ボタン押下時に表示される 確認アラート

f:id:l08084:20180502163019p:plain
確認アラートでOKを選択すると表示されるアラート


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #4 - カート画面を作る(前編)

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第4回目である今回は、ショッピングカートに商品を追加する処理の実装まで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10
  • Google Chrome バージョン: 60.0.3112.113

下準備

カート画面用のファイル作成

Ionic CLIのコマンドで、カート画面用のコンポーネントファイルなどをまず作る。

$ ionic generate page cart --no-module

コマンドの結果、下記の3ファイルが作成される

  • src/pages/cart/cart.ts
  • src/pages/cart/cart.html
  • src/pages/cart/cart.scss

ionic-storageのインストール

カート画面でストレージを使うので、下記のコマンドを実行してionic-storageをインストールしてください。

$ ionic cordova plugin add cordova-sqlite-storage
$ npm install --save @ionic/storage

NgModuleの設定

下記の対応を実行する。

  • @NgModuledeclarations: []entryComponents: []配列に、作成したCartPageを追加
  • @NgModuleimports: []配列にIonicStorageModule.forRoot()を追加

src/app/app.module.ts:

// ...省略

import { CartPage } from "../pages/cart/cart";
import { IonicStorageModule } from "@ionic/storage";

@NgModule({
  declarations: [
    MyApp,
    AboutPage,
    ContactPage,
    HomePage,
    TabsPage,
    DetailPage,
    CartPage
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    HttpClientModule,
    IonicStorageModule.forRoot()
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    AboutPage,
    ContactPage,
    HomePage,
    TabsPage,
    DetailPage,
    CartPage
  ],
// ...省略

実装

タブ(共通ナビゲーション)を実装する

カート画面へのリンクがないので、タブにリンクをつけて、アイコンもショッピングカートぽいのに変える。

src/pages/tabs/tabs.html:

<ion-tabs>
  <ion-tab [root]="tab1Root" tabTitle="商品" tabIcon="home"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="カート" tabIcon="cart"></ion-tab>
</ion-tabs>

src/pages/tabs/tabs.ts:

import { Component } from "@angular/core";

import { HomePage } from "../home/home";
import { CartPage } from "../cart/cart";

@Component({
  templateUrl: "tabs.html"
})
export class TabsPage {
  tab1Root = HomePage;
  tab2Root = CartPage;

  constructor() {}
}

あとの回で、ショッピングカート内の商品数をBadgeで表現する実装も入れます

f:id:l08084:20180501164505p:plain
実装後のタブ

ショッピングカートに商品を追加する処理

商品詳細画面に「カートに1つ追加する」ボタンを押下すると、ストレージに商品情報を格納する処理を追加する。

src/pages/detail/detail.ts:

import { Component } from "@angular/core";
import { NavController, NavParams } from "ionic-angular";
import { Storage } from "@ionic/storage";
import { Product } from "../../model/product.model";

@Component({
  selector: "page-detail",
  templateUrl: "detail.html"
})
export class DetailPage {
  product: Product;

  constructor(
    private navCtrl: NavController,
    private navParams: NavParams,
    private storage: Storage
  ) {}

  ngOnInit() {
    // 商品リスト画面でタップした商品情報を受け取る
    this.product = this.navParams.get("product");
  }

  /**
   * ショッピングカートに商品を追加する
   *
   * @param {Product} product
   * @memberof DetailPage
   */
  addItem(product: Product) {
    let itemList = [];
    this.storage
      .length()
      .then(result => {
        // カート内の商品が0個だった場合
        if (result === 0) {
          itemList = [];
          itemList.push(product);
          // ストレージにボタンを押下した商品を追加する
          this.storage.set("items", itemList);
        } else {
          // カート内にすでに商品があった場合
          this.storage
            .get("items")
            .then(items => {
              items.push(product);
              const count = items.length;
              // ストレージにボタンを押下した商品を追加する
              this.storage.set("items", items);
            })
            .catch(err => console.log(`storage error: ${err}`));
        }
      })
      .catch(err => console.log(`storage error: ${err}`));
    // 商品リスト画面に戻る
    this.navCtrl.pop();
  }
}

src/pages/detail/detail.html:

  <!-- ... 長いので省略 -->

  <button ion-button full color="secondary" class="in-cart" (click)="addItem(product)">カートに1つ追加する</button>
</ion-content>

カートに追加ボタンの動作確認

Chromeのdevtool(デベロッパーツール)で確認すると、ボタンを押下した商品情報がionic-storageの中に格納されていることがわかる。

f:id:l08084:20180501173923p:plain


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #3 - 商品詳細画面を作る

f:id:l08084:20180430174028g:plain

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第3回目である今回は、商品詳細画面を作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10

下準備

商品詳細画面用のファイルを作成

前回作成した商品リスト画面から遷移できる商品詳細画面を作成する。

まず、Ionic CLIの下記コマンドを実行して、必要なファイルのセットを作成する

$ ionic generate page detail --no-module

商品詳細画面のテンプレート、SCSS、コンポーネントファイルが作成される

f:id:l08084:20180430160627p:plain

NgModuleの設定

@NgModuledeclarations: []entryComponents: []配列に先ほど作成した商品詳細画面のクラスDetailPageを追加する。

src/app/app.module.ts

// ...省略

// add this!
import { DetailPage } from "../pages/detail/detail";

@NgModule({
  // add this!
  declarations: [MyApp, AboutPage, ContactPage, HomePage, TabsPage, DetailPage],
  imports: [BrowserModule, IonicModule.forRoot(MyApp), HttpClientModule],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    AboutPage,
    ContactPage,
    HomePage,
    TabsPage,
    // add this!
    DetailPage
  ],
// ...省略

実装

商品リスト画面に画面遷移処理を追加

商品リスト画面で商品をタップした時に、商品詳細画面に遷移するようにする。

src/pages/home/home.ts:

// ...省略

// add this!
import { DetailPage } from "../detail/detail";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  // ...省略

  constructor(
    // add this!
    private navCtrl: NavController,
    private productProvider: ProductProvider
  ) {}
  
  // add this!
  goToDetail(product: Product) {
    this.navCtrl.push(DetailPage, { product: product });
  }
}

タップした商品を商品詳細画面に渡して遷移するメソッドgoToDetail()を追加した

src/pages/home/home.html:

    // ...省略
 // (click)="goToDetail(jacket)"を追加
    <button ion-item *ngFor="let tops of topsList" (click)="goToDetail(tops)">
      <ion-thumbnail item-start>
        <img src="{{tops.imagePath}}">
      </ion-thumbnail>
      <h2>{{tops.name}}</h2>
      <p>¥{{tops.price}}</p>
    </button>
    <ion-list-header>
      ジャケット/アウター
    </ion-list-header>
    // (click)="goToDetail(jacket)"を追加
    <button ion-item *ngFor="let jacket of jacketList" (click)="goToDetail(jacket)">
      <ion-thumbnail item-start>
        <img src="{{jacket.imagePath}}">
      </ion-thumbnail>
      <h2>{{jacket.name}}</h2>
      <p>¥{{jacket.price}}</p>
    </button>
  </ion-list>
</ion-content>

<button ion-item>(click)="goToDetail(tops)"を追加

商品詳細画面の作成

商品詳細画面の実装をしていく

src/pages/detail/detail.ts:

import { Component } from "@angular/core";
import { NavController, NavParams } from "ionic-angular";
import { Product } from "../../model/product.model";

@Component({
  selector: "page-detail",
  templateUrl: "detail.html"
})
export class DetailPage {
  product: Product;

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

  ngOnInit() {
    this.product = this.navParams.get("product");
  }
}

this.product = this.navParams.get("product");の部分で、商品リスト画面からタップした商品データを受け取っている

src/pages/detail/detail.html:

<ion-header>
  <ion-navbar>
    <ion-title>
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <h2>{{product.name}}</h2>
  <div class="image-container">
    <img class="product-image" src="{{product.imagePath}}">
  </div>
  <div class="comment">
    <p>{{product.commentOne}}</p>
    <p>{{product.commentTwo}}</p>
  </div>
  <ion-list no-lines>
    <ion-item>
      <div>
        <p class="title">小計</p>
        <p class="price">¥{{product.price}}</p>
      </div>
    </ion-item>
    <ion-item></ion-item>
  </ion-list>
  <button ion-button full color="secondary" class="in-cart">カートに1つ追加する</button>
</ion-content>

商品リスト画面から渡された商品データの、名称、値段などを表示している

なお、「カートに1つ追加する」ボタンの処理ついては、今回は書かない

src/pages/detail/detail.scss:

page-detail {
  .image-container {
    display: flex;
    justify-content: center;
  }
  .product-image {
    width: 60%;
    height: 60%;
  }
  .comment {
    margin: 5%;
  }
  .in-cart {
    position: fixed;
    bottom: 60px;
    left: 0;
    margin: 0;
  }
  .title {
    float: left;
    font-weight: bold;
    font-size: 1.7rem;
  }
  .price {
    float: right;
    font-weight: bold;
    font-size: 1.9rem;
    color: #3d9970;
  }
}

画像のサイズが思ったより大きかったので、少し小さくして、flexboxで中央寄せしたりしてる

動作確認

ここまで実装した段階で動作確認をする。

f:id:l08084:20180430172818p:plain
商品詳細画面

ちゃんと商品詳細画面が表示されたので、今回はここまで。


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #2 - 商品リスト画面を作る

f:id:l08084:20180429155759p:plain
今回作成する画面

前回の記事はこちら

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第2回目である今回は、商品リスト(ホーム)画面を作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10

JSONにアクセスするサービスクラスを作る

第1回目の記事で商品データ(JSON)を作成したので、それにHTTPアクセスするサービスクラスを作る。

サービスクラス作成

ターミナルを立ち上げて、下記のIonic CLIコマンドを実行

$ ionic generate provider product

Angular CLIのng generate serviceにあたる上記のコマンドで、src/providers/product/product.tsファイルが作成される

NgModuleの設定

HttpClientModule@NgModule.importsの配列に追加する

src/app/app.module.ts:

// ...省略

// add this!
import { HttpClientModule } from "@angular/common/http";

@NgModule({
  declarations: [MyApp, AboutPage, ContactPage, HomePage, TabsPage],
  // imports:[]にHttpClientModuleを追加
  imports: [BrowserModule, IonicModule.forRoot(MyApp), HttpClientModule],
  // ...省略
})
export class AppModule {}

サービスクラスの実装

作成したサービスクラスにローカルのJSONにHTTPアクセスするコードを書く

src/providers/product/product.ts:

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Product } from "../../model/product.model";

@Injectable()
export class ProductProvider {
  constructor(public http: HttpClient) {}

  retrieve(): Observable<Product[]> {
    // JSONにアクセスして、結果をProduct[]に変換
    return this.http.get<Product[]>("assets/data/products.json");
  }
}

商品リスト画面コンポーネントクラス作成

続いて、商品リスト画面のコンポーネントクラスであるhome.tsとそのテンプレートファイルであるhome.htmlの内容を更新する。

src/pages/home/home.ts:

import { Component, OnInit } from "@angular/core";
import { NavController } from "ionic-angular";
import { Product, PRODUCT_TYPE } from "../../model/product.model";
import { ProductProvider } from "../../providers/product/product";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  productList: Product[];
  topsList: Product[];
  jacketList: Product[];

  constructor(
    private navCtrl: NavController,
    private productProvider: ProductProvider
  ) {}

  ionViewDidLoad() {
    // 全商品リストを取得
    this.productProvider.retrieve().subscribe(products => {
      this.productList = products;
      // トップス のみのリストを取得
      this.topsList = products.filter(
        product => product.type === PRODUCT_TYPE.tops
      );
      // ジャケット/アウター のみのリストを取得
      this.jacketList = products.filter(
        product => product.type === PRODUCT_TYPE.jacket
      );
    });
  }
}

HomePageクラスでは、下記のことを行なっている。

  • ProductProviderクラスのretrieve()メソッドを呼び出して、商品リストを取得
  • 商品リストから、トップス(PRODUCT_TYPE.tops)の商品のみをフィルタリング
  • トップスの商品リストを、プロパティtopsListにバインド
  • 商品リストから、ジャケット/アウター(PRODUCT_TYPE.jacket)の商品のみをフィルタリング
  • ジャケット/アウターの商品リストを、プロパティjacketListにバインド

src/pages/home/home.html:

<ion-header>
  <ion-navbar>
    <ion-title>Apparel</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-list-header>
      トップス
    </ion-list-header>
    <button ion-item *ngFor="let tops of topsList">
      <ion-thumbnail item-start>
        <img src="{{tops.imagePath}}">
      </ion-thumbnail>
      <h2>{{tops.name}}</h2>
      <p>¥{{tops.price}}</p>
    </button>
    <ion-list-header>
      ジャケット/アウター
    </ion-list-header>
    <button ion-item *ngFor="let jacket of jacketList">
      <ion-thumbnail item-start>
        <img src="{{jacket.imagePath}}">
      </ion-thumbnail>
      <h2>{{jacket.name}}</h2>
      <p>¥{{jacket.price}}</p>
    </button>
  </ion-list>
</ion-content>

home.htmlテンプレートでは、下記のことを行なっている。

  • 分類がトップスの商品配列topsListngForディレクティブを使って、リスト形式に表示
  • 分類がジャケット/アウターの商品配列jacketListngForディレクティブを使って、リスト形式に表示

動作確認

上記のコードを書いた段階でいったん動作確認をしてみる。

f:id:l08084:20180429155759p:plain
商品リスト(ホーム)画面

ちゃんとそれっぽい画面が表示されたので、今回はここまで。


次回の記事

【JavaScript】Ionicで架空のECアプリを作成する #1 - 商品データを作る

ハイブリットモバイルアプリフレームワークのIonicを使って、架空のアパレルショップにおける注文アプリを作成する。

第1回目である今回は、商品(洋服)データを作成するところまで

  • デモ

こちらのURLで、完成版のアプリを実際に操作することができます

https://l08084.github.io/ionic-sample-shopping-app/www

  • GitHubリポジトリ

github.com

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っている

  • ionic-angular@3.9.2
  • Angular@5.2.10

Ionicプロジェクト作成

Ionic CLIをPCに入れていない場合は、まずインストールする必要がある。

$ npm install -g ionic

タブのテンプレートを採用したプロジェクトを作成する。

$ ionic start ionic-sample-shopping-app tabs

プロジェクトがちゃんと作成されたかの、動作確認

$ cd ionic-sample-shopping-app
$ ionic serve

タブ型の初期プロジェクトがちゃんと作成されていることを確認

f:id:l08084:20180428155357p:plain
初期状態の表示

モデルクラスを作成する

商品を表すモデルクラスを作成する。enumPRODUCT_TYPEは、商品のジャンル(アウターかジャケットかなど)を判定するのに使う予定。

src/model/product.model.ts:

export interface Product {
  id: string;
  name: string;
  price: number;
  type: number;
  imagePath: string;
  commentOne: string;
  commentTwo: string;
  quantity: number;
}

export const enum PRODUCT_TYPE {
  tops = 0,
  jacket = 1
}

商品データを作成する

データを取得できるWebAPIが存在しないので、ローカルにJSONファイルを配置して、そこからデータを取得する方式にする。

画像ファイル取得

サムネイルに使う画像ファイルが欲しかったので、下記のサイトから洋服の画像ファイルを取得する。

https://www.photo-ac.com/

ダウンロードした画像ファイルは、src/assets/配下に配置する。

f:id:l08084:20180429003408p:plain

JSONファイル作成

続いて、架空のアパレルショップA店の商品データ8件を格納した、JSONファイルを作成する

src/assets/data/products.json:

[
  {
    "id": "01",
    "name": "ボーダーシャツ長袖 ホワイト/ブラック",
    "price": 3500,
    "type": 0,
    "imagePath": "assets/imgs/black-white-border.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "02",
    "name": "ドット柄ブラウス ブルー",
    "price": 2400,
    "type": 0,
    "imagePath": "assets/imgs/blue-blouse.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "03",
    "name": "英字プリント半袖Tシャツ",
    "price": 2200,
    "type": 0,
    "imagePath": "assets/imgs/gray-t-shirt.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "04",
    "name": "ストライプ柄ブラウス ホワイト",
    "price": 3700,
    "type": 0,
    "imagePath": "assets/imgs/white-blouse.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "11",
    "name": "シワ加工ジャケット ベージュ",
    "price": 12000,
    "type": 1,
    "imagePath": "assets/imgs/beige-jacket.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "12",
    "name": "スクールダッフルコート ブルー",
    "price": 11000,
    "type": 1,
    "imagePath": "assets/imgs/blue-duffle-coat.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "13",
    "name": "スクールダッフルコート ホワイト",
    "price": 11000,
    "type": 1,
    "imagePath": "assets/imgs/white-duffle-coat.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  },
  {
    "id": "14",
    "name": "ナイロンパーカー パープル/ピンク",
    "price": 37000,
    "type": 1,
    "imagePath": "assets/imgs/nylon-parker.png",
    "commentOne": "合わせやすいカジュアルなボーダーの長袖シャツ",
    "commentTwo":
      "シワ加工ディティール入り、オーバーサイズシャツ、ケアレスファイバー",
    "quantity": 1
  }
]

今回はここまで。


次回の記事はこちら