0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FusetoolsAdvent Calendar 2016

Day 13

【Fuse】カスタムコンポーネントを作ってよりシンプルに

Posted at

今回はこちらのページに沿って進めていきます。

###イントロ

前回はハイキングのデータを複数用意し、各データを選択する用のボタンを追加して、ビューの切り替えを行いました。とても大きな一歩でしたが、今回はコードをより細かく再利用可能なコンポーネントに分割してコードを管理しやすくしようと思います。今回のチュートリアルを通して、実際のアプリ開発プロジェクトの煩雑さを管理するためにヒューズが提供するある機能を活用します。

前回まではボタンと情報が一つのビューにまとまっていましたが、今回はハイキングを選択するビューと、選択したハイキングを編集するビューのを二つに分けます。それに付随して、前回作成したデータモデルも分割して、別々に使用できるようにします。

基礎となるところですので、このチュートリアルは、単純にビューを分けてコンポーネントとするものであることを理解してください。一般的なアプリケーションの開発フローではありません。が、心配いりません。一歩一歩進んでいきましょう。ビューをつなぎ合わせ、分けたビュー同士でデータをやりとりするところは次回説明しますので、このまま進めます。

###ハイキングのデータを分ける

まずは、ハイキングのデータの配列を独自のコンポーネントに分けて、ビューから独立させます。これはただのJSのデータなので、独自モジュールに配置します。モジュールは基本的に自己完結型で、再利用可能な塊とするのが一般的です。

<JavaScript File="myScript.js" />のように記述すると外部JSファイルを読み込めます。これは読み込んだ場所にインラインでJSを書いていくことと同じですが、より綺麗にすることができ、コンポーネントのロジックとUXコードを分離することができます。後ほど詳しく説明します。

しかし、ハイキングの配列を特定のUXファイルに結びつけるのはよくありません。かわりに分離した専用のJSファイルにデータを置き、アプリ全体にこのモジュールをバンドルするよう設定します。そして、各ビューでこのモジュールをインポートします。

さっそく、ハイキングのデータの配列をもったJSファイル(hikes.js)を作成し、プロジェクトのルートに配置します。

$ ls
MainView.ux    hikes.js    tutorial.unoproj

このJSファイルにデータを書く前に、アプリにバンドルさせる方法を確認しましょう。通常、UXコードに<JavaScript>タグを利用してJSをインラインで書く場合、もしくは先ほど説明した、<JavaScript File="myScript.js" />のようにファイル参照する場合、アプリは自動的にJSをバンドルします。しかし、<JavaScript>タグから参照されない、独立したJSモジュールの場合、このコードがプロジェクトの一部であることをFuseに知らせる必要があります。

この時使用するのが.unoprojファイルです。開いてみると、下記のように書かれています。

tutorial.unoproj
{
  "RootNamespace":"",
  "Packages": [
    "Fuse",
    "FuseJS"
  ],
  "Includes": [
    "*"
  ]
}

ご覧の通り、最初はとてもシンプルな内容です。プロジェクトに関する簡単な情報を持っています。ここで注目するのはIncludesのセクションです。現在1つのエントリがあります。これはFuseプロジェクトがデフォルトでたくさんのファイルを自動的に内包することを示しています。*には、.uxファイル、.unoフォルダやその他のファイルが含まれます。ただし、JSファイルは含まれないので、明示的に書いてあげる必要があります。

tutorial.unoproj
{
  "RootNamespace":"",
  "Packages": [
    "Fuse",
    "FuseJS"
  ],
  "Includes": [
    "*",
    "hikes.js:Bundle"
  ]
}

*のあとにカンマを忘れずにいれてください。そしてそのあとにhikes.js:Bundleを追加します。これはFuseに対して、「hikes.jsファイルを知ってるかい?そこにいって、アプリに含めてくれ」と宣言していることになります。

さて、それでは、前回作ったハイキングの配列を、hikes.jsのほうに移動しましょう。.uxからは削除して大丈夫です。

module.exportsの中に配列への参照が残っていてエラーになると思いますが、すぐに修復するのでそのままにしておいて大丈夫です。

コピペが完了したら、新しいモジュール側から、module.exportsを実行します。hikes.jsの一番下に追記します。これでモジュールの完成です。

module.exports = hikes;

それでは、ビューのほうのコードに戻って、importを実行します。方法は今までObservableで行ってきたやり方と同じです。

var Observable = require("FuseJS/Observable");
var hikes = require("hikes");

これで、外部に切り出したハイキングデータの読み込みは完了です。変数名も先ほどのものとあわせておけばエラーも解消されるはずです。外見は特に変わりませんが、コードを見てみると、データモデルが分離され、とても綺麗で管理しやすくなっていますね。

###編集ビューをコンポーネントとして切り出す

さて、ハイキングのデータ配列を切り出すことができましたので、今度はビューを外に切り出したいと思います。これは簡単にできます。まずは、プロジェクト内に新しいPagesというフォルダを作ります。

$ ls
MainView.ux    hikes.js    tutorial.unoproj    Pages

Pagesフォルダの中に、EditHikePage.uxというファイルをつくり、下記のように記述します。

EditHilePage.ux
<Page ux:Class="EditHikePage"></Page>

ここで新しい概念が二つ出てきました。先に進む前に説明します。

<Page>は基本的にナビゲーションに必要な、特別な種類のUI要素です。ここでは<Page>は必須ではなく、<Panel><Button>やその他の要素を使うこともできます。しかし、ナビゲーションを使ってコンポーネントを使用する場合は<Page>を使うのが一般的なベストプラクティスです。後ほど詳細を説明します。

ux:Classは、指定された要素を拡張したクラスを作成することを意味します。今回の場合では、<Page>を拡張したクラスを作成します。オブジェクト指向プログラミングになじみのない人は、しっくりこないかもしれませんが、単純な概念です。基本的に、クラスは要素の一種であると考えられ、インスタンスはそのクラスの要素です。例えば、<Button>はクラスです。UXコード内で<Button />と記述すると、<Button>のインスタンスが作成されます。<Button>クラスには、要素がどのような見た目でどのような機能を持っているかが定義されていて、.uxに<Button />と記述することで、ビューの中にそのインスタンスが存在していることを定義しています。

しかし、このコンテキストでは拡張は何を意味するのでしょうか。<Page>と同じ見た目、機能を持った別の<EditHikePage>クラスを定義したことを意味します。以降、このクラスを使うときは、<EditHikePage />と記述すれば、インスタンスをビューに追加することができます。

このやり方に関しては、Creating Componentsのページにより詳しく書いてあります。

さて、それでは、既存のコードをみていきましょう。

一番上に<App>があり、その次に<ClientPanel>があります。その中に含まれる要素が具体的な見た目となる部分です。この部分を新しく作ったファイルに移行しますので、MainViewからは削除します。

MainView.ux
<App>
    <ClientPanel>
    </ClientPanel>
</App>

切り取った部分をコンポーネント化します。

EditHikePage.ux
<Page ux:Class="EditHikePage">
	<JavaScript>
		var Observable = require("FuseJS/Observable");
		var _hikes = require("hikes");

		var hike = Observable();
		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 chooseHike(_item) {
			hike.value = _item.data;
		}

		module.exports = {
			hikes: _hikes,
			name: _name,
			location: _location,
			distance: _distance,
			rating: _rating,
			comments: _comments,
			chooseHike: chooseHike
		};
	</JavaScript>
	<ScrollView>
		<StackPanel>
			<Each Items="{hikes}">
			    <Button Text="{name}" Clicked="{chooseHike}" />
			</Each>
			<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" />
		</StackPanel>
	</ScrollView>
</Page>

これで、各ボタンと編集ビューからなるコンポーネントが出来上がりました。これをインスタンス化し、MainView.uxに追加します。

MainView.ux
<App>
    <ClientPanel>
        <EditHikePage />
    </ClientPanel>
</App>

これで保存すればプレビューに反映されているはずです。(プロジェクト内にある.uxファイルは自動でバンドルされるので、.jsのようにプロジェクトファイルに追記する必要はありません。)

さて、コンポーネントについてもう1つやりたいことがあります。<JavaScript>の部分をさらに分けることです。これは非常に簡単です。Pagesフォルダ内に新しくEditHikePage.jsファイルを作成して、<JavaScript>の中身だけ移動します。そして、残った<JavaScript>タグを下記のように変更します。

<JavaScript File="editHikePage.js" />

これで、JSをそとに切り出すことができました。見た目に変化はありませんが、中身はどんどん管理しやすくなっています。

###ホーム画面を分ける

さて、いろいろコンポーネント化してきましたが、editHikePage.uxにはまだ二つのコンポーネントが含まれてしまっています。ボタン群と、編集ビューです。一般的に、コンポーネントを作る時は、コンポーネント一つ一つをできるだけシンプルにし、ビューなどは複数含めないようにすることをお勧めします。これからやることは、セレクタークラスをビューから分離させて、HomePageクラスを作ります。

具体的な内容は今までやってきたこととほとんど同じです。まず、空のPageクラスを拡張してHomePageクラスを作り、Pagesディレクトリに入れます。

HomePage.ux
<Page ux:Class="HomePage"></Page>

そしたら、UXコードからボタンを生成している箇所を移行します。

HomePage.ux
<Page ux:Class="HomePage">
	<Each Items="{hikes}">
   		<Button Text="{name}" Clicked="{chooseHike}" />
	</Each>
</Page>

またハイキングの数はどんどん増えていく予定なので、<StackPanel><ScrollView>も含めます。

HomePage.ux
<Page ux:Class="HomePage">
	<ScrollView>
		<StackPanel>
			<Each Items="{hikes}">
	   			<Button Text="{name}" Clicked="{chooseHike}" />
			</Each>
		</StackPanel>
	</ScrollView>
</Page>

この状態で保存すると、2つのことに気づきます。まずは新しく作ったHomePage.uxはまだhikesを参照できません。そこで若干JSを追加します。

少し前にやったようにHomePage.jsを新しく作り、HomePage.uxと同じディレクトリに配置します。

HomePage.js
var hikes = require("hikes");

function chooseHike(args) {
}

module.exports = {
	hikes: hikes,
	chooseHike: chooseHike
}

このコードはEditHikePage.jsに含まれるものと同じものですが、chooseHikeメソッドをコメントアウトしています。次の章で説明しますが、現状はすべてのハイキングを同じビューに表示することはないので、EditHikePage.jsのインポート、エクスポートの部分を削除しておきます。

EditHikeView.js
var Observable = require("FuseJS/Observable");

var hike = Observable();
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; });

module.exports = {
	name: _name,
	location: _location,
	distance: _distance,
	rating: _rating,
	comments: _comments
};

少し前に作ったHomePage.jsHomePage.uxの方に追加します。

HomePage.ux
<Page ux:Class="HomePage">
	<JavaScript File="HomePage.js" />
	<ScrollView>
		<StackPanel>
			<Each Items="{hikes}">
	   			<Button Text="{name}" Clicked="{chooseHike}" />
			</Each>
		</StackPanel>
	</ScrollView>
</Page>

###ホーム画面を表示する

複数のページを作るところまではできたので、各画面の遷移を実装します。Fuseでは、NavigatorRouterコンポーネントを使って実装します。これらのコンポーネントをちゃんと説明するには少し時間がかかるので、次の章に回します。

とりいそぎ、より簡単に実装できるPageControlを使用してみます。PageControlは横並びになっているページ間をスワイプ操作で遷移する場合にとても有用です。次のようにインスタンスを追加します。

MainView.ux
<App>
	<ClientPanel>
		<PageControl>
			<HomePage />
			<EditHikePage />
		</PageControl>
	</ClientPanel>
</App>

<pageControl>内の子要素がそれぞれ画面幅と同じ幅を持ち、かつ横並びに設定されます。セーブすれば、各画面をスワイプで移動できるようになっているはずです。

###今回の成果

今回はひとつにまとめられていたビューや機能をそれぞれコンポーネント化し、分離させました。最後に簡単なページ遷移を実装しました。

01.gif

長くなりますが、念のため全コードを下記に掲載しておきます。

MainView.ux
<App>
	<ClientPanel>
		<PageControl>
			<HomePage />
			<EditHikePage />
		</PageControl>
	</ClientPanel>
</App>
hikes.js
var hikes = [
    {
        id: 0,
        name: "Tricky Trails",
        location: "Lakebed, Utah",
        distance: 10.4,
        rating: 4,
        comments: "This hike was nice and hike-like. Glad I didn't bring a bike."
    },
    {
        id: 1,
        name: "Mondo Mountains",
        location: "Black Hills, South Dakota",
        distance: 20.86,
        rating: 3,
        comments: "Not the best, but would probably do again. Note to self: don't forget the sandwiches next time."
    },
    {
        id: 2,
        name: "Pesky Peaks",
        location: "Bergenhagen, Norway",
        distance: 8.2,
        rating: 5,
        comments: "Short but SO sweet!!"
    },
    {
        id: 3,
        name: "Rad Rivers",
        location: "Moriyama, Japan",
        distance: 12.3,
        rating: 4,
        comments: "Took my time with this one. Great view!"
    },
    {
        id: 4,
        name: "Dangerous Dirt",
        location: "Cactus, Arizona",
        distance: 19.34,
        rating: 2,
        comments: "Too long, too hot. Also that snakebite wasn't very fun."
    }
];

module.exports = hikes;
EditHikePage.ux
<Page ux:Class="EditHikePage">
	<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" />
		</StackPanel>
	</ScrollView>
</Page>
EditHikePage.js
var Observable = require("FuseJS/Observable");

var hike = Observable();
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; });

module.exports = {
	name: _name,
	location: _location,
	distance: _distance,
	rating: _rating,
	comments: _comments
};
HomePage.ux
<Page ux:Class="HomePage">
	<JavaScript File="HomePage.js" />
	<ScrollView>
		<StackPanel>
			<Each Items="{hikes}">
			    <Button Text="{name}" Clicked="{chooseHike}" />
			</Each>
		</StackPanel>
	</ScrollView>
</Page>
HomePage.js
var hikes = require("hikes");

function chooseHike(args) {
}

module.exports = {
	hikes: hikes,
	chooseHike: chooseHike
}

###今回登場したFuseの機能たち

###明日は
セレクターをタップしたら、それに応じた編集ビューが表示されるように改修していきます。今回名前だけ紹介したNavigatorRouterの詳細を説明していきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?