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!
ぜひつかって見てね☆ミ
formizeっていうjquery用のpluginを書いた
description
formにデータと突っ込んだり、データを取得するときに、$('#form').find('[name]').val()
みたいなコードを毎回毎回書くのが面倒だったのでplugin化しました。
getValue(s)
で取得、setValue(s)
で設定できます。エラーがあった時とか入力にバインドしてごにょごにょしたいときように、getControl(s)
で対象要素のjqueryオブジェクトを取得できるようにしてあります。
取得は$form.find('[name]')
で取れるようにしてあり、name属性にサーバー側で受け取りたかったりロジック的に管理したい名前を振ればシームレスにデータ処理ができます。便利ぃ。
Heimdallに同梱しようと思ってた機能なんですけど、こっちのほうが汎用性たかそうだなって思ったので分離して別のpluginにしました。
examples
<form id="example"> <input type="hidden" name="id"> <input type="text" name="title"> <input type="text" name="author"> <textarea name="body" rows="4"></textarea> <select name="publish_state"> <option value="publish">公開</option> <option value="unpublish">非公開</option> </select> <button type="submit">登録</button> </form> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script> <script src="/js/formize.js"></script> <script> $(function () { var $form = $('#example'); var form = $.formize($form); $.getJSON('/api/memo/' + url.param('id')) .done(function (data) { form.setValues({ id: data.id, title: data.title, author: data.author, body: data.body, publish_state: data.publish_state }); }); $form.on('click', function () { var data = form.getValues(); $.post('/api/memo', data).done(function () { alert('登録したよ'); }); }); }); </script>
todo
- checkboxやradioのデータを取れるようにする
repository
requirejsで管理してるjsのキャッシュを管理する方法
requirejsはいいツールなんだが初めてjsがキャッシュに残ってしまって更新されなくてどうしたもんかと調べたので記録。
urlArgs
requirejsのconfigにurlArgs
というのがある。
読んで字の通り、requirejsで読み込んだjavascriptファイルのurlのクエリパラメーターに適当なパラメーターを設定できるというもの。
require.config({ urlArgs: 'v=2', paths: { ...
これでファイル更新にともなうcacheの更新が簡単になった。
今後の展望
package.jsonとかのversionみてリリースのタイミングでまとめてversionアップとかできたら、それはとっても素敵だなって
# 参考にしたサイト
HeimdallというjQuery/Zeptoで使えるFormValidatorを書いた話
温泉発火村#2に参加しましたという記事で紹介したライブラリの話をもうちょっと突っ込んで話します。
Synopsis
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script> <script src="/js/heimdal.min.js"></script> <script> // validatorのインスタンスを生成 var heimdall = $.heimdall({ "name" : ["required", ["length", [1, 32]]], "age" : ["required", "int"], "gender" : ["required", ["select", ["male", "female"], "message" : [ ["length", [0, 256]]] }); $('.form') .on('submit', function () { var result = heimdall.validate({ "name": $('#input-name').val(), // みたいな感じでフォームのデータを{name: value, }でvalidateに渡す ... }); if (result.has_error()) { // エラーがあったら ... $(this).trigger('invalid', [result]); // エラーだよってイベントのトリガ発火するとか。 } }) .on('invalid', function (e, result) { // エラーを表示するなり、アラートを出すなり、リダイレクトするなり。 });
Heimdallがやること
- データの制約(ルール)の定義と保持
- 与えられたデータが制約に基づくものかの検証
- すべてのデータが制約通りのものかどうかの判定
制約(ルール)の定義
言わずもがな、必須入力、数値入力、E-mail、電話番号その他もろもろ、入力フォームに応じ仕様と照合して「何が正しいデータなのか」を定義するもの。
この制約は、formのnameアトリビュート毎にArrayで制約を定義する。 制約は独自のものを定義することもできる。
heimdal.load_constraints('my_rule', function (value) { // `tamanegi`以外の入力は受け付けない return value === 'tamanegi' ? true : false; });
より柔軟に仕様に対応することができ、またheimdallの拡張も可能。
デフォルトでは、
- requires: 必須入力
- length: 入力文字列の上限・下限を設定できる
- select: 文字列の集合の中に含まれている文字列である
- int: 整数値である
が定義されている。
与えられたデータが制約に基づくものかを検証
いわゆるバリデーションの機能。 与えるデータは、formのnameアトリビュートをキーとしたオブジェクトを期待する。
すべてのデータが制約通りのものかどうかの判定
バリデーションはその結果をResultのオブジェクトとして返す。 これはエラーの有無/エラーの内容を取得することができる。
これは主にForm上でユーザにエラーの内容を表示するために利用する、ことを想定している。
Heimdallの背景
そもそもなんで「ヘイムダル」なのか。北欧神話の神が由来で、巨人の軍勢がビフレストを渡ってアースガルズへ侵攻するのを知らせる、という門番の役割を持ってる、ということから。
WEBサイトのフォームは、CGIの時代から多くのパターンが考慮され、またサイトの仕様に密接に関連している。使い勝手もサイトによって千差万別。
「サーバ側で不適切なデータであれば弾けばいい」というものから、「よりユーザにわかりやすいように」と考慮されたリッチなフォームまで存在する。
2013年において、入力フォームはJavaScriptを用いて多くの場合において逐一サーバーにリクエストを投げなくても正しい/正しくないの判定を行うことができ、かつ柔軟に表示を切り替えるようになっている。それも、端末にさほど負荷をかけずに。(例外はあるけれど
しかし、その実装は非情に面倒でありバリデーションようにライブラリは多くあるけれど、なかなか僕の好みに合うライブラリがなかった。
- バリデーションの制約を拡張できる
- エラーが出た時にその事由を取得し、シームレスにUIへ伝達できる
- jQuery, Zeptoのどちらでもつかえること
- (僕個人としての)直感的なインターフェース
4つめは、単純に気持よくかけるかどうかなんですが・・・
Heimdallはこの辺を指標として実装しました。
Heimdallの目指すトコロ
正直、Heimdallには足りない機能がある。
- formに入力されているデータの取得する機能
- formにエラーを伝達しUIを更新する機能
- 単体のinputに対するバリデーション
formとの連携は、悩んだ結果、Heimdallから外すことにした。formの構造や取得方法は、Heimdallに依存してしまう。汎用性を求めようとすれば、その分ソースが膨らむ。メンテナンスコストも。
なので、それは(formaize)https://github.com/rymizuki/jquery-formizeというライブラリに委譲した。
formizeの構造・機能を許容できるのなら、それによって簡単にデータの取得/更新が可能になる。
単体のinputに対するバリデーションは、今後の課題として今もなお検討中。 なかなか良いアイディが浮かばず悶々としている。
あとがきてきな
さて、なんでこんな記事を書いているかというと、@karupanerura に触発されたというのが一番の理由なんだけれど。
jQueryやHTML5の登場で、リッチなUIの実装はそのハードルは大分下がった。 しかし、それでもまだ「全角数字のみを受け入れるフォーム」「どのinputが間違っているのかわからないエラー」等の実にイケてないフォームは未だ多くある。
わざわざサーバにリクエストしてページを遷移しなくても、そのフォーム、そのページ内でエラーをハンドルできるんだから、通信とか無駄に時間を取って、ユーザ体験を損ねているだけじゃね? って思うことは多々ある。
多々あるが、それでも「じゃあJSで書きなヨー」とは言えない。それなりにコストがかかるし、僕もやっぱりPerlで書いたほうが早いし楽だって思うから。
Heimdallしかり、formizeしかり、僕はそういうハードルを下げたいと思ってる。もっとお手軽に、もっと少ないコードで、もっと素早く実装できるようにしたい。
これらのライブラリは少なくとも僕がストレスに思っていた、あるいはコピペで済ませていたやつを集約して汎用化したものなので、まだまだ至らない点は多くあると思う。 それでも、こうして公開して、自分だったり自分以外の誰かに使ってもらって、コードに対する知見をためて、改善していくことができるのも、いまのWEBがあってこそ。Github万歳ヽ(^o^)丿
何が言いたいかというと、全角数字入力のフォームとかうざいし通信の待ち時間とかストレスでそういうフォームを撃滅したいので簡単に実装にできるようなプラグインのために試してIssueとかぷるりくとか投げたり投げられたりしてだせーWEBサイトを減らしていこうぜ!!!
Heimdall、よろしくお願いします。
――と、SYNOPSIS以降はガリレイドンナ見ながら書いてたのでおかしな文章になってるかもしれないけど、がんばって明日はfromizeの記事を書きたいな! サムライフラメンコ見て寝よう。