モックアップという意味を持つ Maqetta: 第 2 回 Maqetta でデザインしたモバイル UI 用に JavaScript でカスタム・コードを作成する

JavaScript と Dojo Toolkit を使用してインタラクティブな UI プロトタイプを開発する

連載第 1 回で学んだように、Maqetta はコードを一切作成することなく洗練されたデスクトップ UI やモバイル UI を簡単にデザインできるようにする、WYSIWYG アプリケーションですが、Maqetta でデザインした UI よりも先進的な方法でユーザー入力に応答する、もっとリッチな UI が必要な場合にはどうすればよいのでしょう?この記事では前回の続きとして、Maqetta でデザインしたモバイル UI の機能を、Dojo と Dojo Mobile ライブラリーを使用して JavaScript でカスタム・コードを作成することで強化するプロセスについて、著者の Tony Erwin がステップバイステップで説明します。

Tony Erwin, Software Engineer, IBM

Tony Erwin - author photoTony Erwin は、IBM Emerging Internet Technologies グループに所属するソフトウェア・エンジニアで、Maqetta 開発チームの主力メンバーです。1998年に IBM に入社して以来、さまざまな技術とツールキットを使用して広範な UI デザインと開発経験を積んでいます。IBM に入社する前は、インディアナ大学でコンピューター・サイエンスの修士号、ローズハルマン工科大学でコンピューター・サイエンスの学士号を取得しました。



2013年 6月 13日

はじめに

この連載について

この連載では、Maqetta を使用して HTML5 によるユーザー・インターフェースのプロトタイプを作成する方法を紹介します。

  • 第 1 回では、リッチ・モバイル・アプリのプロトタイプを作成する手順を通して、Maqetta の主な機能について学びます。
  • 今回の記事では、第 1 回で作成したプロトタイプ・アプリを次のレベルに拡張するために、JavaScript でカスタム・コードを作成してインタラクティブな機能を追加します。
  • 近日中に公開される第 3 回では、PhoneGap を使用して、Maqetta で生成されたモバイル・プロトタイプを実際の端末にすぐにデプロイできるネイティブ・アプリに変身させます。

developerWorks の Tony のブログで Maqetta の使い方を詳しく学んでください。

この連載の第 1 回を読めば、Maqetta がデスクトップ UI とモバイル UI をデザインして開発するためのブラウザー・ベースのアプリケーションであることがわかります。前回の記事では、Maqetta のドラッグ・アンド・ドロップ式インターフェースを使用して、コードを一切作成することなくリッチなモバイル UI プロトタイプをデザインする方法を説明しました。サンプルとして開発したプロトタイプはライブ・バージョンですが (つまり、ユーザーがプロトタイプの機能を操作できます)、ビューに表示されるデータの大半は静的なものでした。ほとんどの概念実証にはこのようなプロトタイプで十分ですが、場合によっては (例えば、自分で考案した概念をクライアントや潜在的投資家に売り込む場合)、実際のアプリの動作をより忠実にデモンストレーションするプロトタイプが必要になることもあります。

今回の記事では、JavaScript でカスタム・コードを作成することにより、体重管理アプリのプロトタイプを次のレベルへと拡張します。更新後のアプリは、Dojo と Dojo Mobile ライブラリーのインタラクティブな機能を利用してユーザー・イベントに応答し、各ビューのウィジェットに表示されるデータを動的に変更するようになります。

第 1 回をまだ読んでいない場合や、これまでに Maqetta による UI プロトタイプを自分で開発したことがない場合には、この記事で説明する例について理解するのは難しいかもしれません。この記事を読む前に、Maqetta による UI 開発の基礎を理解しておくことをお勧めします。


さらにリッチなプロトタイプ

第 1 回で説明した体重管理アプリのフロー (図 1 を参照) を思い出してください。このアプリでは、ユーザーが mainView で体重リストの任意の行をクリックすると、そのクリックした体重エントリーに関する詳細情報を確認および編集できる detailsView が表示されます。このプロトタイプは、視覚的にはリッチでインタラクティブですが、以下に挙げる重要な部分が不足しています。

  • detailsViewdetailsView_Date、および detailsView_Notes には、体重リストで選択した項目に基づくデータが表示されるわけではありません。
  • これらのビューでデータを変更しても、ユーザーが mainView に戻ったときに、メインの体重リストにその変更が反映されません。
  • mainView のプラス (+) ボタンを押しても、実際に新しい項目が体重リストに追加されるわけではありません。

今回の記事では、これらの不足している部分に対処するために、JavaScript で作成したカスタム・コードを体重管理アプリのプロトタイプに追加します。これにより、体重管理アプリに求められる機能に関して、より現実に近いリッチなユーザー・エクスペリエンスを提供するプロトタイプにします。

図 1. 体重管理アプリのフローチャート
体重管理アプリのフローチャート図

ワークスペースのセットアップ

この記事に記載する例は、第 1 回で体重管理アプリの UI プロトタイプ用に作成した index.html ファイルと weights.json ファイルに基づくため、この両方のファイルについて十分に理解していると理想的です。Maqetta を使用してモバイル UI をデザインした経験はないものの、UI 機能をより動的なものにするカスタム JavaScript コードの作成には重点的に取り組みたい場合は、第 1 回のファイルをダウンロードして解凍してから、以下の手順に従って Maqetta ワークスペースにアップロードするのでも構いません。

  1. Maqetta の「Files (ファイル)」パレット (ディスプレイの左下) で、ルート・レベルにあるいずれかのファイル (例えば、app.js) を右クリックし、「Upload files... (ファイルをアップロード…)」オプションを選択します。
  2. 「Upload Files (ファイルのアップロード)」ダイアログで、「Select Files... (ファイルを選択...)」をクリックします。すると、それぞれの OS に固有のダイアログが開き、お使いのファイルシステムからファイルを選択することができます。アップロードするファイル (index.html および weights.json) を選択すると、それらのファイルが「Upload Files (ファイルのアップロード)」ダイアログのリストに追加されます (図 2 を参照)。
    図 2. ファイルのアップロード
    「Upload Files (ファイルのアップロード)」ダイアログのスクリーン・ショット
  3. Upload (アップロード)」ボタンをクリックします。これで、ファイルがアップロードされて、「Files (ファイル)」パレットに表示されます。アップロードしたファイルは、自分で作成したファイルと同じように、直接 Maqetta で使用することができます。

Maqetta の HTML ソース

Maqetta によるプロトタイプ用の JavaScript コードを作成するには、その前に、このプロトタイプのコードとして生成された HTML ソースを十分に理解しておく必要があります。そこで、まずは index.html のソースを表示してみましょう。

  1. Maqetta ページ・エディターで index.html を開きます。
  2. ソースだけを表示したい場合 (デザイン・キャンバスを非表示にする場合) は、Maqetta ツールバーで「Source (ソース)」ボタンをクリックします。「Design (デザイン)」をクリックすると、デザイン・キャンバスが再表示されます。
  3. ファイルのソースとデザイン・キャンバスを並べて表示するには、「Source (ソース)」ボタンの横にあるプルダウン・メニューを開いて (図 3 を参照)、「Split Vertically (上下に分割)」または「Split Horizontally (左右に分割)」を選択します。選択したオプションは、次回「Source (ソース)」ボタンをクリックしたときのデフォルト・ビューになります。
    図 3. 「Source (ソース)」メニューの表示オプション
    「Source (ソース)」メニューの表示オプションを示すスクリーン・ショット

分割した画面で作業しているときに、デザイン・ペインでウィジェットを選択すると、そのウィジェットのソースをソース・ペインで強調表示できることを覚えておいてください。図 4 では、mainViewEdgeToEdgeDataList を選択して、このウィジェットの HTML をソース・ペインで強調表示しています。

図 4. 分割された画面に表示されたソース・ペイン
分割された画面に表示されたソース・ペイン

HTML ソースの内容

Maqetta によって生成された HTML ソースが最初に実行することの 1 つは、Dojo のセットアップです。HTML がロードされると、dojo/parser (詳細については「参考文献」を参照) がその文書の本体にある HTML タグを構文解析し、その内容に従って Dojo ウィジェットを作成します。これらの Dojo ウィジェットは JavaScript オブジェクトであり、カスタム JavaScript コードで参照して操作することができます。

Dojo リファレンス・ガイド

この記事で開発するリッチなモバイル・アプリ UI では、Dojo ToolkitDojo パッケージ、Dijit パッケージ、および Dojo Mobile (dojox/mobile) パッケージにある機能とウィジェットを利用します。これらのコンポーネントについて詳しく学ぶには、Dojo Toolkit Reference Guide (この記事を執筆している時点での最新バージョンは 1.8) を参照してください。

リスト 1 に、index.html から抜粋した mainView を構成する部分の HTML スニペットを記載します。この HTML スニペットは、最初の行で HTML <div> 要素を定義するところから始まります。data-dojo-type 属性の値によって、Dojo パーサーに対し、Dojo Mobile ライブラリー (「参考文献」を参照) に含まれる JavaScript クラスの 1 つ、dojox/mobile/ScrollableView のインスタンスを作成するように指示していることに注意してください。このインスタンスは、mainViewScrollableView ウィジェットを表します。

IDmainView に設定されている点も重要です (この ID は、第 1 回でプロパティー・パレットを使って設定したものであることを覚えているでしょうか)。Dojo ウィジェットのタイプと ID がわかれば、ウィジェットの実行時の動作を変更するための JavaScript コードを作成するのは比較的簡単です。これについては、この後すぐ説明します。

リスト 1. index.html から抜粋した mainView 用のスニペット
<div data-dojo-type="dojox.mobile.ScrollableView" id="mainView" 
        keepScrollPos="false" scrollBar="true" selected="selected">
    <h1 label="Weight Tracker" data-dojo-type="dojox.mobile.Heading" fixed="top">
        <div label="+" data-dojo-type="dojox.mobile.ToolBarButton" 
                moveTo="detailsView" style="float: right;"></div>
    </h1>
    <span data-dojo-type="dojo.data.ItemFileReadStore" id="ItemFileReadStore_1" 
            jsId="ItemFileReadStore_1" url="weights.json"></span>
    <ul data-dojo-type="dojox.mobile.EdgeToEdgeDataList" store="ItemFileReadStore_1" 
            query="{"label":"*"}"></ul>
</div>

リスト 1 に示されているその他の要素にはお馴染みの属性が使用されています。例えば、mainView 用の <div> 要素の中には、以下の要素が含まれています。

  • ラベルが “Weight Tracker” に設定された Dojo タイプ dojox/mobile/Heading<h1> 要素。この要素の中には、ラベルが “+” に設定された Dojo タイプ dojox/mobile/ToolBarButton<div> 要素があります。後で、このボタンのクリック・イベントに応答して新しいエントリーを体重リストに追加する JavaScript コードを作成します。
  • Dojo タイプ dojo/data/ItemFileReadStore<span> 要素。この要素は、weights.json の URL を指定しています。
  • store 属性が設定された Dojo タイプ dojox/mobile/EdgeToEdgeDataList<ul> 要素。store 属性は、その前の行で作成された ItemFileReadStore を指していることに注意してください。後で、このリストへの参照を取得して、データストアを変更する JavaScript コードを作成します。

weights.json の更新

カスタム JavaScript コードの作成に取り掛かる前に、weights.json にいくつかのフィールドを新たに追加したり、既に存在するフィールドを更新したりする必要があります。Maqetta ワークベンチの左下にある「Files (ファイル)」パレットで weights.json をダブルクリックして開いてください。そしてこのファイルの内容をリスト 2 に記載する内容に置き換えてから、ファイルを保存します。

リスト 2. weights.json の更新内容
{
    "identifier": "id",
    "items": [
        {id: "weight_0", label: "149", moveTo: "#", rightText: "2012-10-01", 
		notes: "Starting to track my weight."},

        {id: "weight_1", label: "150", moveTo: "#", rightText: "2012-10-09", 
		notes: "Ran 5 miles and ate lots of broccoli!"},

        {id: "weight_2", label: "151", moveTo: "#", rightText: "2012-10-15", 
		notes: "Oops, going in wrong direction."},

        {id: "weight_3", label: "148", moveTo: "#", rightText: "2012-10-23", 
		notes: "Wow, lost 3 pounds!"},

        {id: "weight_4", label: "146", moveTo: "#", rightText: "2012-11-01", 
			notes: "Feeling good!"}
    ]
}

リスト 2 の各行には、1 つの体重エントリーを表すように意図された項目があります。それぞれの項目には、以下の新しい属性および変更された属性が含まれています。

  • id は、体重エントリーを一意に表す ID の役割を果たす新しいフィールドです (新しく追加された行 "identifier": "id" に注目してください)。このフィールドにより、JavaScript 内の個々の項目を参照して変更するのが容易になります。
  • label は、その項目の体重の値を収容する既存のフィールドです。
  • moveTo は既存のフィールドですが、JavaScript で detailsView への遷移を処理できるように、設定を "#" に変更してあります。
  • rightText は、その項目の日付を収容する既存のフィールドです。
  • notes は、その項目のテキスト・メモを収容する新しいフィールドです。

この記事の本題は JavaScript コードを作成することなので、weights.jsonItemFileReadStore の項目構造 (「参考文献」を参照) に従っているということをお伝えしておきます。HTML ソースを調べると、ItemFileReadStoreEdgeToEdgeDataList で使用するデータストアの種類であることがわかります。


Maqetta での JavaScript コード

この記事の残りの部分では、体重管理アプリの app.js ファイルに、JavaScript によるカスタム・コードを追加していきます。app.js は、すべての Maqetta プロジェクトにデフォルトで提供されるファイルです。index.html (または Maqetta によって生成された任意の HTML ファイル) のソースを調べると、そのソース内にある以下の行で app.js をロードすることがわかります。

<script type="text/javascript" src="app.js"></script>

まずは、プロジェクトで app.js ファイルを開きます。それには、「Files (ファイル)」パレットでこのファイルをダブルクリックします。ファイルの内容は、リスト 3 のようになっているはずです。

リスト 3. デフォルト app.java ファイル
/*
 * This file is provided for custom JavaScript logic that your HTML files might need.
 * Maqetta includes this JavaScript file by default within HTML pages authored in
 * Maqetta.
 */
require(["dojo/ready"], function(ready){
     ready(function(){
         // logic that requires that Dojo is fully initialized should go here

     });
});

ご覧のように、app.js ファイルは dojo/ready を使用します。ready 関数の中に配置するコードは、必要な Dojo リソースが完全にロードされて、ページが構文解析されて、指定された Dojo ウィジェットが作成されてからでないと実行されません (dojo/ready の詳細を学ぶには、「参考文献」を参照してください)。

単純な app.js のデモ

まずは手始めに、app.js にアラート・メッセージを追加して、アプリがブラウザーにロードされるとそのメッセージが表示されるようにします。

  1. リスト 4 に示されているような単純なアラート文を app.js に追加します。
    リスト 4. app.js に追加するアラート
    /*
     * This file is provided for custom JavaScript logic that your HTML files might need.
     * Maqetta includes this JavaScript file by default within HTML pages authored in   
     * Maqetta.
     */
    require(["dojo/ready"], function(ready){
         ready(function(){
             // logic that requires that Dojo is fully initialized should go here
    
             //Add a temporary alert just to make sure we're working
             alert("Code from app.js is running!");
         });
    });
  2. app.js を保存します。
  3. Preview in Browser (ブラウザーでプレビュー)」ボタンをクリックして、index.html をプレビュー表示します。体重管理アプリがロードされた後、ブラウザーにアラート・ダイアログが表示されることを確認してください。

カスタム JavaScript コードの追加

体重管理アプリを機能強化する作業のほとんどは、app.js の中で行われます。これから、このファイルに対して、さまざまな Dojo ウィジェットを呼び出して操作するカスタム JavaScript コードを追加していきます。この作業を読者の皆さんが行うには、記事を読んで行く中で登場する JavaScript コード・スニペットを順次 app.js ファイルにコピー・アンド・ペーストして行きます。

これらの JavaScript コード・スニペットの多くは、アプリに対する機能強化としてテスト可能になっています。つまり、コードを追加するごとに、Maqetta のプレビュー機能を使用して新しい機能をテストすることができます。コードを順次追加する作業を飛ばしたい場合は、app.js最終版をダウンロードして、現在のバージョンをそのまま完成版で置き換えてください。

必要なモジュール

デフォルトの app.js ファイルでは、Dojo ローダーによって定義された require 関数を使用して、Dojo モジュールを 1 つだけロードします (前述の dojo/ready)。私たちが目的とする機能強化には、他にも以下のモジュールが必要になります (「参考文献」にリンクが記載されている「Dojo Toolkit Reference Guide」を参照)。

  • dojo/dom
  • dojo/dom-style
  • dijit/registry
  • dojo/on
  • dojo/date/stamp
  • dojo/data/ItemFileWriteStore

これらのモジュールを app.js に追加するには、app.js の内容をリスト 5 に記載するコードで置き換えます。これにより、必要な追加モジュールが読み込まれるようになります。

リスト 5. app.js に追加する Dojo モジュール
/*
 * This file is provided for custom JavaScript logic that your HTML files might need.
 * Maqetta includes this JavaScript file by default within HTML pages authored in 
 * Maqetta.
 */
require(["dojo/ready", 
        "dojo/dom", 
        "dojo/dom-style", 
        "dijit/registry", 
        "dojo/on", 
        "dojo/date/stamp",
        "dojo/data/ItemFileWriteStore"], 
function(ready, 
         dom, 
         domStyle, 
         registry,
         on, 
         stamp,
         ItemFileWriteStore){

    ready(function(){
        // logic that requires that Dojo is fully initialized should go here

    });
});

ウィジェットの参照

次に行う処理は、JavaScript を使用して、操作したいすべての Dojo ウィジェットへの参照を取得することです。生成された HTML を調べると、Dojo ウィジェットの ID がわかれば、どの Dojo ウィジェットでも参照できることがわかります。この処理は、dijit/registry モジュールに含まれる byId 関数を利用すると、とりわけ簡単に行えます。例えば、registry.byId("weightList") によって、体重リスト・ウィジェットにアクセスすることができます。

リスト 6 では、個々のウィジェットを検索するたびに registry.byId を呼び出し、そのウィジェットへの参照を変数に格納します (この変数は後で使用することになります)。ID を誤って入力したり、入力し忘れたりすることはよくあることなので、ここでは安全策として if 文を追加して、変数のすべてが定義済みであることを確実にしています。変数が欠落している場合は、ユーザーがその問題を突き止められるように、エラー・メッセージが表示されます。

リスト 6. ウィジェットへの参照の取得
        /* *******************************************
         * Get a reference to all the widgets we need
         *********************************************/
        var weightList = registry.byId("weightList");
        var mainView = registry.byId("mainView");
        var detailsView = registry.byId("detailsView");
        var detailsView_Date = registry.byId("detailsView_Date");
        var detailsView_Notes = registry.byId("detailsView_Notes"); 
        var weightSpinWheel = registry.byId("weightSpinWheel");
        var dateListItem = registry.byId("dateListItem");
        var notesListItem = registry.byId("notesListItem");
        var dateSpinWheel = registry.byId("dateSpinWheel");
        var notesTextArea = registry.byId("notesTextArea");
        var addWeightButton = registry.byId("addWeightButton");
       
        // Make sure we found all of the widgets
        if (!weightList || 
            !mainView || 
            !detailsView || 
            !detailsView_Date || 
            !detailsView_Notes || 
            !weightSpinWheel || 
            !dateListItem || 
            !notesListItem || 
            !dateSpinWheel || 
            !notesTextArea || 
            !addWeightButton) {
            
            // show an error to make it easier to figure out
            // which widget(s) could not be found
            alert("could not find at least one of the widgets:\n" + 
                "\t weightList = " +  weightList + ",\n" + 
                "\t mainView = " +  mainView + ",\n" +
                "\t detailsView = " +  detailsView + ",\n" +
                "\t detailsView_Date = " +  detailsView_Date + ",\n" +
                "\t detailsView_Notes = " +  detailsView_Notes + ",\n" +
                "\t weightSpinWheel = " +  weightSpinWheel + ",\n" +
                "\t dateListItem  = " +  dateListItem + ",\n" +
                "\t notesListItem = " +  notesListItem + ",\n" +
                "\t dateSpinWheel = " +  dateSpinWheel + ",\n" + 
                "\t notesTextArea = " +  notesTextArea + ",\n" +
                "\t addWeightButton = " +  addWeightButton);
                
            // return, so don't run any other JavaScript
            return;
        }

このコードを app.js ファイルの ready 関数のすぐ内側にコピー・アンド・ペーストします。その後、ファイルを保存してアプリをプレビュー表示し、すべてのウィジェットが揃っていることを確認します。


データストアの変更

HTML ソースを調べてわかったように、Maqetta は、mainViewEdgeToEdgeDataList で使用するためのデータストアとして ItemFileReadStore を生成します。ItemFileReadStore は静的データには適していますが、ここでは体重管理アプリのデータを実行時に変更したいので、EdgeToEdgeDataListItemFileWriteStore を使用するようにデータストアを変更する必要があります。

ItemFileWriteStoreItemFileReadStore の全機能に加え、dojo/data/api/Writedojo/data/api/Notification の API に必要な実装済みの関数も備えています。これらの関数が提供されることにより、このデータストア内のデータを、特定の関数を呼び出して変更できるようになります。リスト 7 に、体重リストで使用するデータストアを変更するためのコードを記載します。

リスト 7. 別のデータ・ストアの設定
        /* *******************************************
         * Replace ItemFileReadStore generated by
         * Maqetta with ItemFileWriteStore  
         *********************************************/
        var weightWriteStore = new ItemFileWriteStore  ({
            url:"weights.json"
        });
        weightList.setStore (weightWriteStore);

リスト 7 のコードをコピーして app.js ファイルに貼り付けます。貼り付ける場所は、リスト 6 で追加した if 文の後です。app.js ファイルを保存した後、体重管理アプリをプレビュー表示して、EdgeToEdgeDataListweights.json ファイルの体重リストが前と同じく表示されることを確認してください。

選択されたデータのプレースホルダーの追加

ここでの一般戦略は、現在選択されている体重エントリーのデータを、selectedWeightData 変数に格納することです。その変数を定義するリスト 8 のコードを app.js ファイルに追加します。ただし、この変更を行った後に体重管理アプリをプレビュー表示しても、機能には何の違いも現われないことに注意してください。

リスト 8. 選択されたデータのプレースホルダー
        /* *******************************************
         * Provide placeholder for the weight data
         * currently being edited.
         ********************************************/
        var selectedWeightData = null;

クリック・ハンドラーの追加

次に、listItemClick という関数を定義します。この関数は、mainViewEdgeToEdgeDataList 内の項目がクリックされる度に呼び出されることになります。listItemClick 関数は、dojox/mobile/ListItem 型の dojoListItem を引数として要求します。したがって、ListItem への参照を使用して、リスト 8 に記載した selectedWeightData 変数に値を取り込みます。

selectedWeightData に値を取り込んだ後、detailsView への遷移を開始します (この遷移を手動で行えるように、リスト 2 で moveTo を "#" に変更したことを思い出してください)。

リスト 9 に、listItemClick を定義する JavaScript を記載します。

リスト 9. クリック・ハンドラー
        /* *******************************************
         * Function to be called when item in the 
         * EdgeToEdgeDataList is clicked.
         ********************************************/
        var listItemClick = function(dojoListItem) {
            // Fill in selected weight data based on selected item
            selectedWeightData = {
                id: dojoListItem.params.id,
                label: dojoListItem.params.label,
                rightText: dojoListItem.params.rightText,
                notes: dojoListItem.params.notes,
            };
    
            //Perform the transition
            dojoListItem.transitionTo("detailsView");                    
        };

この新しいクリック・ハンドラーを app.js に追加します。ただし、次のセクションでクリック・ハンドラーを接続するまでは、アプリのプレビューには何の変化も現われません。

クリックのリッスン

次に必要な作業は、体重リストの項目が選択されると、必ず listItemClick 関数が呼び出されるようにすることですが、クリック・ハンドラーを個々のリスト項目に直接追加する手段はありません。けれども前に説明したように、HTML ソースでは EdgeToEdgeDataList<ul> 要素によって表現されています。EdgeToEdgeDataList は実行時に、リストに表示する項目ごとに個々の<li> 要素を作成します。この情報を踏まえて、必要な体重リストの機能を作成します。

リスト 10 では dojo/on を使用して、EdgeToEdgeDataList がクリックされたときに呼び出される関数を登録しています。続いてイベントのターゲットを調べ、クリックされたときに、リストのどのサブ要素がマウスの下にあったのかを判別します。そのサブ要素が、興味の対象となる <li> 要素の子孫です。このサブ要素の祖先を検索すれば、<li> が見つかります。<li> が見つかれば、registry.byId によって Dojo ListItem への参照を取得して、listItemClick を呼び出すことができます。

リスト 10. EdgeToEdgeDataList のクリック・ハンドラー
       /* *******************************************
         * When weight list is clicked, we want to 
         * find the Dojo ListItem that was actually
         * targeted by the user and handle the click.
         ********************************************/
        on(weightList, "click", 
            function(event) {
                // The event's "target" will be the list's
                // sub-element what was clicked. (The 
                // event's "currentTarget" should be the list
                // itself.
                var subElement = event.target;

                // The subElement of the list may be an LI or 
                // a child of an LI element. If not an LI,
                // we want to search the ancestry of the
                // subElement to find the LI.
                var parent = subElement.parentNode;
                while (parent != null && parent.nodeName != "LI") {
                    parent = parent.parentNode;
                }

                if (parent) {
                    // If parent is set, then we've found the LI. From
                    // there we can use the id to get the Dojo ListItem.
                    var dojoListItem = registry.byId(parent.id);

                    // Handle the click
                    listItemClick(dojoListItem);
                }
        });

app.js を上記のコードで更新して保存したら、体重管理アプリをプレビュー表示してください。EdgeToEdgeDataList の項目をクリックすると、detailsView に遷移しますか?現在、この遷移は listItemClick 関数によって開始されるようになっています。したがって、listItemClick 関数が実行されると、遷移が発生するはずです。さらに、遷移が発生した場合には、ほぼ間違いなく、選択されたリスト項目のデータが selectedWeightData に取り込まれています。


遷移イベントのモニター

ここまでのところで、app.js に目的とする機能を実装するための基礎をセットアップし、より動的な機能を UI プロトタイプに追加する上で最も難しい部分の作業を完了しました。以降のセクションでは、同じ一般戦略を繰り返して、各種のビューが表示される際や、非表示にされる際に発生する遷移イベントをモニターします。この過程を通して、JavaScript で作成したカスタム・コードを Maqetta による UI プロトタイプに追加する方法を、さらによく理解できるようになるはずです。

詳細ビューの遷移

ビューが表示される直前 (onBeforeTransitionIn イベントによって示されます) に、そのビューのウィジェットには、selectedWeightData に含まれるデータに基づいて値を設定する必要があります。同様に、ビューが非表示になる直前 (onBeforeTransitionOut イベントによって示されます) には、selectedWeightData 変数内のデータを更新することになります。この更新によって、ユーザーがビューの UI ウィジェットを操作しているときに行った変更が反映されます。selectedWeightData を更新した後は、遷移ハンドラーが次に表示するビューにその変更内容を適用することで、変更が表示されます。

最初に、以下に記載するリスト 11detailsView への遷移について探ります。このコードでは、dojo/on モジュールを使用して、beforeTransitionIn イベントが detailsView に対して起動されると呼び出される関数を指定します。その関数の中で、selectedWeightData 内で検出した値を基に weightSpinWheeldateListItem、および notesListItem を更新します。リスト 11 のコードでは、以下の点に注意してください。

  • weightSpinWheel を更新するには何らかの工夫が必要になります。それは、(selectedWeightData.label に配置される) 体重の数字の各桁に基づいてスピンホイールのスロットごとの値を設定する必要があるからです。weightSpinWheeldojox/mobile/SpinWheel のインスタンスなので、getSlots 関数を呼び出すことでスロットを取得することができます。それぞれのスロットは、dojox/mobile/SpinWheelSlot のインスタンスです。ほとんどの Dojo Mobile ウィジェットと同じく、スロットの value 属性は set 関数によって設定することができます。
  • dateListItemrightText 属性を更新するには、該当する set 関数を呼び出します。notesListItemrightText 属性を更新するのにも、set 関数を使用します。
  • 更新を行った後、domStyle.get を使用して、dateListItem の右側のテキストを収容する DOM ノードの幅を取得します。次に、その幅の値を domStyle.set で使用して、notesListItem の右側のテキストを収容する DOM ノードの幅を変更します。幅が適切に設定されていれば、他の CSS 属性 (つまり、white-space 属性、overflow 属性、text-overflow 属性) を使用して、必要に応じて rightText の先頭の部分以外を (省略記号を使って) 省略することができます。
リスト 11. 詳細ビューに遷移する前の処理
        /* *******************************************
         * detailsView Transitions
         *********************************************/	 
        on(detailsView, "beforeTransitionIn", 
            function(){
              if (selectedWeightData) {
                // Get the slots from the spin wheel
                var weightSpinWheelSlots = weightSpinWheel.getSlots();

                // Loop over digits in weight to set value for each slot in the
                // spin wheel. For simplicity (and this is a prototype 
                // after all) assuming all weight labels have a string length 
                // of 3 (e.g., weight > 100)
                for (var i = 0; i < 3; i++) {
                   var char = selectedWeightData.label.charAt(i);
                   weightSpinWheelSlots[i].set("value", char);
                }

                // Update the date list item
                dateListItem.set("rightText", selectedWeightData.rightText);

                // Update the notes list item
                notesListItem.set("rightText", selectedWeightData.notes);

                // Update styling of notesListItem's rightTextNode so that
                // it's the same width as the date label and will automatically
                // add an ellipsis for us. The settings for whiteSpace,
                // overflow, and textOverflow are static, so they technically
                // should go in app.css and override the "mblListItemRightText"
                // style class.
                var width = Math.round(domStyle.get(dateListItem.rightTextNode, "width"));
                domStyle.set(notesListItem.rightTextNode, "width", width + "px");
                domStyle.set(notesListItem.rightTextNode, "whiteSpace", "nowrap");
                domStyle.set(notesListItem.rightTextNode, "overflow", "hidden");
                domStyle.set(notesListItem.rightTextNode, "textOverflow", "ellipsis");
            }
       });

上記のコード・スニペットで app.js を更新すると、アプリのプレビューが最終目標にかなり近い形で動作するようになります。mainView で任意の項目をクリックすると、detailsView のウィジェットは、クリックされた項目を実際に反映したものになるはずです。一例として、図 5 に detailsViewEdgeToEdgeDataList で最初の項目をクリックした後の画面を示します。Maqetta ページ・エディターに表示されるデフォルト値とは異なる値が表示されていることに注目してください。また、表題部分にある「Home (ホーム)」ボタンをクリックして mainView に戻ってからウィジェット・リストで別の項目を選択すると、detailsView に表示される値がまた変わっているはずです!

図 5. デフォルトではない値を表示する詳細ビュー
デフォルトではない値を表示する詳細ビューのスクリーン・ショット

detailsView からの遷移

今度は、ユーザーが detailsView から離れるときのアプリの動作を処理します。ビューを離れるときには、ユーザーがそのビューで行った変更を反映するように selectedWeightData を更新しなければならないことを思い出してください。リスト 12 では dojo/on モジュールを使用して、beforeTransitionOut イベントが detailsView に対して起動されると呼び出される関数を登録します。

このビューでユーザーが編集できるウィジェットは weightSpinWheel に限られます。したがって、ビューを離れるときに変更しなければならないのは、selectedWeightData.label に格納されている体重の値のみです。この場合も getSlots 関数を使用して個々のスロットを取得しますが、今回はスロットをループ処理して、各スロットから value 属性を抽出し、結果を連結して体重を表すストリングにします。そして、このストリング値を selectedWeightData に再び格納します。

リスト 12. 詳細ビューから遷移する前の処理
        on(detailsView, "beforeTransitionOut", 
            function(){
                if (selectedWeightData) {
                    // Get the slots from the spin wheel
                    var weightSpinWheelSlots = weightSpinWheel.getSlots();

                    // Build up the new label for weight from the weight spin wheel slots
                    var newLabel = "";
                    for (var i = 0; i < weightSpinWheelSlots.length; i++) {
                        newLabel += weightSpinWheelSlots[i].get("value");
                    }

                    // Update selected weight data
                    selectedWeightData.label = newLabel;
                }
        });

上記のコード・スニペットで app.js を更新しても、プレビューの動作は変化しません。ただし、次の作業で mainView の beforeTransitionIn イベント・ハンドラーを実装し、selectedWeightData.label に格納された更新後の値を利用してリスト項目が更新されるようにすると、プレビューの動作に違いが現われてきます。

detailsView_Date との間の遷移

現状の app.js では、detailsView のウィジェットが mainView でクリックされた体重エントリーの値を反映するようになっています。けれども、detailsViewdateListItem をクリックして detailsView_Date にアクセスした場合、日付スピンホイールに取り込まれる値は、実行時に選択した値ではなく、Maqetta ページ・エディターで設定した静的な値のままです。そこで、さらに別の遷移ハンドラーを実装する必要があります。

リスト 13 では、detailsView_Date に対して beforeTransitionIn イベントが起動されると実行される関数を登録しています。この場合の実装はかなり単純で、dateSpinWheel (dojox/mobile/SpinWheelDatePicker のインスタンス) の value 属性を selectedWeightData.rightText に設定しているだけです。これで上手く機能する理由は、SpinWheelDatePicker で扱う日付を ISO-8601 フォーマットで表すようにしてきたためです。

リスト 13. detailsView_Date に遷移する前の処理
        /* *******************************************
         * detailsView_Date Transitions
         *********************************************/
        on(detailsView_Date, "beforeTransitionIn", 
            function(){
                if (selectedWeightData) {
                    // NOTE: Date spin wheel expects an ISO date (which is 
                    // what we've been putting in rightText)
                    dateSpinWheel.set("value", selectedWeightData.rightText);
                }
        });

上記のコードで app.js ファイルを更新してから再びプレビュー機能を実行すると、detailsView_Date が期待通りに機能していることがわかります。つまり、mainView で体重エントリーを選択した後の、detailsView_Date の日付スピンホイールの値は、EdgeToEdgeDataList の該当する項目の rightText、および detailsView の日付リスト項目の rightText と一致するはずです。

detailsView_Date での残りの作業は、ユーザーが日付スピンホイールの値を変更した場合に、その値で selectedWeightData を更新することだけです。それには、リスト 14 に示されているように、detailsView_Date に対して beforeTransitionOut イベントが起動されたときに、dateSpinWheel から値を取得して selectedWeightData.rightText を設定するだけのことです。

リスト 14. detailsView_Date から遷移する前の処理
        on(detailsView_Date, "beforeTransitionOut", 
            function(){
                if (selectedWeightData) {
                    // Get value from the spint wheel
                    var value = dateSpinWheel.get("value");

                    // Update selected weight data
                    selectedWeightData.rightText = value;
                }
        });

app.js を更新してプレビューを実行してください。detailsView_Date の日付スピンホイールで値を変更してから detailsView に戻ると、dateListItemrightText が新しい値で更新されているはずです。

detailsView_Notes との間の遷移

detailsView_Date の場合と同じ基本プロセスに従って、今度は detailsView_Notes を対象にコードを更新します。リスト 15 を見るとわかるように、detailsView_Notes を対象とした beforeTransitionInbeforeTransitionOut のハンドラーは、detailsView_Date で該当するそれぞれのハンドラーと非常によく似ています。異なる点は、dateSpinWheelselectedWeightData.rightText の代わりに notesTextAreaselectedWeightData.notes が使用されていることです。

リスト 15. detailsView_Notes との間の遷移
        /* *******************************************
         * detailsView_Notes Transitions
         *********************************************/
        on(detailsView_Notes, "beforeTransitionIn", 
            function(){
                if (selectedWeightData) { 
                    notesTextArea.set("value", selectedWeightData.notes);
                }
        });

        on(detailsView_Notes, "beforeTransitionOut", 
            function(){
                if (selectedWeightData) {
                    // Get value from the text area
                    var value = notesTextArea.get("value");

                    // Update selected weight data
                    selectedWeightData.notes = value;
                }
        });

上記のコードで app.js を更新した後にプレビューを再度実行すると、detailsView_NotesnotesTextArea が、mainView で選択された項目の notes 属性の値を反映していることがわかります。さらに、notesTextArea の文字列の先頭部分を変更してから detailsView に戻ると、notesListItemrightText も更新されるようになっています。

メイン・ビューの遷移

detailsViewdetailsView_DatedetailsView_Notes の遷移コードがすべて揃ったところで、次に必要なのは、ユーザーが detailsView から mainView に戻ったときに、EdgeToEdgeDataList に最新のデータが表示されるようにすることです。そのために、ここでは主に weightWriteStore を扱うことになります。この app.js の更新プロセスの最初のほうで作成した weightWriteStore (リスト 7 を参照) は、ItemFileWriteStore のインスタンスであり、weightList にデータを提供する役目を持ちます。

ここでは、最初にクリックされたリスト項目を格納しているデータ項目の値を変更する必要があります。そこで、リスト 16 ではまず、対象とする項目の ID を持つ weightWriteStorefetchItemByIdentity を呼び出します。fetchItemByIdentity 関数は非同期で動作するため、この関数には、項目が取得されたときに呼び出す必要がある関数を (onItem 引数を介して) 渡さなければなりません。

onItem 関数が呼び出された後のプロセスは、もっと単純です。取得した項目の labelrightTextnotes のそれぞれを selectedWeightData に格納されている値で更新するために、weightWriteStoresetValue を合計 3 回呼び出すことによって、データの変更を「コミット」します。次に、weightWriteStore を引数に指定して weightListsetStore を呼び出し、EdgeToEdgeDataList がこのデータストアの値でリフレッシュされるようにします。そして最後に selectedWeightDatanull にします。なぜなら、アクティブな選択項目はなくなったためです。

リスト 16. mainView に遷移する前の処理
        /* *******************************************
         * mainView transition
         *********************************************/
        on(mainView, "beforeTransitionIn",
            function(){
                if (selectedWeightData) {
                    weightWriteStore.fetchItemByIdentity({
                        identity: selectedWeightData.id,
                        onItem: function(item) {
                            // We've retrieved the item we want to edit, so 
                            // update it in the weight list data store
                            weightWriteStore.setValue(item, "label", 
                                selectedWeightData.label);
                            weightWriteStore.setValue(item, "rightText", 
                                selectedWeightData.rightText);
                            weightWriteStore.setValue(item, "notes", 
                                selectedWeightData.notes);

                            // Force weight list to reload the data store
                            weightList.setStore(null); 
                            weightList.setStore(weightWriteStore);

                            //Clear out the selected weight data
                            selectedWeightData = null;
                        },
                        onError: function(error) {
                            // TODO: in production environment, would want to do 
                            // something with error
                            console.error("fetchItemByIdentity failed!");
                        }
                    });
                }
        });

app.js を更新してアプリをプレビュー表示すると、目標の大半を達成したことがわかります。具体的には、detailsViewdetailsView_Date で変更を行ってから mainView に戻ると、その変更が EdgeToEdgeDataList に反映されます。detailsView_Notes での変更はすぐに明らかにはなりませんが、上記のコードに示されているように、notes に加えられた変更は weightWriteStore にコミットされています。これをテストするには、weightList で同じ体重エントリーをもう一度選択して detailsView_Notes にナビゲートします。notesTextArea を変更した場合には、更新された値が表示されるはずです。


体重エントリーを追加する

この動的な UI プロトタイプを仕上げるために、最後にもう 1 つ JavaScript のコードを作成します。mainView の表題部分に表示されるプラス (+) ボタンによって、新しい項目を体重のリストに追加してから detailsView に遷移し、ユーザーが新しいエントリーを編集できるようにする必要があるからです。

そのためにはまず、リスト 17 に示されているように、dojo/on を使用して、addWeightButton がクリックされたときに呼び出される関数を登録します。その関数の中で、以下の操作を行います。

  1. addWeightCounter カウンターを使用して、新しい項目の一意の ID を生成します。
  2. その一意の ID を使用して、新規項目のデフォルト・データを作成します。
  3. 新規項目がクリックされたかのように振る舞って、その項目のデータを selectedWeightData に配置します。これで、detailsView への遷移が発生したときには、このビューの beforeTransitionIn ハンドラーがそのデータを使用できるようになります。
  4. 新規項目のデータを引数に指定して、weightWriteStorenewItem を呼び出します。これにより、そのデータがデータストアに追加されるだけでなく、該当するイベントが起動されて、EdgeToEdgeDataList が新しいウィジェットを表す新規リスト項目ウィジェットを自動的に作成します。

リスト 17 に、このプラス・ボタンの処理を行う JavaScript コードを記載します。

リスト 17. addWeightButton のクリックを処理するコード
        /* *******************************************
         * Handle addWeightButton
         ********************************************/
         var addWeightCounter = 0;
         on(addWeightButton, "click", function() {
             // Generate a unique id for the new item
             var newWeightId = "newWeight_" + addWeightCounter++;
     
             // Fill in some default data for the new item
             var newWeightData = {
                 id: newWeightId,
                 moveTo: "detailsView",
                 //Default to 150, but production code would use most recent weight
                 label: "150", 
                 //Default rightText to today's date
                 rightText: stamp.toISOString(new Date(), {selector: 'date'}), 
                 //Default notes to empty string
                 notes: ""
             };
   
             // Set the selected weight data to data for new item
             selectedWeightData = newWeightData;

             // Add new item to the data store. NOTE: We're keeping this simple for 
             // the prototype and just always adding the new item to the data store. 
             // That is, we're not considering possibility of user canceling the
             // operation.
             weightWriteStore.newItem(newWeightData);
         });

app.js を更新してアプリをプレビュー表示すると、プラス・ボタンのクリックによって detailsView に遷移し、新規項目のデータが表示されるはずです。すぐに mainView に戻ると、「150」というラベルの付いた新しいエントリーが表示され、右側のテキストに今日の日付が設定されているのがわかります。最後のテストとして、detailsView に戻って体重や日付を変更してみてください。その上で mainView に戻ると、これまでに追加したすべての遷移ハンドラーのおかげで、新しいエントリーの値が更新されているはずです。


第 2 回のまとめ

第 1 回で開発した Maqetta による UI プロトタイプは、コーディングを一切することなく簡単に作成できて、しかもかなりリッチなものでしたが、今回の記事ではそのプロトタイプを大幅に機能強化しました。JavaScript のカスタム・コードを追加し、Dojo と Dojo Mobile のインタラクティブな機能を使用した結果、体重管理アプリのフローはかなり現実的なものに改善されています。完成にはまだ程遠いものの (例えば、追加または編集した体重は、その後のセッションまで維持されません。また、ユーザーには体重エントリーを削減する手段がありません)、プロトタイプとしては立派なものです。このプロトタイプに追加された機能は、経営陣や潜在的な投資家に対し、UI に意図された実際の動作を十分にデモンストレーションします。

第 1 回と第 2 回の演習を完了した今、Maqetta による独自のプロトタイプの構築や、さらには必要なカスタム・コードを JavaScript で作成するだけの経験は十分に積んでいます。この連載の最終回となる第 3 回では、引き続きこれまでに探ったさまざまな概念に基づいて、GPS 位置情報表示アプリの簡単なプロトタイプを作成した後、PhoneGap を使用して、このプロトタイプを実際のモバイル端末にデプロイできるネイティブ・アプリに変換します。次回の記事までは、「参考文献」セクションに記載されているリンクを参照して、Maqetta に関する知識を深めてください。

謝辞

この連載記事を慎重にレビューして建設的なフィードバックを提供してくださった Maqetta チーム (Jon Ferraiolo 氏、Javier Pedemonte 氏、Adam Peller 氏、Bill Reed 氏) に深く感謝いたします。


ダウンロード

内容ファイル名サイズ
Final source of the custom appmaqetta_part2.zip5KB

参考文献

学ぶために

製品や技術を入手するために

議論するために

  • Maqetta ユーザー・グループに加わってください。Maqetta を使用してデスクトップ/モバイル用 UI を作成している他のデザイナーや開発者と情報を交換できます。
  • developerWorks コミュニティーに加わってください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Mobile development, Open source, Web development
ArticleID=933830
ArticleTitle=モックアップという意味を持つ Maqetta: 第 2 回 Maqetta でデザインしたモバイル UI 用に JavaScript でカスタム・コードを作成する
publish-date=06132013