モデルとアプリのデータ

シリーズの最後の部分(今のところは少なくとも)は一段と力を入れて、アプリにタイムゾーン機能を追加します。 途中でモデル(MVC の 'M')を紹介し、Ember アプリでアプリのデータを扱ったり、外部/サードパーティのライブラリを利用したりする方法を紹介します。

ここでは、タイムゾーンのリンクをクリックしたときにディスプレイされる追加のタイムゾーンをユーザーが選択できるようにする機能を追加して、world-clock アプリを完成させます。 これにより、ユーザーはサンフランシスコ、ブエノスアイレス、ロンドンなどのさまざまな場所の友人とのミーティングをスケジュールするのに役立ちます。 Ember Data と LocalForage ライブラリを使用してデータを IndexedDB インスタンスに保存し、アプリをオフラインで使用できるようにし、Moment Timezone ライブラリを使用してタイムゾーンデータを提供します。

モデルの生成

前述のように、次のコマンドを使用して、モデル(および関連するテストファイル)を生成することができます。

ember generate model name-of-model

しかし、一般的に Ember のモデルを作成するときには、モデルのデータを表示して操作できるように、ルートとテンプレートも必要になります(データはそのままではあまり使用されません)。 Ember の言葉では、モデル + ルート + テンプレートが一緒になってリソース(resource)を構成します。

アプリのリソースの生成

先に進み、リソースを生成します。 プロジェクトのルートディレクトリ内で、次のコマンドを一度に1つずつ実行します。

ember generate model timezones
ember generate route timezones

次のファイルが生成されます。

  • app/models/timezone.js
  • tests/unit/models/timezone-test.js
  • app/routes/timezones.js
  • app/templates/timezones.hbs
  • tests/unit/routes/timezones-test.js

また、ビューとテンプレートの記事の最後で app/templates/application.hbs に行った変更を元に戻すこともできます — このファイルを開いて行を変更します。

<li>Manage Timezones</li>

この行を次のように変更します。

<li>{{#link-to "timezones"}}Manage Timezones{{/link-to}}</li>

ビューとテンプレートの最後で話したエラーが消えて、application のビュー(localhost:4200)で時計を見ることができ、機能する「Clock」 のリンクと「Manage Timezones」のリンクを使って2つのビューを切り替えることができます。 現在、timezones のビューには何も表示されませんが、今後はそれを修正します。

メモ: Ember.js のファイル命名規則は、アプリの関連部分を関連付けるのに役立つことを忘れないでください。

Ember Data

私たちのアプリにはデータストア(data store)が必要です — アプリに含まれる各タイムゾーンに名前を付け、それとオフセット値を関連付け、クライアント側でデータを保存できるようにしてアプリがオフラインで動作するようにします。

Ember アプリでデータを管理するには、モデル内にデータストアとして機能する Ember CLI に付属のライブラリ Ember Data を使用できます。 Ember Data を使用してモデル内のデータ構造を定義し、ルート内のモデルを参照すると、アプリのコントローラやテンプレートがそのデータにアクセスできるようになります。 モデルにはデータ自体はありません。 レコード(record)と呼ばれる特定のインスタンスのプロパティと動作を定義するだけです。

Ember Data を使用すると、使用している実際のデータストレージメカニズムと Ember データレコードとの間でデータを変換できるアダプター(adapter)があれば、アプリのデータを保存するような基本的なストレージメカニズムをほとんど使用できます。 これにより、使用するデータストアの種類(たとえば、localStorageIndexedDB など)を柔軟に選択できます。

アプリでデータが必要な場合は、

  1. アプリは、モデルのデータストアに行って探します。
  2. データストアは、アダプターに対してデータを返すよう要求を送ります。
  3. アダプターは実際のデータストレージメカニズム(IndexedDB、RESTful XHR API など)に行き、データを取得し、モデルが理解できる JSON に変換します。
  4. モデルは JSON を受け取り、シリアライザー(serializer)を介してデータレコードに変換し、アプリに送り返します。

ここでは、LocalForage ライブラリを利用して、ブラウザーで利用可能なデータストアを自動的に検出し、利用可能な最適なデータストアを選択します(IndexedDB を使用し、前者をサポートしていないブラウザーでは WebSQL/localStorage にフォールバックします)。 それについては、Ember のアダプターが利用可能で、これについては以下のインストールを参照してください。

メモ:これがどのように動作するかを学ぶときは、Ember.js のドキュメントの Models guide も非常に便利です。

モデルへのデータの追加

テキストエディタで app/models/timezone.js を開き、次のようにコードを更新していくつかのデータ属性を追加します。

import DS from 'ember-data';

export default DS.Model.extend({
    name: DS.attr('string'),
    offset: DS.attr('number')
});

このコードは Ember により、データ構造がどのようになるかを指定するだけです — 各タイムゾーン名を含む文字列と、各タイムゾーンのオフセット値を含む数値。

LocalForage でのデータの保存

今度は、Ember Data で使用できるように LocalForage アダプターをインストールします。 これを行うには、次のコマンドを実行します。

ember install ember-localforage-adapter

(訳注:インストールを完了すると、world-clock/bower_components の下に新しい localforage ディレクトリがあり、world-clock/node_modules の下に新しい ember-localforage-adapter ディレクトリがあるはずです。 ember-localforage-adapter の下には node_modules ディレクトリがありますが、古いようなので削除します。)

(訳注開始:install:addon から install に変更されているので、この部分は、見直しが必要です。)

無効なエラーが発生した場合すなわち、「The specified command install:addon is invalid. For available options, see `ember help`.(指定されたコマンド install:addon は無効です。 利用可能なオプションについては、`ember help` を参照してください。)」では、次の特定のバージョンの bower コマンドを実行します。 $ bower install localforage -v '1.2.2'

: Bower と Ember は分割されていますが、localforage-adapter は引き続き Bower を使用しています。 後で、好ましい方法での依存関係の管理についてお話します。 ただ、これは汚いし、廃止予定であることを知っています。

これにより、アプリのためのサードパーティのライブラリや依存関係を簡単にインストールできるパッケージマネージャである Bower を使用して、LocalForage とそのアダプターをインストールする必要があります。

:このコマンドの実行後に ember-data のバージョンを選択するように求められたら、アプリで必要なバージョンを選択します。 選択肢番号の後に!を入力して、選択肢を選択して持続させることができ、そして Enter キーを押します。

Bower の依存関係のプロンプト

Bower が LocalForage パッケージのインストールを完了すると、world-clock/bower_components の下に新しい localforage ディレクトリがあるはずです。

/bower_components ディレクトリには、アプリの依存関係の多くが含まれています。 すでにこのディレクトリにはかなりのフォルダがありますが、その多くは、あらかじめインストールされている Ember フレームワークのコア依存関係です。 (訳注:Ember が標準では Bower を使わなくなったので、localforage しかないはずです。)

(訳注終了)

新しい Ember アダプターの作成

LocalForage と Ember LocalForage アダプターをアプリに追加したので、LFAdapter オブジェクトにアクセスして、データストアから Ember Data にデータを送ることができます。 これを使用して、タイムゾーンのデータベースを作成します。

Ember では、このコードを格納するためのアダプターファイルを生成することができます。 つまり、モデル、コントローラなどとは別に保存することができます。

次のコマンドを使用して新しいアダプターファイルを生成してみましょう - プロジェクトのルートディレクトリから実行してください。

ember generate adapter application

次のものが生成されます。

  • アダプターのコードを含む app/adapters の JavaScript アダプターファイル。
  • アダプターのユニットテストを含む tests/unit/adapters の JavaScript ファイル。

いくつかの不足している依存関係をインストールするには、bower install を実行する必要があります — これは問題ありません。

これで、app/adapters/application.js を開き、そのコードを次のように置き換えます。

import LFAdapter from 'ember-localforage-adapter/adapters/localforage';

export default LFAdapter.extend({
    namespace: 'WorldTimeZones'
});

このコードは、LocalForage に WorldTimeZones という名前のデータストアを作成し、それを Ember Data インスタンスに結合します。

データストアからのレコードの取得

最後に、タイムゾーンレコードを timezones のルートのモデルとして返したいと思うでしょう。 モデルをルートに設定すると(Ember.Routemodel メソッドを使用する、ルートのモデルを指定するも参照)、コントローラとテンプレートは指定されたデータにアクセスし、操作およびディスプレイできるようになります。

app/routes/timezones.js を開き、コードを次のように変更します。

import Route from '@ember/routing/route';

export default Route.extend({
    model() {
        return this.store.findAll('timezones');
    }
});

このコードでは、timezones のルートで timezones のデータモデルを利用できるように Ember に「このルートにナビゲートすると、このデータにアクセスできるので、コントローラで制御してテンプレートでディスプレイできます。」と伝えます。

タイムゾーンのデータの収集

私たちは、ユーザーがすべてのタイムゾーンのリストから選択できるようにしたいと考えています。 このために、JavaScript の日付と時刻を処理するためのすばらしいライブラリである Moment Timezone を使用します。 このライブラリは利用可能なすべてのタイムゾーンのリストを提供し、わかりやすい方法でそれらをフォーマットすることができます。

 Ember の仕組みは変化しています。 彼らは Bower から npm に向かって移動しています。 先に進む前にいくつかの追加ツールをインストールする必要があります。

まず、ember-browserify という拡張が必要です。これは ES5 と ES6 の依存関係の橋渡しに役立ちます。 プロジェクトのルートディレクトリで次のコマンドを入力してください。

ember install ember-browserify --save-dev

今度は、npm を使って Moment Timezone ライブラリをインストールすることができます。 プロジェクトのルートディレクトリで次のコマンドを入力してください。

npm install moment moment-timezone --save-dev

これにより、node_modules/moment ディレクトリに Moment ライブラリファイルがインストールされます。 --save-dev フラグは、後でモジュールをインポートできるようにします。 私たちが使用しようとしている2つのファイルは次のとおりです。

  • node_modules/moment/moment.js — 日付と時刻をフォーマットするためのコア moment.js ライブラリ。
  • node_modules/moment-timezone/builds/moment-timezone-with-data-2012-2022.js — 世界のタイムゾーンのデータ。(訳注:ファイル名に含まれている年度は Moment Timezone のアップデートで変更されます。)

タイムゾーンをフォーマットするときにアプリが moment スクリプトにアクセスできるようにするには、プロジェクトのルートディレクトリにある ember-cli-build.js.eslintrc.js を編集する必要があります。

ember-cli-build.js は Ember CLI が依存関係をプロジェクトに組み込むために使用するアセットパイプライン/依存関係マネージャーです。 Ember CLI は構築時に ember-cli-build.js を見て、あなたが app.import() で指定した依存関係を組み込むことができます。 これにより、アプリが正しく機能するために必要なすべてのコンポーネントを持っていることを保証します。

ここでは2つの Moment ライブラリをインポートする必要があります。 ember-cli-build.js を開き、次のように app.import() を使用してそれらを組み込みます。 (訳注:これで各ソースでインポートしなくてもよくなりますが、コード品質ツールはそんなことは知らないので文句を言うようになります。)

module.exports = function(defaults) {
  var app = new EmberApp(defaults, {
    // Add options here
  });

  // Use `app.import` to add additional libraries to the generated
  // output files.

  app.import('node_modules/moment/moment.js');
  app.import('node_modules/moment-timezone/builds/moment-timezone-with-data-2012-2022.js');

  return app.toTree();
};

(訳注開始:現在は、JSHint を使っていません。 JSHint では、ES6 と Babel を完全にはサポートしていないそうです。)

.jshintrc ファイルで、predef 配列に moment を追加します。 これにより、Ember CLI アプリに含まれているコード品質ツール jshint が、エラーや潜在的な問題のコードをチェックしながらエラーを投げるのを防ぎます。

"predef": [
    "document",
    "window",
    "-Promise",
    "moment"
  ]

 

(訳注終了)

(訳注開始:現在は、ESLint を使っています。)

.eslintrc.js ファイルで、globals プロパティに moment を追加します。 rules プロパティの前辺りに追加してください。 これにより、Ember CLI アプリに含まれているコード品質ツール ESLint が、エラーや潜在的な問題のコードをチェックしながらエラーを投げるのを防ぎます。 エラーといっても、内容を確認して問題なさそうなら、無視して問題ありませんが、確認項目が少ないことは良い事です。

  globals: {
    "moment": false
  },

(訳注終了)

 

警告: これまでは、サーバーをリフレッシュまたは再起動することなく、ブラウザーに自動的に反映されたアプリの変更を確認できました。 ただし、ember-cli-build.js を編集するたびに、Ember サーバーを再起動する必要があります。 ターミナルまたはコマンドラインから、サーバーを停止し、コマンド ember serve を実行してサーバーを再起動します。 (訳注:node_modules/ember-localforage-adapter の下の node_modules ディレクトリがサーバーの再起動で復活するようですが、サーバーの停止して削除すれば、それ以上は復活しないようです。 また、原因不明で Build Error が発生することがあります。 このときも停止して再起動です。 サーバーの停止は、Ctrl-C(Ctrl キーを押しながら C キーを押す)です。)

(訳注開始:ここからは、Ember のバージョンアップで使えなくなった機能の代わりを用意します。)

ヘルパーの作成

ここでは、テンプレートやコンポーネントで使えるヘルパーを作成します。 プロジェクトのルートディレクトリで次のコマンドを入力してください。

ember generate helper is-equal

次に、app/helpers/is-equal.js を次のように更新します。

import { helper } from '@ember/component/helper';

export function isEqual([leftSide, rightSide]/*, hash*/) {
  return leftSide === rightSide;
}

export default helper(isEqual);

コンポーネントの作成

ここでは、テンプレートで使うコンポーネントを作成します。 プロジェクトのルートディレクトリで次のコマンドを入力してください。

ember generate component select-tz

次に、app/components/select-tz.js を次のように更新します。

import Component from '@ember/component';

export default Component.extend({
  timezones: null,
  selectedTimezone: null,

  didInitAttrs(attrs) {
    this._super(...arguments);
  },

  actions: {
    change() {
      const changeAction = this.get('action');
      const selectedEl = this.$('select')[0];
      var selectedIndex = selectedEl.selectedIndex;
      const timezones = this.get('timezones');
      const selectedTimezone = timezones[selectedIndex];
      this.set('selectedTimezone', selectedTimezone);
      changeAction(selectedTimezone);
    }
  }
});

次に、app/templates/components/select-tz.hbs を次のように更新します。 先ほど作成した is-equal ヘルパーをここで使います。

<select {{action "change" on="change"}}>
  {{#each timezones as |tz|}}
    <option
      value={{tz.offset}}
      selected={{is-equal tz.name selectedTimezone.name}}
    >
      {{tz.name}}
    </option>
  {{/each}}
</select>

(訳注終了)

タイムゾーンのモデルとのやりとり

このアプリでは、選択メニューからタイムゾーンを追加したり、以前に選択したタイムゾーンを削除することができます。 前述のように、Ember のコントローラを使用してデータを操作できます。 時計を作成するときは、現在の現地時間に関する情報を clock のコントローラから clock のテンプレートに送りました。 この例では、ユーザーとの対話を通じて、timezones のテンプレートから timezones のコントローラに情報を送ります。

Moment.js からタイムゾーンのデータを追加し、 "add" と "remove" の2つの actions を実装する timezones のコントローラを作成しましょう。 まず、プロジェクトのルートディレクトリの中で次のコマンドを実行して、タイムゾーン用の新しいコントローラを生成してください。

ember generate controller timezones

次に、app/controllers/timezones.js を次のように更新します。

import Controller from '@ember/controller';

export default Controller.extend({
    /* create array of timezones with name & offset */
    init() {
        this._super(...arguments);
        var timezones = [];
        for (var i in moment.tz._zones) {
          var zone = moment.tz.zone(i);
          timezones.push({
            name: zone.name,
            offset: zone.offsets[0]
          });
        }
        this.set('timezones', timezones);
        this.set('selectedTimezone', timezones[0]);
      },
      actions: {
        /* save a timezone record to our offline datastore */
        add() {
          const selectedTimezone = this.get('selectedTimezone');
          var timezone = this.store.createRecord('timezones', {
            name: selectedTimezone.name,
            offset: selectedTimezone.offset
          });
          timezone.save();
        },        
        /* delete a timezone record from our offline datastore */
        remove(timezone) {
          timezone.destroyRecord();
        }
      }
});

次に、作成した actions と変数を使用するように、 timezones のテンプレートを変更します。 先ほど作成した select-tz コンポーネントも使います。 {{action}} ヘルパーを使って add メソッドと remove メソッドを呼び出すことができます — app/templates/timezones.hbs を更新すると、次のようになります。

<h2>Add Timezone</h2>

<div>
  {{select-tz
    timezones=timezones
    selectedTimezone=selectedTimezone
    action=(action (mut selectedTimezone))
  }}
</div>

<button {{action "add"}}>Add Timezone</button>

<h2>My Timezones</h2>

<ul>
  {{#each model as |timezone|}}
    <li>
      {{timezone.name}} <button {{action "remove" timezone}}>Delete</button>
    </li>
  {{/each}}
</ul>

これで、http://localhost:4200/timezones に timezones のルートが追加され、追跡したいタイムゾーンを追加したり削除したりすることができます。 このデータは、アプリをリフレッシュしても持続します。

タイムゾーンの対比

私たちが最後にする必要があるのは、clock のルートでの現地時間との相対的な時間を表示することです。 これを行うには、clock のルートに timezones のモデルを読み込む必要があります — app/routes/clock.js の内容を次のように更新します。

import Route from '@ember/routing/route';

export default Route.extend({
    model() {
        return this.store.findAll('timezones');
    }
});

clock のコントローラでは、moment.js で各タイムゾーンの現在時刻を更新するだけでなく、現地時刻も更新します — app/controllers/clock.js の内容を次のように更新します。

import Controller from '@ember/controller';
import { later } from '@ember/runloop';

export default Controller.extend({
    init() {
        this._super(...arguments);
        // Update the time.
        this.updateTime();
    },
 
    updateTime() {
        var _this = this;
 
        // Update the time every second.
        later(function() {
            _this.set('localTime', moment().format('h:mm:ss a'));

            _this.get('model').forEach(function(model) {
                model.set('time',
                          moment.tz(model.get('name')).format('h:mm:ss a'));
            });

            _this.updateTime();
        }, 1000);
    },
 
    localTime: moment().format('h:mm:ss a')
});

最後に、{{each}} ヘルパーを clock のテンプレートに追加して、model のタイムゾーンを繰り返し、name プロパティと time プロパティをビューに出力します — app/templates/clock.hbs を次のように更新します。

<h2>Local Time: <strong>{{localTime}}</strong></h2>
 
<ul>
  {{#each model as |timezone|}}
    <li>{{timezone.name}}: <strong>{{timezone.time}}</strong></li>
  {{/each}}
</ul>

これでアプリは機能完成(feature-complete)になるはずです: 選択された各タイムゾーンの現地時間を表示して更新するオフライン対応のアプリです。

ドキュメントのタグと貢献者

このページの貢献者: Wind1808
最終更新者: Wind1808,