JavaScriptの即時関数

チラ裏。

普通に書くときはこんな感じ。

(function(global) {
// ... ...
})(this);

thisはブラウザの場合はwindowになるけど、オブジェクトに依存しない処理ならオブジェクト名を書かないほうがよいんではないか。

windowオブジェクトに依存する場合は

(function(global, window) {
var document = window.document,
    location = window.location;
// ... ...
})(this, window);

と、敢えて分けて書く。上のやつと合わせるって感じ。

undefinedを書いておきたいときは、

(function(global, undefined) {
// ... ...
})(this);

と書きたいんだが、JSLintが引数の変数名に予約語使うな!って言ってくるので、

(function(global, undef) {
// ... ...
})(this);

とかしておいたほうがいいのかなぁとは思ってる。
ちなみにJavaScript的にglobalって予約語じゃないのよね。不思議。

なにかライブラリ的なものを書く場合は、

this.foo = (function(global) {
  var exports = {};
  // ... ...
  return exports;
})(this);

とか書くことが多いんだけど、なんかびみょい。this.fooを他のファイルで弄りたい時どうしようとか。

jQueryでbindとliveとdelegateの使い分け

ふと整理しておこうと思ったのでメモ。
1.7で各メソッドはon/offの委譲として実装されるようになったが、これをちゃんと理解してないと非効率なイベント設定をすることになってしまう。

イベントが登録される要素

bindとdelegateはセレクタで指定された要素。liveはdocument。

$('#foo').bind('click', fn); // #foo
$('#foo').delegate('#bar', 'click', fn); // #foo
$(document).delegate('#bar', 'click', fn); // document
$('#foo').live('click', fn); // document

bindが動的に追加された要素に適用できないのはイベントを登録する要素がないから。
liveが同じような書き方なのにできるのは、$('#foo')で検索された要素を使用せず、contextにイベントハンドラを登録しているから。その代償として$('#foo')で余計な検索を一度行なってしまっている。

$('#foo').context === document // true

また、bindは要素に対してイベントを登録するため、セレクタにマッチする要素が多いとイベント登録処理に時間がかかり、メモリ効率も悪い。

実行順序

<div id="outer">
  <div id="inner">
    hello
  </div>
</div>

といったHTMLで#innerに対するクリックイベントを書こうとすると以下のようになる。

$('#inner').bind('click', fn); // A
$('#outer').delegate('#inner', 'click', fn); // B
$(document).delegate('#inner', 'click', fn); // C
$('#inner').live('click', fn); // D

#innerをクリックすると、

#inner.click → #outer.click → document.click

の順でイベントが発生するので、実行順序は

A → B → C ≒ D

になる。

まぁ、大差ないだろうけど。

あれ?んじゃ、delegateだけでよくね?

delegateだとchainを書きにくいんだよね。。

$('#foo a').bind('click', fn).show(); // スッキリ
$('#foo').delegate('a', 'click', fn);
$('#foo a').show(); // モヤット

俺的まとめ

  • 基本、bindで。書きやすいしチェーンもしやすい。スコープが小さいのもいいことだ。
  • bindする対象が多い or DOM追加した時にbindをつけるのがめんどくさそうだったらdelegate。
  • liveはいらない子。

liveが$(selector).live()じゃなくて、$.live(selector)だったらよかったのにねぇ。
$(document).on('click', '#foo a', fn)って書き方もちょっと冗長。

jqMobiのはじめかた(Ajax編)

jqMobiを使って、みんな大好きAjaxを使ったサンプルを書いてみる。
Ajaxのサンプルといえば、Twitter Search APIJSONPで叩くというのが世のならわしなのでそれに従う。(そうなの?

土台のHTML

まずはpanel要素。検索フィールドを置いて、結果を#resultsにつっこむという算段。

<div id="search" class="panel" title="Twitter Search">
<form id="search-form" action="#" method="get">
<input id="search-word" type="search" name="q" placeholder="Input search word." />
</form>
<ul id="results"></ul>
</div>

Ajaxでリクエストを投げる

続いてJavaScript。#search-form.submit()を捉えて、Ajaxリクエストに変える。

$('#search-form').bind('submit', function() {
  $.ui.showMask();
  $('#results').empty();
  var q = $('#search-word').val();
  var url = 'http://search.twitter.com/search.json?callback=?&q=' + encodeURIComponent(q);
  var onSuccess = function(data) {
    console.log(data);
    $.ui.hideMask();
  };
  var onError = function() {
    console.log(arguments);
    $.ui.hideMask();
    alert('Error!');
  };
  $.jsonP({
    url: url,
    success: onSuccess,
    error: onError
  });
  return false;
});

jQueryだとgetJSONでJSONPを処理する感じなのだが、jqMobiではjsonPという別メソッドが用意されているのでこちらを使う。
$.ui.showMask()、$.ui.hideMask()はローディングインジケーターを表示・非表示にするメソッド。jQuery MobileのshowPageLoadingMsg/hidePageLoadingMsgに相当。
ちなみにjQUiではwindow.alertとかを置き換えてるらしく、独自のUIで表示される。

表示する

さて、戻ってきた結果を表示したいところだが、タグを文字列連結とかやりたくないのでテンプレートエンジンが欲しい。pluginsディレクトリにjq.template.jsというのがあるのでとりあえずそれで。
jq.templateについての説明はチートシートに書いてある。…が、その通りにやっても動かないので気をつけろ。
簡単な使い方は以下の通り。

$.template('<li><%= name %></li>', {name: 'jhoshina'});
// => '<li>jhoshina</li>'

では、テンプレの追加とonSuccessの実装。

<script id="status-tmpl" type="text/x-jq-template">
<li class="status">
  <figure>
    <img src="<%= profile_image_url %>" />
  </figure>
  <div>
    <span class="user_name"><%= from_user_name %></span>
    <span class="screen_name"><%= from_screen_name %></span>
    <div class="text"><%= text %></div>
  </div>
</li>
</script>
var onSuccess = function(data) {
  var results = data.results;
  var i, tags = '';
  var tmpl = $('#status-tmpl').html();
  for (i = 0; i < results.length; i++) {
    var r = results[i];
    tags += $.template(tmpl, r);
  }
  $('#results').html(tags);
  $.ui.hideMask();
};

内容的にはだいたい表示されるのだが、jq.ui.cssで

ul > li { height: 48px; }

とかふざけた定義がされているので、もろもろ見た目の修正を入れる。

#search-form {
  display: block;
  text-align: center;
  margin: 8px;
}
#search-form [type="search"] {
  display: block;
  width: 90%;
  margin: 0 auto;
  padding-left: 0.5em;
  padding-right: 0.5em;
  font-size: 200%;
}
.status {
  display: -webkit-box;
  height: auto;
}
.status > :first-child {
  display: block;
  width: 50px;
  -webkit-box-sizing: border-box;
  padding: 1px;
}
.status > :last-child {
  -webkit-box-flex: 1;
  -webkit-box-sizing: border-box;
  padding: 4px 8px;
}

f:id:jhoshina:20120325211217p:plain

うまく表示できたようですね。めでたしめでたし。

ソースはこちらで。

jqMobi 1.0のはじめかた(jqUIイベント編)

jqUIのイベント関連を見てみる。

ページ読み込みイベント

$.ui.ready

jQuery Mobileだと初期ページのpageinitあたりに該当するか。
(普通のDOM構築完了のほうはjqMobiの$.readyを使えばよい。)
違うのは、このイベントの発行をユーザ自身が決められるということ。

$.ui.autoLaunch

準備完了かを自動で処理するかどうかのフラグ。デフォルトtrue。
falseにしておくと$.ui.launch()を呼ぶまで$.ui.readyを呼ばない。初期処理とかそのへんに使うんだと思う。
サンブルではスプラッシュスクリーンとか出したりしてる。

$.ui.launch()

「初期処理終わったんで、$.ui.readyに処理移しちゃってくださいな」メソッド。

panel単位のイベント

jQuery Mobileみたいなpageshow/pagehideとかのイベントあるんだろうなー、と思ったけどない。イベントではなく、

<div class="panel" data-load="load" data-unload="unload">
hello!
</div>

といったように、data-load/data-unload属性に関数名を文字列で指定してあげる必要がある。

( ゚д゚)ポカーン

文字列指定とかダサいのでイベントで処理したい

まぁ、これはきっと中の人の意地悪で、必要な人は自分でイベント投げろや、ってことだと思うことにする。

具体的にどうするか。

var panelLoad = function(panel) {
  console.log('show');
  console.log(panel);
  setTimeout(function() {
    $(panel).trigger('load');
  }, 0);
};
var panelUnload = function(panel) {
  console.log('hide');
  console.log(panel);
  setTimeout(function() {
    $(panel).trigger('unload');
  }, 0);
};
$.ui.ready(function() {
  $('.panel')
    .data('load', "panelLoad")
    .data('unload', "panelUnload");
});

とかしておけば、

$('#jQUi')
  .delegate('#foo', 'load', function() {
    console.log('#foo load');
  })
  .delegate('#bar', 'unload', function() {
    console.log('#bar unload');
  });

みたいにかけるね。ε-(´∀`*)ホッ

ちなみに、jQ.Mobiにはbind/delegate/onはあるがliveがない。なんぞ。

jqMobi 1.0のはじめかた(ページ構造編)

まずjqMobiというかjqUiのページ構造を理解しよう。
前回のサンプル見ながらだとわかりやすいはず。

基本構造

f:id:jhoshina:20120322234345p:plain

#jQUi

親となるコンテナ。

#header

タイトルや操作ボタンが入る要素。ただし、自分でタイトルを入れてはいけない。後述の.panelのtitle要素がここに入る。ページ遷移した際には戻るボタンが勝手に追加されたり。

#content

ページ要素(.panel)がここに入る。複数ページの場合、ここに複数の.panelが入る。

.panel

ページ要素。これにひとつのコンテンツを入れる。jQuery Mobileでいう[data-role="page"]にあたる。
ページ間の遷移はこのパネルにIDを付けて実現する。
title属性を付けてやると、このパネルに遷移したときにtitle属性が#headerにh1要素として入る。
んで、data-*とかつけていろいろと挙動を変更できる。くやしくはWebで!

#navbar

フッター的なもの。アイコンとかはicon.cssが実現しているものなので特別なものはない。

nav

サイドメニュー。Facebookアプリとかで使われてる「にゅん!」って横から出るやつ。$.ui.toggleNavMenu()で開け閉め可能。
…って、navタグそのまま使っちゃうんすかΣ(゚Д゚;

わりとjqUiってタグそのものとか、よく使うクラス名とか使ってくれちゃってるのよね。いろいろとめんどくさい。。

jqMobi 1.0のはじめかた(下準備編)

jqMobi 1.0が出て1週間経つというのに、「出ました」系記事しかないような感じなので、「使ってみた」系の記事を書いてみようと思う。

1. ダウンロード

https://github.com/appMobi/jQ.Mobi/downloads
から、1.0a.zipをダウソ。1.0.zipと何が違うかは知らない。
展開してテケトーな名前に変更。

mv 1.0a hello

2. 下ごしらえ

ディレクトリ名とかカオスなので整理。ここに出てこないものは削除しちゃっていい。

mv kitchensink styles
mkdir js
cp ui/jq.ui.min.js js

3. テンプレ

http://www.jqmobi.com/quickstart.php のサンプルを俺風味に整理したもの。

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>jqMobi Starter</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <link rel="stylesheet" href="styles/icons.css" />
    <link rel="stylesheet" href="styles/jq.ui.css" />
    <link rel="stylesheet" href="styles/default.css" />
  </head>
  <body>
    <div id="jQUi">
      <div id="header">
        <a href="#" id="toggle-nav" class="button" data-ignore="true">Toggle Nav</a>
      </div>

      <div id="content">
        <div title="Welcome" id="main" class="panel" selected="true">
          This is a basic skeleton jqUi sample
        </div>
      </div>

      <div id="navbar">
        <div class="horzRule"></div>
        <a href="#main" id="navbar_home" class="icon home">home</a>
      </div>

      <nav>
        <div class="title">Home</div>
        <ul>
          <li class="icon home mini">
            <a href="#main">Selectors</a>
          </li>
        </ul>
      </nav>
    </div>
    <script src="js/jq.ui.min.js"></script>
    <script src="js/init.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>

styles/default.css

#toggle-nav { float: right; }

js/init.js

(function($) {
  // include touch.js on desktop browsers only
  if (!((window.DocumentTouch && document instanceof DocumentTouch) || 'ontouchstart' in window)) {
    var script = document.createElement('script');
    script.src = 'touch.js';
    var tag = $('head').append(script);
    $.os.android = true; //let's make it run like an android device
    $.os.desktop = true;
  }
  $.ui.ready(function () {
    $('#toggle-nav').bind('click', function() {
      $.ui.toggleSideMenu();
      return false;
    });
  });
})(jq);

js/main.js

(function($) {
  $.ui.ready(function () {
    console.log('hello');
  });
})(jq);

これでなんとなく動くはず。
とりあえずスタートアップまで。
手動でやるのがめんどくさいなら、

git clone git://github.com/jhoshina/jqmobi-1.0-startup.git

で。

jQuery Mobileを使う動的ページにJavaScriptのオブジェクトを渡す

いままでjQuery Mobileでページ作ってて、pagecreate/pageshowでページを構築する場合、なんかテキトーな変数名でグローバルに置いてた。

<div id="hoge-page" data-role="page">
<script>
var hoge = <?php echo json_encode($hoge) ?>
jQuery('#hoge-page').live('pagecreate', function() {
  console.log(hoge);
});
</script>
</div>

グローバル変数使うのやだなー、と思いつつ、他に思いつかなかったのでこうしてた。
んで、ふと解決策思いついた。
#hoge-page自体にdata属性としてもたせてやればいいんじゃないかと。

<div id="hoge-page" data-role="page">
jQuery('#hoge-page').data('params', <?php echo json_encode($hoge) ?>);
jQuery('#hoge-page').live('pagecreate', function() {
  var page = jQuery(this);
  var hoge = page.data('params');
  console.log(hoge);
});

ページ内遷移するときも、

<div id="hoge-page" data-role="page">
<a href="#fuga-page">fuga</a>
</div>
<div id="fuga-page" data-role="page">
</div>
<script>
jQuery('a[href="#fuga-page"]').click(function() {
  var params = { name: "jhoshina", age: 17 };
  jQuery('#fuga-page').data('params', params);
});
jQuery('#fuga-page').live('pagecreate', function() {
  var params = jQuery(this).data('params');
  console.log(params.name);
});
</script>

みたいに遷移前にパラメータつっこんでおけば、pagecreate/pageshowで余裕で読める。