今回はこちらのページに沿って進めていきます。訳すのが難しい箇所があったので、自分の言葉で書いている箇所がいくつかあります。英語が読める方は原文を参照してください。。
###イントロ
前回はビューをコンポーネントとして分離させました。これはスケーラブルでナイスな構造へ向けた大きな一歩です。また、PageControl
を使って、ページ間の簡単な移動も行いました。
しかし、PageControl
は横並びの2つのビューをスワイプで移動できる点は有効ですが、完璧ではありません。たとえば、ハイキング編集ビューは編集するハイキングがない場合、スワイプの意味がありません。PageControl
は基本的に最初に全てのビューをインスタンス化し、(画面に表示されていなくても)そのビューを削除することなくずっと裏で保持し続けます。アプリ全体で、メモリをそんなに消費しない場合や、必要なときだけインスタンス化したいということがない場合には有効です。
とにかく、Fuseは他の簡単な概念と一緒にNavigator
とRouter
を扱うツールを提供しています。今回はそれらの取り込み方について説明していきます。
###Navigator
に移行しよう
PageControl
同様、Navigator
もナビゲーション用のコンテナです。ナビゲートできるコンポーネントを含むことができるコントロールであると言えます。ただし、PageControl
とは異なり、Navigator
は要求に応じてテンプレートを使用して、都度子要素をインスタンス化します。つまり、各ページをインスタンス化し、リサイクルすることで、メモリリソースを節約することができます。PageControl
のように、デフォルトでスワイプ操作を持ってはいませんが、逆に多様な表現ができると言ってよいでしょう。
Navigator
を使うには、どのコンポーネントをナビゲートするかを指示しなくてはなりません。ここでRouter
が登場します。すこしややこしいので、実際にコードを変更しながら見ていきたいと思います。早速Navigator
を追加しましょう。
前回追加したPageControl
をNavigator
に変更しましょう。
<App>
<ClientPanel>
<Navigator>
<HomePage />
<EditHikePage />
</Navigator>
</ClientPanel>
</App>
<Navigator>
に含まれる子要素は基本的にテンプレートです。インスタンス化されません。子要素をテンプレートとして扱うため、子要素に属性を追加します。
<App>
<ClientPanel>
<Navigator>
<HomePage ux:Template="home" />
<EditHikePage ux:Template="editHike" />
</Navigator>
</ClientPanel>
</App>
ここで設定した属性ux:Template
に、与えられた値(ここではhomeとeditHike)をもとに、テンプレートからインスタンス化します。例えばNavigator
がhome
へナビゲーションするよう指示すると、HomePageクラスのインスタンスが(まだ生成されていない場合は)生成され、そこにナビゲートされます。editHikeの場合も同様です。Template
属性に与えられた値が重複していない限り、テンプレートを追加することができます。(重複した場合は、どれも認識してくれません。)
さて、この状態で保存すると、表示は消えてしまいますね。先ほども説明した通り、<Navigator>
の子要素はインスタンスではなく、テンプレートなので、まだビューが生成されていません。通常は、特定のルートに遷移することでインスタンス化を行いますが、デフォルトのパスを指定することもできます。デフォルトを設定しておくと、まだその子要素にナビゲートしていない場合は自動でナビゲートされます。今回のアプリではデフォルトを設定しておくのがよいでしょう。デフォルトにホーム画面を設定し、ハイキングを編集すると、編集ビューに移動するように作ってみましょう。
デフォルトのパスを設定したい<Navigator>
にDefaultPath
属性を設定します。
<App>
<ClientPanel>
<Navigator DefaultPath="home">
<HomePage ux:Template="home" />
<EditHikePage ux:Template="editHike" />
</Navigator>
</ClientPanel>
</App>
これで、起動時にホーム画面が表示されるはずです。当然、editHike
を指定すれば、編集ビューが最初に表示されるようにすることもできます。
###Router
でルーティング
Navigator
とテンプレートの準備が終わったので、今度はRouter
を追加します。Router
はアプリ内のどの画面に移動するかということと、実際にそこに移動することを管理します。具体的には、Router
は与えられたルートから、移動先の「ターゲット」を決定し、場合によってはデータを追加するなどして、ターゲットまで移動します。ルーターの出口を見つけるために、ツリー構造をさがして、実際にナビゲーションします。PageControl
とNavigator
は両方ともRouter
の出口となりえます。Router
は今までのルートの履歴を持っていて、必要であれば戻ることも可能です。
いろいろと説明しましたが、その分Router
にはいろいろな機能があり、強力であることを示しています。使い方はわりと簡単なので、実際にアプリに追加して、2つのページ間の移動を実装してみましょう。
まずはRouter
のインスタンスを作るところからです。アプリ全体でひとつのRouter
を参照していく形でつかうので、<App>
の最上位に配置します。また、他のビューから参照するためのName
も設定しておきましょう。
<App>
<Router ux:Name="router" />
<ClientPanel>
<Navigator DefaultPath="home">
<HomePage ux:Template="home" />
<EditHikePage ux:Template="editHike" />
</Navigator>
</ClientPanel>
</App>
これでインスタンス化できます。次に、HomePage
からEditHikePage
に移動するための準備をします。これを行うにはいくつかの方法がありますが、ベストプラクティスはDependecy
をうまく使うことです。コンポーネントが適切に機能するために必要な情報を追加することができます。コンポーネントの一つにDependecy
を追加すると、Fuseはその関係を満たすためにいろいろな設定を行います。うまくいかない場合はコンパイルエラーとなります。これにより、依存関係が常に保たれ、正しいコードを保証することができます。
ではそのDependecy
を追加してみましょう。まずはDependency
属性を追加して、依存関係にあるコンテキストの名前を指定します。
<Page ux:Class="HomePage">
<Router ux:Dependency="router" />
Router
インスタンスを追加するのによく似ていますが、これは依存関係を定義しているだけです。あとは、MainView.ux
の方にこれを公開すれば、依存関係ができあがります。
<App>
<Router ux:Name="router" />
<ClientPanel>
<Navigator DefaultPath="home">
<HomePage ux:Template="home" router="router" />
<EditHikePage ux:Template="editHike" />
</Navigator>
</ClientPanel>
</App>
小文字で指定する傾向があること以外は、他の属性値と同じように指定できます。このrouter
を指定しないと、コンポーネントの依存関係が満たされず、コンパイルエラーとなります。
それでは、今実装したRouter
をつかって、EditHikePage
に遷移してみましょう。宣言しておいたchooseHike
メソッドを書き換えていきましょう。
まずはメソッドの名前をgoToHike
に変えます。このとき、module.export
で公開するときの名前や、HomePage.ux
から参照する場合の名前も変えておいてください。そして引数からハイキングオブジェクトを取得します。
function goToHike(args) {
var hike = args.data;
}
ハイキングを選択したときの遷移先も設定する必要があるので、Router
のdependancy
を取得します。幸い、Fuseでは、ux:Dependency
の設定したRouter
をJSから参照することができます。ここで参照するRouter
は、実際にはMainView.ux
からHomePage.ux
のDependency
に渡されたRouter
と同じものになります。
参照したRouter
に、それぞれのEditHikePage
に移動するよう指示します。これを行うには、push
関数を実行します。
function goToHike(args) {
var hike = args.data;
router.push("editHike");
}
push
は指定されたルートに実際にナビゲートするメソッドです。指定する値はux:Template
に指定した値をセットします。今回は少し前に設定した「editHike」と指定しました。プレビューを確認すると期待通りの動作をしていることがわかります。もちろん、遷移時にデータを送っていないので、中身は空のままです。ここで初めてNavigator
によりeditHikePage
のインスタンスが作成されます。
さて、ハイキングを選択して、編集ビューに行くことはできましたが、そこから戻る機能を実装しなくてはいけません。戻るボタンを追加します。
まずはHomePage
と同じように、EditHikePage
にDependency
を追加する必要があります。
<Page ux:Class="EditHikePage">
<Router ux:Dependency="router" />
そして、MainView.ux
の方も設定します。
<Navigator DefaultPath="home">
<HomePage ux:Template="home" router="router" />
<EditHikePage ux:Template="editHike" router="router" />
これで、ルーティングはできたので、あとは戻るボタンを追加して、Clickハンドラーを設定します。
<TextView Value="{comments}" TextWrapping="Wrap" />
<Button Text="Back" Clicked="{goBack}" />
</StackPanel>
</ScrollView>
</Page>
JSファイルに実際にページ移動を実行するメソッドを定義します。
function goBack() {
router.goBack();
}
module.exports = {
...
goBack: goBack
};
router.goBack
は直前のルートへ戻るように指示します。Fuseでは、router.push
で移動した場合は履歴をためることができるので、前の画面に戻ることができます。しかし、router.goto
を使うと、移動はできますが、その履歴は蓄積されないことに注意してください。
goBack
は自動的に、端末のハードウェア戻るボタン(があれば)と連動します。PCでプレビューしている場合は、Cmd + B(OS X)または、Ctrl + B(Windows)でシミュレートすることができます。
ここまでで、一通りの画面遷移が完成しました!
ページ間でデータをやりとりする
最後に、画面遷移時に選択したハイキングの情報を編集ビューに渡すことをやりたいと思います。これを実現するには、「ホーム画面から編集ビューにデータを送ること」と「編集ビューでデータを受け取ること」の二つの実装が必要になります。
これは非常に簡単です。router.push
を実行する際に、ハイキングデータオブジェクトを追加してあげれば、Router
はデータを一緒に送信してくれます。今回のデータは全てシンプルな文字列データのみ含まれているので、他の機能を実装する必要がありません。
ハイキングデータオブジェクトを追加するには、下記のように修正します。
function goToHike(arg) {
var hike = arg.data;
router.push("editHike", hike);
}
これで、editHike
に遷移するとともに、データをわたすことができます。(この場合、EditHikePage
のインスタンスにデータが渡されます。)
このチュートリアルでは単純なナビゲーションしか使用しませんが、マルチレベルのナビゲーション()も可能です。詳細についてはこちらを参照してください。
今度は受け取る側の実装です。FuseはこのためにParameter
と呼ばれる特別なObservable
を用意しています。このParameter
はRouter
経由でコンポーネントに渡される全ての値を示しています。前にEditHikePage.js
に設定したObservable
を確認してみましょう。
var Observable = require("FuseJS/Observable");
var hike = Observable();
...
Parameter
として渡ってきた値をhike
に設定できれば理想的です。これを実現するには、つぎのようにします。
var hike = this.Parameter;
Parameter
はObservable
の一つなので新しいObservable
を作る必要はありません。ハイキングデータオブジェクトをもっているParameter
を使うだけでOKです。なので、Observable
のインポートが削除できる点にも注目です。
先に示したように、Parameter
はthis.Parameter
として使います。これはParameter
がモジュールの1部分となるからです。EditHikePage
の特定のインスタンスを参照するために、「このインスタンスの」という指定を行う必要があります。これはJSのどの部分で行われたかにより、コンテキストが変わる可能性があるので、モジュールのルートの正しいインスタンスを使うことが大事です。
これで、選択したハイキングのページに遷移しつつ、それぞれのデータが表示されていることがわかると思います!
###今回の成果
今回は、ページ間の遷移とデータの受け渡しを行いました!
######公式サイトの動画拝借しちゃいました…
今回修正したファイルは下記です。
<App>
<Router ux:Name="router" />
<ClientPanel>
<Navigator DefaultPath="home">
<HomePage ux:Template="home" router="router" />
<EditHikePage ux:Template="editHike" router="router" />
</Navigator>
</ClientPanel>
</App>
<Page ux:Class="HomePage">
<Router ux:Dependency="router" />
<JavaScript File="HomePage.js" />
<ScrollView>
<StackPanel>
<Each Items="{hikes}">
<Button Text="{name}" Clicked="{goToHike}" />
</Each>
</StackPanel>
</ScrollView>
</Page>
var hikes = require("hikes");
function goToHike(args) {
var hike = args.data;
router.push("editHike", hike);
}
module.exports = {
hikes: hikes,
goToHike: goToHike
}
<Page ux:Class="EditHikePage">
<Router ux:Dependency="router" />
<JavaScript File="EditHikePage.js" />
<ScrollView>
<StackPanel>
<Text>Name:</Text>
<TextBox Value="{name}" />
<Text>Location:</Text>
<TextBox Value="{location}" />
<Text>Distance (km):</Text>
<TextBox Value="{distance}" InputHint="Decimal" />
<Text>Rating:</Text>
<TextBox Value="{rating}" InputHint="Integer" />
<Text>Comments:</Text>
<TextView Value="{comments}" TextWrapping="Wrap" />
<Button Text="back" Clicked="{goBack}" />
</StackPanel>
</ScrollView>
</Page>
var hike = this.Parameter;
var _name = hike.map(function(x) { return x.name; });
var _location = hike.map(function(x) { return x.location; });
var _distance = hike.map(function(x) { return x.distance; });
var _rating = hike.map(function(x) { return x.rating; });
var _comments = hike.map(function(x) { return x.comments; });
function goBack() {
router.goBack();
}
module.exports = {
name: _name,
location: _location,
distance: _distance,
rating: _rating,
comments: _comments,
goBack: goBack
};
###今回登場したFuseの機能たち
その他、DefaultPath
属性、Dependecy
属性、Parameter
の使い方などがありました。だいぶアプリっぽくなってきましたね。
###明日は
いい感じに動いてはいますが、実際にModelのデータを変更することができていません。なので、バックエンドのモックアップを作る作業をします。いつか実際のバックエンドを追加するときを想定して、アプリの構造を最適化していきましょう。