uu.snippetで「やりましょう」、Widgetの作り方入門

続き書きました uu.snippet 入門(2) 複数の部品を一つのsnippet に - latest log

Widget(Web ブラウザ上で動作する UI 部品)を作るには、構造を HTML で、見栄えを CSS で、アクションを JavaScript で記述することになります。

Widget を作る際の問題は大きく3つ。

  • 標準的な JavaScript には DOM 構造やスタイルをヒアドキュメント化し埋め込む仕組みが無い(頑張ればなんとかなるけど、サクっとやれない)
  • CSS には定数や外部参照可能な変数の概念が欠如している
    • 画像数、画像サイズ、レイアウト変更に耐えるだけの構造を持たせることが非常に難しく、○○CSSなどの外部ツールに頼るか、妥協するか、実数(マジックワード)として埋め込むかを選択することになる(変数が使えないために発生するトライ&エラーや悩む時間が無駄すぎる)
    • カスタマイズ性が無く、一度作った部品をパラメタだけ変更して流用できない
  • JavaScript 上で、HTML や CSS を文字列の塊として扱うと、見た目にも汚く分かりづらい
    • 見た目を優先すると、HTML, CSS, JavaScript を別々のファイルにしたくなるが、それをやってしまうと部品として利用しづらい構成になってしまう
    • Widget を構成するファイルがディレクトリに散在する状況になると、ミスも手間も増え、Widget を利用するメリットがデメリットで相殺されかねない
      • 10行〜20行のちょこちょこしたファイルまでバージョン管理したくない

uu.snippet を使うと、このような厄介な問題の多くを解決できます。

例題(画像がスッスッとスライドする、ところてん風味(?)なメニュー)

ちょっと前のauのトップページにあったような、横長な画像をホバーでスライドするメニューを JavaScript で作成してみました。

jQuery でつくるとこうなりがち

jQueryWidget を実装しようとすると、インスパイア元のページのように取り回しのしづらい構造になりがちです。
このスタイルからは、

  • 機能をひとつ埋め込むだけなのに、意識/管理しなければならないファイル数やコードブロックが多いこと(6つもある)
  • コードブロックのコピペって、なんとかなりませんか…

などの問題を感じます。

// 見栄えを外部CSSファイルとして読み込んでいる
<link href="../CSSFiles/jimgMenu.css" rel="stylesheet" type="text/css" /> 

// jQuery本体
<script src="../js/jquery.js"></script>

// jQuery で easing 関数を使うためにはプラグインが必要
<script src="../js/jquery-easing-1.3.pack.js"></script>
<script src="../js/jquery-easing-compatibility.1.2.pack.js"></script> 

// Widget 本体は 外部JavaScriptファイル化するか、ページに直接埋め込む必要がある
// どちらにしても、JavaScript のコードブロックを別管理しなければならない
<script>
$(document).ready(function () {
  // find the elements to be eased and hook the hover event
  $('div.jimgMenu ul li a').hover(function() {  
    // if the element is currently being animated
    if ($(this).is(':animated')) {
      $(this).addClass("active").stop().animate({width: "310px"}, {duration: 450, easing: "easeOutQuad", complete: "callback"});
    } else {
      // ease in quickly
      $(this).addClass("active").stop().animate({width: "310px"}, {duration: 400, easing: "easeOutQuad", complete: "callback"});
    }
  }, function () {
    // on hovering out, ease the element out
    if ($(this).is(':animated')) {
      $(this).removeClass("active").stop().animate({width: "78px"}, {duration: 400, easing: "easeInOutQuad", complete: "callback"})
    } else {
      // ease out slowly
      $(this).removeClass("active").stop(':animated').animate({width: "78px"}, {duration: 450, easing: "easeInOutQuad", complete: "callback"});
    }
  });
});
</script>

// Widget の構造は、ページに直接コピペで埋め込むか、JavaScript で文字列から動的に作る必要がある
// やはり、Widget の構造を別管理しなければならない
<div class="jimgMenu"> 
  <ul> 
    <li class="landscapes"><a href="#">Landscapes</a></li> 
    <li class="people"><a href="#">People</a></li> 
    <li class="nature"><a href="#">Nature</a></li> 
    <li class="abstract"><a href="#">Abstract</a></li> 
    <li class="urban"><a href="#">Urban</a></li> 
  </ul> 
</div>

uu.snippet で「やりましょう」

http://uupaa-js.googlecode.com/svn/trunk/0.8/test/ui/menu/tokoroten.htm のソースから抜粋します。

// uupaa.js 本体のロード
<script src="../../../src/uupaa.js"></script>

// ヒアドキュメントを使っているため、ビルドツールを使って uupaa.js にマージすることはできません
// type="text/html" として Widget をロードします
<script src="../../../src/ui/menu/tokoroten.js" id="tokoroten" type="text/html"></script> 

<script>
uu.ready(function() {
    // Widgetに与えるパラメタを作る
    var arg = {
        id:     "tokoroten", // UI部品のID。CSSのIDとしても使われる(複数設置する場合はユニークにする)
        auto:   4000,        // 自動展開のディレイ時間。0 で自動展開OFF
        // 画像の大きさと、チラ見せ幅, 画像の縁取り幅
        image:  { width: 160, height: 160, padding: 50, border: 2 },
        list:   {
            key:    ["クラス名", ...]      // 個々の画像に設定するCSSクラス名
            href:   ["リンクURL", ...]
            text:   ["リンクテキスト", ...]
            src:    ["画像のURL", ...]
        },
        onclick:  function(evt, node, index) { // onclick イベントハンドラ
            uu.log("click : " + index); // index = クリックされた画像の番号(0〜
        }
    };

    uu.body(uu.snippet("tokoroten", arg)); // スニペットにパラメタを与えてビルドし…
    uu.ready.fire(arg.id); // Widgetとしてアクティベート(活性化)
});
</script>
Widget(src/ui/menu/tokoroten.js) のソースコード

tokoroten.js は、uu.snippet() のヒアドキュメントや簡易テンプレート機能を使い、HTML, CSS, JavaScript のコードブロックを1つの js ファイルにパッケージしています。

var key = arg.list.key,
    keyz = key.length,
    width = arg.image.width,
    border = arg.image.border,
    padding = arg.image.padding,
    lastNode = 0,
    userHover = 0,
    autoHover = 0;

// uu.snippet の第二引数に渡されたパラメタは、Widget 内では arg 変数としてアクセス可能
// ヒアドキュメントから参照する計算済みの値をここで作成し、arg 変数に代入しておく

arg.image.frameWidth = (padding + border) * keyz + width - padding * 0.5 - border; // フレーム幅
arg.image.innerWidth =  padding           * keyz + width * 2; // <ul> の幅
arg.image.last = key[keyz - 1]; // 末尾画像のCLASS名

function hoverEvent(evt, hover, node) {
        if (evt) {
            // 自動展開中なら、ユーザのhover動作にあわせて、展開中の画像を閉じる
            if (autoHover) { // auto hover -> close
                hoverEvent(0, autoHover = 0, lastNode); // 閉じる
            }
            userHover = hover;
        }
        // Widget のキモ (2行しかないけど)
        // hover in で開き、hover out で閉じる
        uu.klass.toggle(node, "active");
        uu.fx(node, 350, { stop: 1, w: hover ? [width, "OutQuad"] : padding });

        // 自動展開用に、最後に開いたノードを覚えておく
        lastNode = node;
}


// アクティベーション
// ホバー + クリックイベントハンドラの設定
// uu.ready.fire("tokototen") から呼ばれる

uu.ready(arg.id, function() {
    uu("#" + arg.id + ">ul>li>a").hover(hoverEvent).click(function(evt) {
        arg.onclick && arg.onclick(evt, evt.node, uu.attr(evt.node, "data-uueachindex"));
    });
});

// 自動展開まわりの実装
arg.auto && setInterval(function() {
    if (!userHover) {
        var ary;

        if (!lastNode) { // 初期化
            ary = uu.query("#" + arg.id + ">ul>li>a"); // 先頭画像を検索
            lastNode = ary[ary.length - 1];
        }
        ary = uu.node.array(lastNode.parentNode); // <li>
        hoverEvent(0, autoHover = 0, lastNode); // 現在の画像を閉じる
        hoverEvent(0, autoHover = 1, (ary.next && ary.next.firstChild) || // 次の画像を開く
                                     ary.first.firstChild);
    }
}, arg.auto);

// ヒアドキュメント, <> 〜 </> までをHTMLフラグメントとして認識。
// ヒアドキュメント内では {{arg.変数名}} の展開や <each arg.コレクション変数名> のループ展開が行われる
// {{i}} には、ループ変数 i の値が入る。
return <>
<style>
    #{{arg.id}} {
        position: relative;
        width: {{arg.image.frameWidth}}px;
        height: {{arg.image.height}}px;
        overflow: hidden;
        margin: 0 0 0 0;
    }
    #{{arg.id}} ul {
        list-style: none;
        margin: 0;
        display: block;
        width: {{arg.image.innerWidth}}px;
        height: {{arg.image.height}}px;
    }
    #{{arg.id}} ul li {
        float: left;
    }
    #{{arg.id}} ul li a {
        text-indent: -1000px;
        background: #fff repeat;
        border-right: {{arg.image.border}}px solid #fff;
        cursor: pointer;
        display: block;
        overflow: hidden;
        width: {{arg.image.padding}}px;
        height: {{arg.image.height}}px;
    }
    #{{arg.id}} ul li.{{arg.image.last}} a {
        min-width: {{arg.image.width}}px;
    }
    #{{arg.id}} .clear {
        clear: both;
    }
<each arg.list>  // arg.list.forEach() 相当
    #{{arg.id}} ul li.{{key}} a {     // {{key}} は {{arg.list.key[i]}} として展開する
        background: url({{src}});     // {{src}} は {{arg.list.src[i]}} として展開する
    }
</each>
</style>
<div id="{{arg.id}}">
  <ul>
    <each arg.list>
        <li class="{{key}}"><a   // 面倒なのでテキストノードが挟まらないようにしてる
            href="{{href}}" data-uueachindex="{{i}}">{{text}}</a>
        </li>
    </each>
  </ul>
  <br class="clear" />  // ここ手抜き
</div>
</>

まとめ

(ε・◇・)з uu.snippet を使うと JavaScript にヒアドキュメントやコードスニペットを導入できるよ。CSS に変数を埋め込むのにも使えるから、Widget も楽しく作れるよ!