githubで複数ユーザを使い分ける

githubというかgitosisはsshの鍵でユーザを判定します。
設定の仕方はhelpでも見てくだしあ。
Redirecting...
Redirecting...
Redirecting...
自分のgithubユーザとして色んなマシンからgithubを使う場合は簡単です。
上記の説明のとおりに公開鍵を追加していけばいいだけです。


で、同じマシンの同じユーザアカウントで、
複数のgithubユーザとしてgithubを使うのはどうすればいいかというと、
ホストのエイリアスを設定して別の秘密鍵を設定してやれば大丈夫です。
入門OpenSSH / 第4章 OpenSSH を使う
↓の例は複数HostNameへのSSH接続の管理ですが、
~/.ssh/config で簡単に複数ホストへのSSH接続を管理する - すぱぶろ
同一HostNameで別Hostというふうにも出来るのでそれを使います。


元々のgithubユーザで使っていた秘密鍵をid_rsaとして、
別のgithubユーザで使う秘密鍵をid_rsa.anotherとしたら、
~/.ssh/config に以下のように記述します。

# monjudoh
Host gist.github.com
HostName gist.github.com
User git
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
# another
Host gist.github.com-another
HostName gist.github.com
User git
IdentityFile ~/.ssh/id_rsa.another
IdentitiesOnly yes
Host github.com-another
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa.another
IdentitiesOnly yes

git@github.com:another/hoge.git をcloneしてきたい場合、
以下のようにすれば、anotherユーザとしてcloneしてくることが出来ます。

git clone git@github.com-another:another/hoge.git

アイコンについて

githubgithub.tokenとuser.emailの設定によって
changesetにどのアイコンを出すか決めているようなので、
git config --local user.email
で、別のメールアドレスを設定しておきましょう。
これをやらないとお客さんも見るリポジトリに二次元キャラアイコンが並ぶなどという事故が起こる可能性があります。
それで別に問題がないなどという可能性もあります。

Shibuya.js - Test.js LT テスターを支援する仕組みの話

お前、誰よ

  • 文殊堂といいます
  • BePROUD社員

今日のお話

  • 自動テストの話はしません
  • テスターによるテストの話をします
  • タイミングによって発生したりしなかったりする類のバグってありますよね
  • テスターさんがモンキーテストをやってくれてる時に見つけてくれたりします
  • でも報告されるのは…
  • 「何をやっているときに」
  • 「何が起こったか」
  • つまり、「操作」と「現象」だけです
  • 原因箇所を特定するにはプログラマもその操作をなんども繰り返さないといけません
  • ダルい
  • 何とかしたい

問題の実例

IE(主に6,7)で「操作は中断されました」が出る

What Happened to Operation Aborted? – IEBlog

  • HTML解析中にまだ閉じタグまで解析されていない要素(bodyとか)に対して、子要素の追加や削除を行うと「操作は中断されました」エラーになる
  • IE6,7で特に酷い…
  • IE8でも発生する
  • DOMContentLoaded前にDOM操作しなければいい?
  • 開発規模が大きくなるとそういうミスは混入してしまいがち
  • jQuery pluginのメソッドの先の先で『.appendTo(document.body)』しているのを見逃す
  • これに非同期ローダーを組み合わせると大丈夫だったり大丈夫じゃなかったりがタイミングによって違ってきてしまう

対策

  • やってはいけないことを規定し、自動検出し、
  • 原因箇所を特定する情報を取得する仕組みを作った
  • やってはいけないこと
HTML解析中にまだ閉じタグまで解析されていない要素(bodyとか)に対して、子要素の追加や削除を行う

技術的詳細

(function(){
  var domManip = jQuery.fn.domManip;
  jQuery.fn.domManip = function(){
    if(!jQuery.isReady && !!this.closest('body').size() ) {
      var msg = printStackTrace().join('\n');
      alert(msg);
      throw new Error();
    }
    return domManip.apply(this,arguments);
  }
})();
  • jQueryでのDOM操作は最終的に概ねdomManipで行われる
  • domManipをラップし、DOM操作してはいけない条件なら、スタックトレースを表示

スタックトレースの表示にはこのライブラリを使っていて、
https://github.com/emwendelin/javascript-stacktrace
例えばIEでもこんな感じで取れる

まとめ的な物

テスターによるテストでも原因箇所のあたりくらいはつけて、
お互いにハッピーになりましょう

続・IEでのa要素の各属性について

前置き

IEでのa要素の各属性について - 文殊堂の続き。
IE 6,7 で相対URL -> 絶対 URL の変換 - #生存戦略 、それは - subtechを参考にして、
cloneNodeハックとlink.hrefによるURLの絶対URL化を組み合わせてみました。
http://jsdo.it/monjudoh/o2Mk
http://jsdo.it/monjudoh/9aHd
link.hrefによるURLの絶対URL化はIE6,7では使えないので割愛。

検証

IE6

なぜかhostnameがiframeではなく外側のものになってしまっている。
少なくとも短いURLについてはouterHTMLハックを使った場合に各属性の値をちゃんと取れていたので、
そっちを使ったほうが良さそう。

a要素の各属性(cloneNodeハック)
---url.length:11
href:[http://jsdo.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsdo.it]
, port:[80]
, pathname:[a]
, hash:[#hash]
---url.length:4097
href:[[object Error]] 
, search:[[object Error]]
, protocol:[[object Error]]
, hostname:[[object Error]]
 , port:[[object Error]]
 , pathname:[[object Error]]
 , hash:[[object Error]]
IE7

IE6同様hostnameがiframeではなく外側のものになってしまっている。
短いURLについてはouterHTMLハックを使用したほうが良さそう。
長いURLの場合にprotocol,portが正しく取得できるというメリットはある模様。

---url.length:11
href:[http://jsdo.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsdo.it]
, port:[80]
, pathname:[a]
, hash:[#hash]
---url.length:4097
href:[http://jsdo.it/a?1(略)1#ha] 
, search:[?1(略)1]
, protocol:[http:]
, hostname:[jsdo.it]
, port:[80]
, pathname:[a]
, hash:[#ha]

IE8

cloneNodeハックのみ使用した場合はIE7とほぼ同じ。

---url.length:11
href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsdo.it]
, port:[80]
, pathname:[a]
, hash:[#hash]
---url.length:4122
href:[http://jsrun.it/a?1(略)1] 
, search:[?1(略)1]
, protocol:[http:]
, hostname:[jsdo.it]
, port:[80]
, pathname:[a]
, hash:[]

cloneNodeハックとlink.hrefを併用するとほぼ問題なく各属性の値を取れる模様。
outerHTMLハック使用時の結果がIE6,7と違い惨憺たるものだったので助かります。

---url.length:11
link.href:[http://jsrun.it/a?1111#hash]
,href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[80]
, pathname:[a]
, hash:[#hash]
---url.length:4122
link.href:[http://jsrun.it/a?1(略)1#hash]
,href:[http://jsrun.it/a?1(略)1] 
, search:[?1(略)1]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[80]
, pathname:[a]
, hash:[]

結論

IE6,7ではouterHTMLハック、IE8ではcloneNodeハックとlink.hrefによるURLの絶対URL化の組み合わせを使うのが良さそう。
長いURLを同対応するかについては要検討。

IEでのa要素の各属性について

色々あってa要素でURLをパースするというコードを書いていて色々はまったのでまとめます。

IE6-8でのa.hrefの上限

  • IE6,7:4096bytes
  • IE8:4121bytes

でした。
なお、Firefox,Google Chrome,Safariは1MBとか普通に扱えます。
使わないけど。
http://jsdo.it/monjudoh/8Fm6/read

各属性の取得状況

a.hrefにURLを代入して各属性がどうなるか調べてみました。

  • URLの長さが短い⇔上限超え
  • outerHTMLハックを使わない⇔使う
    • a.hrefにURLを代入後、別の要素のinnerHTMLにa.outerHTMLを代入し、そのfirstChild(a要素)の各属性を見ること

の二軸を変えて調べてみました。
http://jsdo.it/monjudoh/sc82

IE6

a.hrefへの代入で更新される属性は以下の3つのみ(なのでURLのパースをするにはouterHTMLハックが必要になる)

  • href
  • search
  • hash

上限超えのURLを代入すると、各属性(更新された分)を参照するだけでErrorが投げられる

---url.length:10,use outerHTML:false
href:[/a?111#hash] 
, search:[?111]
, protocol:[]
, hostname:[]
, port:[]
, pathname:[]
, hash:[#hash]
---url.length:4097,use outerHTML:false
href:[/a?1(中略)1#has] 
, search:[[object Error]]
, protocol:[]
, hostname:[]
, port:[]
, pathname:[]
, hash:[[object Error]]
---url.length:10,use outerHTML:true
href:[http://jsrun.it/a?111#hash] 
, search:[?111]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[80]
, pathname:[a]
, hash:[#hash]
---url.length:4097,use outerHTML:true
href:[[object Error]] 
, search:[[object Error]]
, protocol:[[object Error]]
, hostname:[[object Error]]
, port:[[object Error]]
, pathname:[[object Error]]
, hash:[[object Error]]
IE7

a.hrefへの代入で更新される属性は以下の4つ(IE6に比べるとpathnameが追加されている)
protocolも更新されているがなぜか"http:"になるべきところが":"になっている

  • href
  • search
  • pathname
  • hash

上限超えのURLを代入すると、各属性(更新された分)は末尾が切れるなどする。
outerHTMLハックを使っても以下のような問題が残る。

  • 末尾は切れる
  • protocolが"about:"になってしまう
  • hostname,portが空文字
---url.length:10,use outerHTML:false
href:[/a?111#hash] 
, search:[?111]
, protocol:[:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[#hash]
---url.length:4097,use outerHTML:false
href:[/a?1(中略)1#has] 
, search:[?1(中略)1]
, protocol:[:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[#has]
---url.length:10,use outerHTML:true
href:[http://jsrun.it/a?111#hash] 
, search:[?111]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[80]
, pathname:[a]
, hash:[#hash]
---url.length:4097,use outerHTML:true
href:[about:/a?1(中略)1#has] 
, search:[?1(中略)1]
, protocol:[about:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[#has] 
IE8

a.hrefへの代入で更新される属性は以下の4つ
protocolも更新されているがなぜか"http:"になるべきところが":"になっている
IE7と同様だがa.hrefにprotocol,hostnameを除いたURLを代入しても
protocolから始まるフルのURLにhref属性が更新される点が違う。

  • href
  • search
  • pathname
  • hash

outerHTMLハックを使うと(IE7と違い)短いURLでも以下のような問題が発生する。
なお、フルのURLを代入する分には問題ない。

  • protocolが"about:"になってしまう
  • hostname,portが空文字

上限超えのURLを代入すると、何故かhashが反映されない。
hash属性も取れないしhref属性にもhash分がない。

---url.length:11,use outerHTML:false
href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[#hash]
---url.length:4122,use outerHTML:false
href:[http://jsrun.it/a?1(中略)1] 
, search:[?1(中略)1]
, protocol:[:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[]
---url.length:11,use outerHTML:true
href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[about:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[#hash]
---url.length:4122,use outerHTML:true
href:[http://jsrun.it/a?1(中略)1] 
, search:[?1(中略)1]
, protocol:[about:]
, hostname:[]
, port:[]
, pathname:[a]
, hash:[]
Firefox3.6

4KB程度では上限全然行かないので短いURLのみ。
また、outerHTMLは使えないのでouterHTMLハック使用版はなしで。

  • a.hrefにprotocol,hostnameを除いたURLを代入してもprotocolから始まるフルのURLにhref属性が更新される
  • portは80の場合は空文字
  • pathnameは最初の"/"が付く
href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[]
, pathname:[/a]
, hash:[#hash]
Google Chrome8
  • a.hrefにprotocol,hostnameを除いたURLを代入してもprotocolから始まるフルのURLにhref属性が更新される
  • portは80の場合はなぜか0
  • pathnameは最初の"/"が付く

outerHTMLハック使用有無で違いはなし。

href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[0]
, pathname:[/a]
, hash:[#hash]
Safari5
  • a.hrefにprotocol,hostnameを除いたURLを代入してもprotocolから始まるフルのURLにhref属性が更新される
  • portは80の場合はなぜか0
  • pathnameは最初の"/"が付く

outerHTMLハック使用有無で違いはなし。
Google Chrome8と同じ結果

href:[http://jsrun.it/a?1111#hash] 
, search:[?1111]
, protocol:[http:]
, hostname:[jsrun.it]
, port:[0]
, pathname:[/a]
, hash:[#hash]

結論

URLのパースにa要素を使ってもクロスブラウザ対応を考えるとあまり幸せになれないのではないか

BPStudy#41 RequireJSとeventとUIコンポーネント

自己紹介

文殊堂といいます

BePROUDの見習いiOSプログラマです。
JavaScriptで30byteでFizzBuzzを書けます。

location.href='//is.gd/kzkQhu'

iOSの前はUIがリッチな業務システムを作るとかそんな仕事をしてました。

疎結合なUIコンポーネントの作成について

複数画面で使えるAjax formダイアログのコンポーネントを作る

初期状態

Google CalendarのようなWebアプリを想像してください。
1日が1個のセルになっていて、セルをクリックしたらスケジュール登録ダイアログが表示され、
入力して登録ボタンを押すとスケジュールが登録され、セルの中に登録されたスケジュールが表示されます。
イメージ

define([
    'schedule-model'
    ,'schedule-api'
    ,'schedule-to-selectors'
    ,'growl'
    ,'app/schedule-dialog-template'
    ,'debug'
    ,'jqueryui/dialog'
],function(
    ScheduleModel
    ,ScheduleApi
    ,scheduleToSelectors
    ,growl
    ,scheduleDialogTemplate
    ,debug){
  $('.cell').live('click',function(ev){
    // custom data attributeです
    var props = $(this).data('date-properties');
    var dateStr = props.date;
    // 祖先要素のどれかにeditable classが振られていないと更新系のダイアログは出しません
    if (!$(this).closest('.editable').size()) {
      return;
    }

    var initialValue = {startDate : dateStr};
    // scheduleDialogTemplateはオブジェクトを渡せば
    // HTML templateに食わせてHTML文字列化して返してくれる都合のいい関数です
    var html = scheduleDialogTemplate(new ScheduleModel(initialValue));
    $(html).dialog({
      width : 400
      ,buttons : {
        '登録' : function(){
          var $dialog = $(this);
          var $form = $dialog.find('form');
          var schedule = new ScheduleModel();
          // formの入力内容から値を取るメソッドとかModelに生えてます
          schedule.fromForm($form);
          // modelからパラメータを組み立ててWebAPIを叩いて新規登録するのに使うclassです
          // サーバサイドも都合がいいので決して失敗しません
          // なのでsuccess以外のcallbackは不要です
          var api = new ScheduleApi();
          api.success = function(createdSchedule){
            // グロール風に右上にメッセージを出します
            growl('スケジュール登録完了');
            // scheduleToSelectors関数はなんとmodelを渡すとそれを表示すべきセルのCSSセレクタを返してくれます
            // セルのredraw eventを叩くとそのセルに表示すべきスケジュールを全件よしなに取得して、表示してくれます
            $(scheduleToSelectors(createdSchedule)).trigger('redraw');
          };
          api.create(schedule);
          $(this).dialog('close');
        }
        ,
        'キャンセル' : function() {
          $(this).dialog('close');
        }
      }
    });
  });
});

click eventからの切り離し

仕様変更が入りました。内容は以下の通りです。

セルをクリックしたらダイアログが開く
↓
セルをクリックしたらアクティブになり、アクティブなセルをクリックしたらダイアログが開く

せっかくなので今後似たような微妙な仕様変更が来ても対応しやすいようにしましょう。

define([],function(){
  $('.cell').live('click',function(ev){
    $(this).addClass('active');
  });
  $('.cell.active').live('click',function(ev){
    $(this).removeClass('active');
    $(this).trigger('open-schedule-new');
  });
});
define([
    'schedule-model'
    ,'schedule-api'
    ,'schedule-to-selectors'
    ,'growl'
    ,'app/schedule-dialog-template'
    ,'debug'
    ,'jqueryui/dialog'
],function(
    ScheduleModel
    ,ScheduleApi
    ,scheduleToSelectors
    ,growl
    ,scheduleDialogTemplate
    ,debug){
  $('.cell').live('open-schedule-new',function(ev){
    var props = $(this).data('date-properties');
    var dateStr = props.date;
    if (!$(this).closest('.editable').size()) {
      return;
    }

    var initialValue = {startDate : dateStr};
    var html = scheduleDialogTemplate(new ScheduleModel(initialValue));
    $(html).dialog({
      width : 400
      ,buttons : {
        '登録' : function(){
          var $dialog = $(this);
          var $form = $dialog.find('form');
          var schedule = new ScheduleModel();
          schedule.fromForm($form);
          var api = new ScheduleApi();
          api.success = function(createdSchedule){
            growl('スケジュール登録完了');
            $(scheduleToSelectors(createdSchedule)).trigger('redraw');
          };
          api.create(schedule);
          $(this).dialog('close');
        }
        ,
        'キャンセル' : function() {
          $(this).dialog('close');
        }
      }
    });
  });
});

ダイアログを開くという挙動をopen-schedule-newというcustom eventにしました。
ダイアログを開くという挙動と、ダイアログを開く為のユーザのアクションを検知する箇所を分離したわけです。

呼び出し元からの依存の除去

仕様追加が入りました。

今まではカレンダー画面だけでしたが、別にスケジュールの一覧表画面も作る。
スケジュールの一覧表画面には、スケジュール新規追加ボタンがあって、
そこをクリックするとカレンダーのセルをクリックした時と同じようにスケジュール登録ダイアログを開いて、
そこからスケジュール登録を出来るようにして欲しい。

とのことです。

define([],function(){
  // カレンダー画面用
  $('.cell').live('click',function(ev){
    if (!$(this).closest('.editable').size()) {
      return;
    }
    $(this).addClass('active');
  });
  $('.cell.active').live('click',function(ev){
    $(this).removeClass('active');

    // custom data attributeから値を取得するという処理を移動したことで、
    // open-schedule-new eventの対象要素にcustom data attributeを設定することが必須ではなくなる
    var props = $(this).data('date-properties');
    var dateStr = props.date;

    // ダイアログの初期値は開始日が、そのセルの日付
    var initialValue = {startDate : dateStr};
    $(this).trigger('open-schedule-new',[initialValue]);
  });
});
define([],function(){
  // スケジュール一覧表画面用
  $('.cell.schedule-new-open-button').live('click',function(ev){
    $(this).removeClass('active');
    // ダイアログの初期値は全空欄
    var initialValue = {};
    $(this).trigger('open-schedule-new',[initialValue]);
  });
});
define([
    'schedule-model'
    ,'schedule-api'
    ,'schedule-to-selectors'
    ,'growl'
    ,'app/schedule-dialog-template'
    ,'debug'
    ,'jqueryui/dialog'
],function(
    ScheduleModel
    ,ScheduleApi
    ,scheduleToSelectors
    ,growl
    ,scheduleDialogTemplate
    ,debug){
  $('.cell').live('open-schedule-new',function(ev,initialValue){
    var html = scheduleDialogTemplate(new ScheduleModel(initialValue));
    $(html).dialog({
      width : 400
      ,buttons : {
        '登録' : function(){
          var $dialog = $(this);
          var $form = $dialog.find('form');
          var schedule = new ScheduleModel();
          schedule.fromForm($form);
          var api = new ScheduleApi();
          api.success = function(createdSchedule){
            growl('スケジュール登録完了');
            $(scheduleToSelectors(createdSchedule)).trigger('redraw');
          };
          api.create(schedule);
          $(this).dialog('close');
        }
        ,
        'キャンセル' : function() {
          $(this).dialog('close');
        }
      }
    });
  });
});

ユーザのアクションを検知するmoduleをカレンダー画面用とスケジュール一覧表画面用の二つに分けました。
また、ダイアログを開くmoduleの側で特定のマークアップに依存していた処理を、
ユーザアクション検知moduleの側に移動しました。
なお、jQuery custom event 応用編の引数付き関数呼び出しのパターンを使っています。


これで、ダイアログをどう開くか?についてはかなり自由度が上がりました。
cell classが振られた要素に対して、open-schedule-new eventをtriggerしてやればいいのです。
ユーザがどんなアクションをしたらダイアログが開くか、
あるいはユーザのアクション以外によって開くのか、
初期値はなんかのか、すべて自由です。
open-schedule-new eventをtriggerするmoduleで好きにその部分だけ好きに差し替えてやれば良いのです。


では登録完了後のcallbackの方はどうでしょうか?
グロール風メッセージ表示は今のところ両方で出したいのでいいですが、あくまで今のところです。
メッセージを出したくない箇所で使いたくなるかもしれません。
セルのCSSセレクタ→要素を取得し、.trigger('redraw')して再描画するところですが、
これではスケジュール一覧表画面では、エラーは起こらないものの再描画が出来ません。

callbackからの依存の除去

スケジュール一覧表画面でも再描画が効くようにしましょう。
ユーザアクション検知moduleは変更していないので省略します。

define([
    'schedule-model'
    ,'schedule-api'
    ,'app/schedule-dialog-template'
    ,'debug'
    ,'jqueryui/dialog'
],function(
    ScheduleModel
    ,ScheduleApi
    ,scheduleDialogTemplate
    ,debug){
  var $cell = $('.cell');
  $cell.live('open-schedule-new',function(ev,initialValue){
    var html = scheduleDialogTemplate(new ScheduleModel(initialValue));
    $(html).dialog({
      width : 400
      ,buttons : {
        '登録' : function(){
          var $dialog = $(this);
          var $form = $dialog.find('form');
          var schedule = new ScheduleModel();
          schedule.fromForm($form);
          var api = new ScheduleApi();
          api.success = function(createdSchedule){
            $cell.trigger('api-success',createdSchedule);
          };
          api.create(schedule);
          $(this).dialog('close');
        }
        ,
        'キャンセル' : function() {
          $(this).dialog('close');
        }
      }
    });
  });
});
define('api-success-growl',['growl'],function(growl){
  $('.cell').live('api-success',function(ev,schedule){
    growl('スケジュール登録完了');
  });
});
define('api-success-redraw-calendar',['schedule-to-selectors'],function(scheduleToSelectors){
  $('.cell').live('api-success',function(ev,schedule){
    $(scheduleToSelectors(schedule)).trigger('redraw');
  });
});
define('api-success-add-schedule-to-list',[],function(){
  $('.cell.schedule-new-open-button').live('api-success',function(ev,schedule){
    // addRowというメソッドが一覧表示を実現するためのjQuery pluginで定義されていて、
    // そこにmodelを渡すと行を追加してくれるということになっている
    $('#schedule-list').addRow(schedule);
  });
});

今までは登録完了callbackの中に直接処理を書いていましたが、
それを$cell.trigger('api-success',createdSchedule);だけにしました。
カレンダー画面ではapi-success-growlapi-success-redraw-calendarの2moduleを読み込んでおけば、
今まで通りgrowl風に登録完了メッセージを表示→セルの内容更新という風に動きます。
スケジュール一覧表画面ではapi-success-growlapi-success-add-schedule-to-listを読み込んでおけばOKです。


jQuery custom event 応用編で言えば、「callbackパターン」と「同名のn(≧0)個の関数の呼び出しパターン」を使っています。

まとめ

ユーザのアクションでwidgetの表示を行い、そのwidgetに対するユーザの操作によりメインの処理を行い、その結果を画面に反映させる。
ということをやりたい場合、

  • widget表示のきっかけになるユーザのアクションの検知
  • 結果の画面への反映

を取り除いたcustom eventベースのmoduleを作りましょう。
そうすれば様々な場所で再利用することが出来ます。

後付drag&drop

draggable未導入状態

先程のカレンダーのセル内再描画用custom eventです。

define('schedule-redraw',[
  'app/schedule-elem-template'
  ,'debug'
],function(
    template
    ,debug){
  $('.cell').live('redraw',function(ev){
    // 説明用のコードなので細かいところは置いておくとして、
    // セルに表示すべきスケジュール全件のmodelの配列を取得している
    var $cell = $(this);
    $cell.remove();
    var props = $cell.data('date-properties');
    var dateStr = props.date;
    var schedules = $cell.closest('#calendar').data('calendar').findSchedulesByStartDate(dateStr);
    schedules.forEach(function(schedule){
      // 細かいことはさておき1スケジュール分の部分HTMLを作ってくれる。
      var html = template(schedule);
      $(html).appendTo($cell);
    });
  });
});
draggable導入

仕様変更が来ました。

スケジュールをドラッグ&ドロップ出来るようにして欲しい

とりあえずdraggableを導入してdrag出来るようにします。
本当はdroppableで対応するdrop先についても色々やらないといけません。
でも今は、そんな事はどうでもいいんだ。重要な事じゃない

define('schedule-redraw',[
  'app/schedule-elem-template'
  ,'debug'
  ,'jqueryui/draggable'
],function(
    template
    ,debug){
  $('.cell').live('redraw',function(ev){
    var $cell = $(this);
    $cell.remove();
    var props = $cell.data('date-properties');
    var dateStr = props.date;
    var schedules = $cell.closest('#calendar').data('calendar').findSchedulesByStartDate(dateStr);
    schedules.forEach(function(schedule){
      var html = template(schedule);
      var $schedule = $(html);
      $schedule.appendTo($cell);
      $schedule.draggable();
    });
  });
});
draggableを後付可能にする

また仕様変更が来ました。

こっちの画面のカレンダーではドラッグ&ドロップしたいが、
あっちの画面のカレンダーではドラッグ&ドロップ出来ないようにして欲しい

moduleの中の一部だけを差し替えたい状況です。
サンプルコードだと1行ですが、まあ実際に書くとそこそこの分量になるでしょう。

define('schedule-redraw',[
  'app/schedule-elem-template'
  ,'debug'
],function(
    template
    ,debug){
  $('.cell').live('redraw',function(ev){
    var $cell = $(this);
    $cell.remove();
    var props = $cell.data('date-properties');
    var dateStr = props.date;
    var schedules = $cell.closest('#calendar').data('calendar').findSchedulesByStartDate(dateStr);
    schedules.forEach(function(schedule){
      var html = template(schedule);
      var $schedule = $(html);
      $schedule.appendTo($cell);
      $schedule.addClass('schedule');
      $schedule.trigger('create');
    });
  });
});
define(['jqueryui/draggable'],function(){
  $('.schedule').live('create',function(ev){
    $(this).draggable();
  });
});

こんなふうにしました。
まず、要素を生成追加後にcreate eventをtriggerしてやるようにします。
そして、create eventを拾って要素をdraggableにするmoduleを作ります。
draggableにするmoduleを読みこめばdrag出来、読み込まなければdrag出来ないという差し替えを、
schedule-redrawを一切変更せずに出来るようになりました。

まとめ

要素生成後にcreate eventをtriggerすることで、
要素生成後の処理に、プラガブルな拡張ポイントを用意することが出来ます。
jQuery UI Draggable等要素に手を加え機能を付加するようなものを使う場合大変有用です。

おまけjQuery Create Event plugin

ちなみに、jQuery Create Eventというpluginがあって、

$(selector).live('create',handler)

とやると、DOM操作の際にselectorを満たす要素が出来ると.trigger('create')してくれます。
多分、軽い画面なら手軽で便利だと思います。
しかし、DOM操作が大変多い画面でFirebugでprofileを取ってみたところ、
これのDOM操作の対象要素の中からselectorを満たすものを探索する関数が
全体のCPU時間の1割を消費していたので導入をやめました。

まとめ

jQuery custom event 応用編のテクニックを使えば、UIコンポーネントの機能から、
要件やコンポーネントの配置先の構造によって変わりやすい部分を外出しして、本体の再利用性を高めることが出来ます。
また、プラガブルな拡張ポイントに差し込むパーツをそれぞれ単機能にすることで、
こちらも再利用が容易になります。

jQuery custom event 応用編

前置き

custom eventとは何か?(前置きの前置き)
  • ブラウザがサポートしているeventではない独自定義event。
    • clickとかはブラウザがサポートしているevent
  • ユーザのアクションやブラウザの状態等によって直接発火されることはない
    • click eventは、ユーザがマウスポインタを要素の上に移動後にマウスのボタンを押すと発火される
    • DOMContentLoaded eventは、ページのHTMLがダウンロード完了し、すべてパースされると発火される
  • jQueryではtrigger,triggerHandler methodでeventをユーザのアクション等に関係なく発火することが出来る
    • custom eventも発火出来る

以前The JUI 2009 Returnsで話をしました。
http://dl.dropbox.com/u/612874/slide/jui20090710/s6maker.html

前置き本編

eventの発火については、事前に登録されている関数(eventHandler)が実行されるくらいに思っている人も多いと思うが、
custom eventで通常のプログラム並かそれ以上の豊かな表現ができるという話をします。

普通のプログラムで出来ることをcustom eventでもやってみる

(通常の)関数呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-function-call

uu.ready(function(){
  function fun(){
    uu.log('fun is called.');
  }
  fun();
  $('#target').bind('fun',function(ev){
    uu.log('fun is triggered.');
  });
  $('#target').trigger('fun');
});

一番基本的なものです。
function 関数名(){}で関数を定義し、関数名()で関数を呼び出すというのと同じような事は、
.bind(eventType,関数)で特定の要素にeventを貼りつけ、.trigger(eventType)で発火させるということで出来ます。

引数付き関数呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-function-call-with-args

uu.ready(function(){
  function fun(a,b){
    uu.log('fun is called. @,@.',a,b);
  }
  fun('hello','world');
  $('#target').bind('fun',function(ev,a,b){
    uu.log('fun is triggered. @,@.',a,b);
  });
  $('#target').trigger('fun',['hello','world']);
});

.bind(eventType,関数)で渡す関数(callback関数)の第一引数はevent objectです。
.trigger(eventType)で発火させるとcallback関数の第一引数としてevent objectが渡されるわけです。
では第二引数以降はどうなっているのかというと、
第二引数に配列以外を渡すと、callback関数の第二引数としてそれが渡され、
第二引数に配列を渡すと、callback関数の第二引数以降に展開されて渡されます。

インスタンス変数へのアクセスを含むメソッドの呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-method-call

uu.ready(function(){
  var obj = {
    prop : 'hogehoge'
    ,method : method
  };
  function method(){
    uu.log('method is called.@.',this.prop);
  }
  obj.method();
  $('#target').data('prop','hogehoge');
  $('#target').bind('method',function(ev){
    uu.log('method is triggered.@.', $(this).data('prop') );
  });
  $('#target').trigger('method');
});

ここまでは関数呼び出しっぽい事をする話でしたが、
今度はオブジェクトのメソッド呼び出しっぽい事をする話です。
custom eventでインスタンス変数っぽいものをどう扱うかというと、
jQueryのdata methodを使います。
data methodはDOM要素ごとに独立した連想配列を扱うものです。

Class

http://jsdo.it/monjudoh/bps41-custom-event-Class

function Class(){}
Class.prototype.method = method;
function method(){
  uu.log('method is called.@.',this.prop);
}


$('.clazz').live('method',function(ev){
  uu.log('method is triggered.@.', $(this).data('prop') );
});
uu.ready(function(){

  var obj1 = new Class();
  obj1.prop = 'hogehoge';
  obj1.method();
  var obj2 = new Class();
  obj2.prop = 'fugafuga';
  obj2.method();
  
  $('<div id="target1" class="clazz"></div><div id="target2" class="clazz"></div>').appendTo('body');
  
  $('#target1').data('prop','hogehoge');
  $('#target1').trigger('method');
  $('#target2').data('prop','fugafuga');
  $('#target2').trigger('method');
});

Classをどう再現するかという話。
これにはbindによる要素へのevent貼付けではなく、
live methodによるCSSセレクタへのeventの貼付けを使います。
あるCSSセレクタ(例えばclassセレクタ)に対してliveでeventを貼り付け、
その後そのCSSセレクタを満たす要素(例えばclass属性に該当classが含まれている)を作成するのは、
Classを定義し、そのインスタンスを生成するのに似ています。


liveについては以前BPStudyで発表しました。jQuery1.4aでのlive event/special event - 文殊堂

mixin

http://jsdo.it/monjudoh/bps41-custom-event-mixin

var module = {method:method};
function method(){
  uu.log('method is called.@.',this.prop);
}


$('.clazz').live('method',function(ev){
  uu.log('method is triggered.@.', $(this).data('prop') );
});
uu.ready(function(){

  var obj = {prop:'hogehoge'};
  try {
    uu.log("method追加前にmethod呼び出し");
    obj.method();
  } catch(e) {
    uu.log(e);
  }
  $.extend(obj,module);
  uu.log("method追加後にmethod呼び出し");
  obj.method();
  
  $('<div id="target">').appendTo('body');
  
  $('#target').data('prop','hogehoge');
  uu.log("addClass前にtrigger");
  $('#target').trigger('method');
  $('#target').addClass('clazz');
  
  
  uu.log("addClass後にtrigger");
  $('#target').trigger('method');
});

後付でobjectにmethod群を生やすmixinです。
事前に、liveでclassセレクタにeventを貼り付けている状態で、
対象の要素のclass属性に該当classを後付で追加してしまえば実現できます。
liveでeventを貼りつけたCSSセレクタを満たすようになるわけですから。

callback

http://jsdo.it/monjudoh/bps41-custom-event-callback

function WebApiWrapper(){}
WebApiWrapper.prototype.call = call;
function call(){
  // Timerで大体500ms後に非同期でcallbackを実行する
  // WebAPI等をXHRで叩いてcallbackを実行するのの代わり
  var self = this;
  setTimeout(function(){
    (self.callback || function(){})('result');
  },500);
}


$('.clazz').live('call',function(ev){
  var self = this;
  // Timerで大体500ms後に非同期でcallbackを実行する
  // WebAPI等をXHRで叩いてcallbackを実行するのの代わり
  setTimeout(function(){
    $(self).trigger('callback',['result']);
  },500);
});
$('.clazz').live('callback',function(ev,result){
  uu.log('callback is triggered.@.', result);
});
uu.ready(function(){

  var obj1 = new WebApiWrapper();
  obj1.callback = function(result){
    uu.log('callback is called.@.',result);
  };
  obj1.call();
  
  $('<div id="target1" class="clazz">').appendTo('body');
  
  $('#target1').trigger('call');
});

JavaScriptではAjax等callback関数を登録し、
何かのタイミングでそれが呼ばれるといったコードを頻繁に書きます。
これをcustom eventで再現するにはどうすればいいかというと、
そもそも、eventからしてcallback登録→何かのタイミングでそれを実行、
というものなので特に頑張ることはありません。

普通のプログラムでは出来ないことをやる

mixout

http://jsdo.it/monjudoh/bps41-custom-event-mixout

$('.clazz').live('method1',function(ev){
  uu.log('method1 is triggered.@.', $(this).data('prop') );
});
$('.clazz').live('method2',function(ev){
  uu.log('method2 is triggered.@.', $(this).data('prop') );
});
uu.ready(function(){
  $('<div id="target">').appendTo('body');
  
  $('#target').data('prop','hogehoge');
  uu.log("addClass前にtrigger");
  $('#target').trigger('method1');
  $('#target').trigger('method2');
  $('#target').addClass('clazz');
  uu.log("addClass後にtrigger");
  $('#target').trigger('method1');
  $('#target').trigger('method2');
  $('#target').removeClass('clazz');
  uu.log("removeClass後にtrigger");
  $('#target').trigger('method1');
  $('#target').trigger('method2');
});

mixinの項では要素にclassを振ることで、後付で要素とlive eventを紐付けました。
じゃあ逆に要素からclassを外してしまえば、紐付けを解除することが出来ます。


普通のプログラミングでのmixinはmethod群を持つmoduleを取り込んで、
そのmethod群を生やす訳ですが、その逆、
moduleが持つmethod群を再び外してしまうということが、
custom eventでは実現可能なわけです。

同名のn(≧0)個の関数の呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-many-function-call

uu.ready(function(){
  uu.log("bind前にtrigger");
  $('#target').trigger('fun');
 
  $('#target').bind('fun',function(ev){
    uu.log('fun(1) is triggered.');
  });
  uu.log("1個bind後にtrigger");
  $('#target').trigger('fun');
 
  $('#target').bind('fun',function(ev){
    uu.log('fun(2) is triggered.');
  });
  $('#target').bind('fun',function(ev){
    uu.log('fun(3) is triggered.');
  });
  uu.log("追加で2個bind後にtrigger");
  $('#target').trigger('fun');
});

普通のプログラミングではある名前の関数を呼んだ場合に実行される関数は一つです。
同じ名前で複数の関数を定義することは出来ません(オーバーロードできてもシグニチャも名前に含めれば1つです)。
また、未定義の関数を呼ぶことは出来ません(method missing等でエラーを回避できる言語はあります)。


custom eventではこれが出来ます。
event handlerの登録とeventの発火なので当たり前です。
eventそのものとして見る分には当たり前ですが、
これを関数の定義と呼び出しとして捉えると、

  • 定義されているかどうかを気にせず安全に呼び出せる
  • 好きなだけ追加で定義できる
    • 普通の関数だったら元関数を変数にバックアップし、追加の処理と元関数の呼び出しという内容の関数で定義を上書きすることになる

というスーパー関数と見ることが出来るのです。

まとめ

custom eventでもClassやmixin module等、普通のプログラミングで使う構造をシミュレートできます。
eventとして見れば当たり前のことでも、別の捉え方をすると凄いことだったりします。
これらを生かせばコードの一部をcustom eventベースに書き換えることが、
コードの構造に縛られることなく行うことが出来るのです。