AngularJSでDOMの更新を監視する with iScroll4
iScrollをAngularJSと使いたい案件があったので、それを使うためにいろいろ調べた。
DOMを動かしてScroll
position:fixed
等、モバイルでは一部バグがあったり非サポートだったりして、その大体にiScrollやscrollerjs等といったライブラリを用いる。
これらはDOMをCSSとJSで動かしてあたかもスクロールしているかのように見せるライブラリだ。
いろいろライブラリがあるけれど、とりあえずiScroll4
を使った。
一度使ったことがあったし、ng-iscroll
も使っていたので。
ng-iScroll
Angular x iScrollを使うとなった時、google先生に問うと真っ先にでてくるのだけど、個人的な都合で利用を避けた。
DOMの変更を監視
ng-iScrollのやり方に沿えば、実際iScroll自体は動いたのだけれど、そのままだと、Ajaxで取得したHTML系のコンテンツが非同期的に投入されたときにiscroll.refresh()
してくれない。
そこでどうしたかっていうのが本題。
$watchを使う
以下のコードを見てくれ、どう思う?
scope.$watch(function () { scroller.refresh() });
・・・実はこれでも、XHRで取得したDOMを追加したときにrefreshしてくれる。 でもこの使い方間違っているので注意。
ドキュメントを見てもらえればわかるけれど、$watch
の第一引数は、監視する値を示していて、変更時に走らせる処理は第2引数に指定しなければならない。
一応、第1引数であるwatchExpression
は、functionを渡すことも可能で、値の変更時に呼ばれるのでこれでも動くが、明らかにBKであり、間違っている。
ではどうすればいいのか。
それを知るには、$watch
を含めたscope
のライフサイクルを理解しなければならない。
$watchと$digest
$watchの仕組みや動くタイミングは以下の記事が丁寧にかかれているので参考になった。
$watch
は$digest
で呼ばれており、XHRや値の変更時等にdigest
が呼ばれる。
以下の記事を併せて読むとより理解が深まるんじゃないかと。
AngularJS の $watch, $digest, $apply について書く
DOMの高さの更新を検知したい
さて、$watch
で登録したfunctionがいつどこで呼ばれるかがなんとなくわかったと思う。
今回僕が知りたかったのはiScrollの対象としたDOMの高さが変わったタイミングだ。 つまり、こうすれば取れるはず。
angular.module("myApp").directive("ngScrollable", function () { return { restrict: "A", link: function (scope, elements, attr) { var element = elements[0]; var content = element.children[0]; // スクロールさせたいコンテンツ。適宜セレクタは書き換えるべし。 var scroller = new iScroll(element, {}); scope.$watch(function () { return content.clientHeight }, function { scroller.refresh(); }); } } });
ここまでで2時間かかった・・・ Angular奥が深い
AngularJS x UI-RouterでpushStateを使う
何も考えずにAngularJSを使うと、遷移のURLはハッシュフラグメント(#
)を使うことになる。
しかし、ngRouterの$locationProvider
にはhtml5Mode
という機能があり、こいつを有効にすると、pushStateを使ってURLを構築することができる。
それをUI-Routerで使うよという話。
UI-Routerも$locationProvider
をそのまま利用可能
ほんとうにまんま一緒に有効化できる。
ngRouterの場合:
angular.module("myApp").config(["$locationProvider", function ($locationProvider) { $locationProvider.html5Mode(true); }]);
UI-Routerの場合:
angular.module("myApp").config(["$locationProvider", function ($locationProvider) { $locationProvider.htmlMode(true); }]);
ui-srefの扱い
UI-Routerを使っていて感動したのは、ui-sref
の扱い。
ui-sref
はstateを指定し、そのstateで定義したURLをhrefの属性に変換してくれるDirectiveですが、html5Modeを有効にすると、ハッシュ(#)を取り除いてくれる。
つまり、URLの差し替えとかせずにhtml5Modeに移行できる。 なので、UI-Routerを使っているなら、hrefよりもui-srefを使ったほうが可用性が高いです。
grunt-contrib-connectと併せて使いたい
変更を最小限にhtml5Modeを利用できるのは素晴らしいが、pushStateを使う以上は当然、サーバ側にも手を入れなければならない。
基本的には/
のアクセスは/index.html
にリライトされているだけなので、/hoge
とかのURLを(内部の遷移ではなく)叩いてしまうと、当然Not Foundになる。
わざわざテンプレートをRouterにまかせて管理しているのに、同じようなHTMLを大量に生産するのも無駄過ぎるし、一般的なプロラマなら違う方法があると考える。
connect-rewrite
grunt-pluginではないが、URLに対して特定コンテンツを返す様に設定できるnode-connectのプラグインがある。これをgrunt-contrib-connectのmiddlewareとして利用してやればいい。
/api
, /css
, /js
, /tmpl
, /fonts
, /img
のHTTP-PATHを除くリクエストをすべて/index.html
へリライトしたい場合は以下の様に書く。
connect: { server: { options: { host: "localhost", port: 8000, base: "www", middleware: function (connect, options) { if (!Array.isArray(options.base)) options.base =[options.base]; directory = options.directory || options.base[options.base.length - 1]; // node-moduleを読み込み modRewrite = require("connect-modrewrite"); middlewares = []; middlewares.push(connect.directory(directory)); // modRewriteの設定 middlewares.push(modRewrite([["!/api|/css|/js|/tmpl|/fonts|/img /index.html"]); } } } }
備忘録
pushState対応してて気づいた余談。
RouterのtemplateUrlは絶対パスで記述しよう
相対パスで指定してて、/hoge
なURLでアクセスした時にテンプレートが404になって困った。
templateUrlとしてはhttp://localhost:8000/tmpl/fuga.html
を期待してても、相対パスで書かれていると、/hoge
から/fuga
に遷移しようとしたときに読みに行くURLはhttp://localhost:8000/hoge/tmpl/fuga.html
になる。
WEB屋としてはあまりに当たり前なコトなのだけど、サンプルで書かれてた相対パスをそのまま使っていた。まあ、ハッシュフラグメントでやっている限りは困らないけど。
URLの指定にHashfragment(#
)ではなくHashbang(#!
)を使いたい
$locationProvider.hashPrefix('!');
ってすれば、#!
になる。
Angular UI-Routerのviewsの定義を確認してみた
state.views
の指定方法を理解してなかったので調べた。
基本
viewの名前はビュー名とステート名を@
で結合したviewName@stateName
で構成される。
無名のview
ui-view
は名前を与えず無名のviewとして定義ができる。
<div ui-view></div>
その場合、stateは""
でviewを指定する
$stateProvider .state("example", { views: { "": "example.html" } });
viewのstateを省略した場合
viewsの定義文字列は@
でビュー名とステート名を区切る。
ビュー名に@
が含まれていない場合、ビュー名の末尾に@
と親ステートの名前^1を結合してview名する。
$stateProvider .state("example", { views: { "main": { templateUrl: "example.html" } }) .state("example.child", { views: { "main": { templateUrl: "example.child.html" } } });
このようなstatesを定義した場合、viewsは以下に変換される
views: { "main@": { templateUrl: "example.html" } }
views: { "main@example": { templaetUrl: "example.child.html" } }
viewsを省略した場合
views
は省略することができる。
$stateProvider.state("example", {templateUrl: "example.html"});
上記の場合、viewsの指定は以下に変換される。
views: { "@": { templateUrl: "example.html" } }
rootステートについて
親要素を$stateProvider
に定義しないステートもroot
という親をもっている。
root = registerState({ name: '', url: '^', views: null, 'abstract': true });
これによって、例えば@
だけをviews
に指定した場合は最上位に定義されている無名のui-view
を指定したことになる。
AngularJSでdirectiveを作ってみる
2014-03-25
っていう文字列の日付をyear
, month
, day
の3つのフォームに分割して入力したい、と思った。
一つのngModelからフォーマットを変えてinputを並べればいいとおもって、directiveを使ってみた記録。
<input type="date" name="year" ng-model="user.birth_on"> <input type="date" name="month" ng-model="user.birth_on"> <input type="date" name="day" ng-model="user.birth_on">
directiveの作り方
ngModelController
に$formatters
というプロパティがArrayで存在するので、そいつにフォーマット用のfunctionを登録してやる。
- $filterを利用してdateを整形する
- 整形文字列は、
ui-date-format
の属性で指定できるようにする- 例)
ui-date-format="yyyy"
- 例)
- フォーマットする文字列はmodelから取ってくる
angular.module("app") .directive("uiDateFormat", [ "$filter", function ($filter) { return { restrict: "A", require: "ngModel", link: function (scope, element, attrs, ctrl) { ctrl.$formatters.push(function (view_value) { return $filter("date")(view_value, attrs.uiDateFormat); }); } } ]);
<input type="date" name="year" ng-model="user.birth_on" ui-date-format="yyyy"> <input type="date" name="month" ng-model="user.birth_on" ui-date-format="M"> <input type="date" name="day" ng-model="user.birth_on" ui-date-format="d">
ui-dateというAngular UIのライブラリ
ui-dateを使えばいいという気はしつつ、ui-dateはjquery-uiとjqueryに依存しているし、そこまでの機能は必要なさそうだった。 なので、勉強を兼ねて作ってみた。
AngularJS x UI-Routerの使い方
UI-Routerはとても強力で、ngRouterよりも多くの事ができるのでとても便利。 しかし、強力すぎてRouterの概念自体を塗り替えてしまっていてなかなかピンと来てなかったのでまとめてみる。
リストページと詳細ページを別々のページとして用意する
state-router.js
app.config(["$stateProvider", function ($stateProvider) { $stateProvider .state("list", { "url": "^/list", "templateUrl": "list.html", "controller": "ListCtrl" }) .state("detail", { "url": "^/detail/:detail_id", "templateUrl": "detail.html", "controller": "DetailCtrl" }) ; }])
index.html
<div ui-view></div>
list.html
<ul ng-repeat="content in contents"> <li><a href="#/detail/{{content.id}}">{{content.title}}</a></li> </ul>
detail.html
<dl> <dt>ID</dt> <dd>{{detail.id}}</dd> <dt>Title</dt> <dd>{{detail.title}}</dd> </dl>
リストと詳細がひとつのページの中にある
2カラムレイアウトでlistとdeitalを同じページの中に内包する。
state-router.js
app.config(["$stateProvider", function ($stateProvider) { $stateProvider .state("contents", { "templateUrl": "contents.html", "controller": "ContentsCtrl" }) .state("contents.detail", { "views": { "detail@contents": { "templateUrl": "detail.html", "controller": "DetailCtrl" } } }) ; }])
index.html
<div ui-view></div>
list.html
<nav class="left-column"> <ul ng-repeat="content in contents"> <li><a ui-sref="contents.detail({id: content.id})">{{content.title}}</a></li> </ul> </nav> <div class="right-column" ng-view="detail">
detail.html
<dl> <dt>ID</dt> <dd>{{detail.id}}</dd> <dt>Title</dt> <dd>{{detail.title}}</dd> </dl>
2カラムレイアウトでdetailをURLで指定できるようにする
/content
でリストとindexを、/content/{detail_id}
で指定したdetailを表示できるようにする。
state-router.js
app.config(["$stateProvider", function ($stateProvider) { $stateProvider .state("contents", { "url": "^/content", "templateUrl": "contents.html", "controller": "ContentsCtrl" }) .state("contents.detail", { "url": "/:id" "views": { "detail@contents": { "templateUrl": "detail.html", "controller": "DetailCtrl" } } }) ; }])
まとめ
- stateは継承が可能
.
で連結することで、親.子
というstateを定義できる
- viewsに
ビュー@ステート
を指定すると、親ステートで定義したui-view
にtemplateを埋め込むことができる - stateをネストした場合、URLは親のURLに追加される形で指定できる
^
からURLを開始する(^/content/:detail
というように)ことで相対ではなく絶対パスで指定できる
href
の代わりに、ui-sref
属性を用いることで、URLではなくstateをリンクに指定できる。ui-sref
を利用するばあい、$stateProviderに定義されてないstateを指定するとリンクにならない
UI-Routerの理解に一日費やしたので、とりあえず僕の理解の範囲をまとめた。 ややこしいけど大変便利!
homebrewでperlオプションのついたvimをインストールする術
Macでちょっと複雑な一斉置換を行いたかったのだけれど、perldoが無いって起こられたのでいろいろやった。
perlフラグが立っていない
$ vim --version | grep perl +cmdline_compl +insert_expand -perl +user_commands
perlのoptionを確認する
試しに打ってみたら、居たっぽい
$ brew options vim --disable-nls Build vim without National Language Support (translated messages, keymaps) --override-system-vi Override system vi --with-client-server Enable client/server mode --with-lua Build vim with lua support --with-mzscheme Build vim with mzscheme support --with-perl Build vim with perl support --with-python3 Build vim with python3 support --with-tcl Build vim with tcl support --without-python Build vim without python support --without-ruby Build vim without ruby support --HEAD install HEAD version
入れなおす
$ brew uninstall vim $ brew install vim --with-perl --with-lua --override-system-vi ==> Installing vim dependency: lua ==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/lua-5.1.5.mountain_lion.bottle.tar.gz ######################################################################## 100.0% ==> Pouring lua-5.1.5.mountain_lion.bottle.tar.gz 🍺 /usr/local/Cellar/lua/5.1.5: 15 files, 300K ==> Installing vim ==> Downloading http://ftp.debian.org/debian/pool/main/v/vim/vim_7.4.161.orig.tar.gz ######################################################################## 100.0% ==> ./configure --prefix=/usr/local --mandir=/usr/local/Cellar/vim/7.4.161/share/man --enable-multibyte --with-tlib=ncu s==> make ==> make install prefix=/usr/local/Cellar/vim/7.4.161 STRIP=/usr/bin/true 🍺 /usr/local/Cellar/vim/7.4.161: 1566 files, 25M, built in 108 seconds
確認する
:perl[tab]
したらperldoされて無事使えたのでこれで完了。
やっぱPerlは最高だぜ!!!!
Heimdall.jsを0.0.2にあげたよ #開発二部
金剛改二がかわいすぎて何も考えずにクリックするだけなのが最近の提督業です。 こんばんわ、Heimdall.jsを0.0.2にアプデしました。
いんすとーる?
bowerでインストールしとくと管理が楽なのでたとえば、
$ bower install --save 'rymizuki/Heimdall#0.0.2'
とかすればいいと思います。ヽ(=´▽`=)ノ
なにが変わったの?
主な更新は二点
- オレオレコーディングルールだったのを一部jquery-pluginの流儀に合わせた
- 特定のinputに対してバリデーションできるようになった
一つ目のは単にgrunt周りとjshint周りを調整したのとか細かいのだけ。ホントはインデントとかメソッド名とかも調整したかったんだけど、結構手間・・・だったので・・・。 気付きとしては、testにjshintかけるの大切だなぁと思ったこと。
testemを使ってmochaでテストを走らせているのだけれど、method名間違ってたりtypo系のうっかりミスが多かったりして、この作業中に気づいた事が多々。 javascriptのsyntax errorが出ると走らないんだよね、テスト。でも正常終了する。怖い。 完璧ではないけれど、ブラウザでの動作確認、及びjshintによる制約掛けは大切ですね!
ふたつめ。 布教も兼ねてformizeつかって説明します。←
var heimdall = $.heimdall({ name: ['required', ['length', [1, 10]], age : ['required, 'int'], // ... その他のrule }); var form = $.forimze('#my-form'); form.getControl('name').on('keyup', function () { var $this = $(this); var result = heimdall.validate('name', $this.val()); if (result.has_error()) { alert('nameは1文字以上10文字以下じゃないとダメなんだお ヘ(゚∀゚ヘ)アヒャ'); } }); form.on('submit', function () { var data = form.getValues(); var result = heimdall.validate(data); if (result.has_error()) { alert('正しくない入力があるから送信できないよぅ (´Д⊂グスン'); } else { $.post(url, data).done(function () { alert('送信したお ヽ(=´▽`=)ノ'); }); } });
name
というname属性を与えたinput要素のkeyupイベントに、入力値が1文字以上10文字以下じゃないとnameは1文字以上10文字以下じゃないとダメなんだお ヘ(゚∀゚ヘ)アヒャ
という文言を表示するなんとも鬱陶しいコードが出来上がりました。たぶん動くはず。
何が言いたいかというと、formって単位じゃなくてinputの単位でも入力値のチェックが簡単にできるようになったということで、よくある入力がOKになるまで赤文字でエラー文言だしとくとか、入力値がエラーになったタイミングで表示を切り替えるとかが簡単にできるようになったわけです。ビバリッチUI!
ぜひつかって見てね☆ミ