ばかおもちゃ本店:Youtube twitter:@sashimizakana Amazon.co.jpアソシエイト
ラベル AngularJS の投稿を表示しています。 すべての投稿を表示
ラベル AngularJS の投稿を表示しています。 すべての投稿を表示

2015年4月7日火曜日

Javascriptでemojiを扱うライブラリ書いた

emoji

emojiは実は権利的に不透明なアイコンを使わなくてもSegoe UIとかに含まれているのでフォントで使える。ただブラウザによって表示に違いはあるようだけど、最近ではだいたいどうにかなるっぽい。Windows8.1のIE11とかならカラーで表示することもできる。

ただ、ユーザーに入力してもらうには、Githubみたいに:smile:って書いたらsmileの絵文字を出すみたいな仕組みがほしいよなーということで、GithubのgemojiというemojiライブラリのJSONを使って、エイリアスからフォントを出力するライブラリ、mojierを書いた。

sashimizakana/mojier

npmにも登録してあるのでnpm install mojierで利用することができる。
mojier.get(“smile”)みたいな感じで使うと対象の文字を返す。
ついでにAngularJSのサービスとフィルタとして利用できるようにangular-mojierというのも書いた。

sashimizakana/angular-mojier

これもnpmに登録してあるのでbrowserifyから使ったりrelease/mojier.jsを普通にロードして使ったりできる。

デモを見てもらえばどんな感じで使えるのかわかると思うので、どうぞ。

angular-mojierのデモ

下の海苔巻きのボタンは息子(3歳)が「ぼくおすしのボタンほしい」というのでつけた。
一応エイリアスからemojiを検索できるようになっているので、こういうことも出来るというサンプルとして。jQuery.textcompleteなんかと組み合わせればGithubのemoji入力みたいなものが簡単に作れると思う。

2015年3月15日日曜日

AngularUI ui-selectの使い方

ui-select?

AngularUIには当初select2を利用するためのui-select2ってのがあったんだけど、そもそもselect2をそのまま利用してるので結構無茶があって、どうしたってあまり整った利用方法が出来るようにはなっていなかった。で、ネイティブでselect2とかselectizeとかみたいなものを実装しようよっていうのがui-selectである。
で、これが出来たことでui-select2は非推奨になってこっちに乗り換えてねって感じなんだけど、サンプルがあんまりなかったりサンプルがまともに動いてなかったり、日本語で使うと微妙に不便な部分があったりするので、使い方をちょこっとだけ書く。

使い方

基本的なサンプル


これ見たらもうだいたい分かるような気がするけど一応説明。
まずui-selectには3つのディレクティブがある。

ui-select

ui-selectは全体の親に当たるselectの部分であとの2つはこれの下位要素として設定する。ng-modelで選択結果を設定する。このときng-modelに設定するの$scope直下の要素だとディレクティブのscopeと分離しているせいで更新できないので、サンプルのようにコントローラーのthis以下の要素にするか、scopeにオブジェクトを渡してそのプロパティを渡す必要がある。
multiple属性を設定すれば複数選択になり、taggingにすれば選択肢追加可能になる(後述)。あとthemeというアトリビュートにbootstrap,select2,selectizeという文字でテーマを決められる。これについては、それぞれ対応するライブラリのcssを読み込んでおく必要がある。

ui-select-match

ui-select-matchはマッチ後の選択結果の表示に関するディレクティブで、ここの記述内容で選択されているときの表示を設定できる。たとえばプルダウンにはいろいろ詳細を表示しておいて、結果として表示するときはIDのみ出しておくというようなことが出来る。
単一選択と複数選択のときで結果の変数名が違う。単一選択の場合、$select.selectedというのが選択されている対象で、複数選択の場合、$itemというのが選択結果のうちの一件分にあたる。

ui-select-choices

プルダウンの表示と選択候補の設定を行う。repeatというアトリビュートにng-repeatと同じような記法で選択肢を設定する。表示内容はディレクティブの子要素として{{item.name}}みたいな感じで設定する。プルダウンの絞込は自動で行われるわけではなく、AngularJSのfilterを利用して行う。サンプルだと単純に$select.searchという検索欄に入力中の文字列を示すプロパティをfilterに設定している。たとえばここにカスタムフィルタを設定して、独自記法で検索させるような拡張もできる(id:nnみたいに書いたらidだけ絞り込むとか)。

タギング

タギング(選択肢を追加する)は、multipleの有無に関わらず可能である。
単純な文字列が直接入っているような配列の場合taggingを属性に入れるだけで問題ないが、普通そうなるであろうオブジェクトを選択肢として利用するようなときには、そのオブジェクトの生成処理をtaggingに渡さなければならない。ちなみにこの表記はAngularJSで良く書くようなng-click=”click()”という感じではなく、tagging=”tagging”というような形になる(カッコを入れない)。作ったオブジェクトは関数から返してやると追加される。

以上。他にChromeのタギングでエンターキー余分に押さなきゃならないとか気持ち悪い部分があってそこを直したりしたいんだけど、それはまた解決策がわかったら書く。

2014年2月25日火曜日

Plunkerでゲームをつくってブログに貼るよ

できあがったゲーム


tiny艦隊clicker! これのために作った!(保存できません)

言いたいこと

コードのサンプルはPlunkerとか使って動かせる形にしたほうがわかりやすいし楽しい。

まえおき

前のAngularJSのエントリでPlunkerの埋め込みページを使った。
このページではPlunkerでゲームをざっくり書いて、それをこのブログに貼るまでを説明する。ちなみにゲームそのものの作り方とか内容には触れない。どうせクソゲーだね。

類似サービスの比較

PlunkerはHTML・JavaScript・CSSなんかをWebアプリ上で作成して、公開できるサービス。競合としてはJSFiddle、Plunker、CodePenとか。ざっくりと比較するとこんな感じ。

JSFiddle
Githubなんかでもバグ報告するとバグが再現するfiddleでくれとか言われるのを見かける。とにかく一番メジャー。
ただ、コード全部を単純に書くことが出来るわけじゃなくて、メニューからJavaScriptのロード位置を設定したり、読み込みライブラリも別途指定したりという感じで、最初は多少戸惑うかもしれない。慣れれば別にって程度ではある。

CodePen
見た目がかっこいいしさくっと動く。
コード全体を書けないのはJSFiddle譲りだけど、今風にJavaScriptにCoffeeScriptつかったりLiveScriptつかったり、CSSにSassとかScssとかLESSとか使ったり出来る。普通にこれが一番良いな! と思ったんだけど無料プランだと外部ライブラリが一個しか読めないとか書いてあって使ってない。使い方あるなら是非使いたい。
でも使ってる人はあんまり見たことない。

Plunker
二番手な感じながら、上の二つとは違って、普通のHTMLとかJavaScriptを書く感じで使えるサービス。つまり普通にファイル作ってそこにプログラムを分割できる。ので、そこそこのサイズのプログラムでも作れる。外部ライブラリ云々とかもHTMLから書けるので気にする必要は無い。というか新規作成時に有名なライブラリを一覧にしてくれて、クリックで挿入してくれたりするのが結構嬉しい。
あとエディタのテーマ変えられるのも良い。難点としてはなんか、実際の動作速度なのかデザイン上の問題なのか、なんとなくもっさりした印象があるところ(個人の感想です)。
こっちもPlunkerでくれとか言う人をわりあいと見かける。

というわけで、私はPlunkerを使う。

Plunkerの使い方

というほど詳細な話はしない。だいたいのことは触れば分かる。
Ctrl+Sを押せば保存もできるし、その履歴も残る。
自分が作ったPlunkじゃなければ保存は出来ないけどForkして自分のページに取り込んで編集することができる。左側がファイルとタイトル・概要、右側がプレビュー、外部ライブラリの検索とインポート(別に使わないでも取り込むことは可能)、右側の一番下が設定でエディタの色とハイライトのテーマとか、タブサイズとか、更新の間隔やオンオフを切り替えられる。
作ったものは冒頭の例のようにiframeでブログなどに埋め込み可能だ。

tiny艦隊Clickerについて


好きにForkして改変等してもらってOK。
ただ画像はAmazon S3上に置いてあるので、呼び出し量が増えてお金がかかるようになったりするとこっちの都合で消したり差し替えたりDropboxに置き直したりする可能性がある。そのためフォークしてどこかに公開するなら差し替えたほうが良いと思う(個人で改変したり実験したりする分にはそのまま使ってもらって全然OKです)。
あともしどっかに公開したりするときはこの記事にリンクしてくれると嬉しい。

てなわけで、コードを公開するときにPlunkerとか使って実際に動いていじれるようにしてくれると色々楽しいと思った。

2014年2月9日日曜日

AngularUI Routerのつかいかた

まえおき

AngularUI RouterはAngularJSのルーティングモジュール。
AngularJSの標準のルーティング機能より高機能で、ページ内に複数のviewを持ったり、
階層化されたviewを利用することなどができる。
最終的なプロジェクトの目標はngRouteに取って代わって標準搭載されることだそうで、AngularJS本体も興味を示しているらしい(GitHubのwikiに書いてある)。
で、ちょこっと触ってみて、実際の使い方に触れた解説記事は少ないようなのでざっくり使い方を書く。

https://github.com/angular-ui/ui-router/wiki

ちなみに上記の公式のwikiを読めば分かることしか書いてない。

あとQiitaに抜粋版を書いたので、細かい説明が要らない人はたぶんそっちのがわかりやすい。

AngularJS - AngularUI Routerの使い方 - Qiita

サンプル


以下でやっていることをだいたい盛り込んだサンプルがこれ。
右側のメニューのCodeを押せば各ファイルが、Editを押せばPlunkerで編集できる。Plunkerでいろいろコードを変えてみたりして試してみるとわかりやすいかもしれない(知ってると思うけど、Plunkerで編集したりしても別にこっちのサンプルが壊れたりしないので安心していい)
いちいち対応させて説明することはしない。

つかいかた

モジュールなので、ダウンロードしたら普通のモジュールと同じように読み込む。
ちなみにAngularJS Ver1.2から標準のルーティング機能もモジュール化されてる。
なのでそっからの移行だと、ngRouteを読み込む部分をui.routerに書き換えればいい。

ngRouteとの対比で書いていくと、

$route -> $state
$routeParams -> $stateParams
$routeProvider -> $stateProvider
ng-view -> ui-view

のような感じで置き換える。
メソッド名は同じにしてあるところもあり、使いまわせるところもある。
たとえば$routeParamsなんかは、$stateParamsに置き換えるだけで使える。
ちなみに$locationProvider(html5Mode設定するやつ)はngRouteと関係ないので
そのまま。

$stateProvider

$stateProviderのstateメソッドでルーティングの設定をする。
ngRouteでの$routeProvider.whenを$stateProvider.stateに置き換える。
で、設定はぱっと見似たようなもんだけど、考え方はちょっと違う。

ngRouteはシンプルで、特定のURLに来たらアプリ全体で唯一のng-viewに対して、指定のテンプレートとコントローラを埋め込むというだけのものだ。設定するのはURLとテンプレートおよびコントローラーの対応関係になる。

それに対してui-routerはstate(状態)を設定する。stateがまず先にあり、そのstateに対応するURLを設定し、また、そのstateのとき、どのview(複数もあり得る)にテンプレートとコントローラーを当てるかという設定をする。
stateは階層を持ち、ドットで区切って指定をする。
つまりitemsというstateを設定して、items.detailというstateを設定すると、items.detailはitemsの子として作成される。子として作成されると、viewで設定するテンプレートを表示する位置が親の内側になったり、URLが親のURLに続く感じになる。もっともこれは何も指定しなければそうなるというだけで、設定次第で変更できる(後述)。

とにかく、ngRouteはURLを基準にして動作するが、ui-routerはstateが基準になる。
なので設定も以下のような感じになる。

$routeProvider(url,{設定}) → $stateProvider(state名,{設定})

たとえば/emp/12345みたいなURLがあり、empだけなら社員一覧、12345が渡されたら詳細というような設計なら、以下のようなコードになる。

ngRouteの場合
$routeProvider
    .when("/emp",{
        templateUrl:"list.html"
        ,controller:"ListCtrl"
    })
    .when("/emp/:empID"){
        templateUrl:"emp.html"
        ,controller:"EmpCtrl"
    };
ui-routeの場合
$stateProvider
    .state("emp",{
        url:"/emp"
        ,templateUrl:"list.html"
        ,controller:"ListCtrl"
    })
    .state("emp.detail",{
        url:"/:empID"
        ,templateUrl:"emp.html"
        ,controller:"EmpCtrl"
    });
という感じになる。
上でも書いてるがemp.detailはempの子なので、:empIDというUrlは、empのUrl(/emp)に続く。
つまり/emp/:empIDとなるので、ngRouteと同じURLになる。

もっとも、ngRouteの場合、すべてのテンプレートは常に単一のng-viewに表示されるが、ui-routerの場合この設定だとemp.htmlはlist.htmlの中のui-viewに表示される。
つまり一覧のリストのページの一部に詳細が表示されるような感じになる。
で、先に書いたとおり、ngRouteと同じように単一のviewに表示することもできる。
そのためには表示先のviewを指定すれば良い。そのためにはviewの名前の指定と、参照先のviewを絶対指定することが必要になるので、順に説明する。

viewの名前付け

表示先のviewは名前をつけることができ、これにより一度に複数指定したりできる。
<div ui-view="view1"></div>

<div ui-view="view2"></div>
上記のタグのviewを設定する場合、
.state("emp.detail"),{
    url:"/:empID"
    ,views:{
        view1:{
            templateUrl:"view1.html"
            ,controller:"View1Ctrl"
        }
        ,view2:{
            templateUrl:"view1.html"
            ,controller:"View1Ctrl"
        }
    }
}
のように設定する。viewsは保持するプロパティ名がviewの名前に対応する。
ちなみに最初のコードでは名前を指定していないが、それは以下の設定と同じだ。
.state("emp.detail"),{
    url:"/:empID"
    ,views:{
        "":{
            templateUrl:"emp.html"
            ,controller:"EmpCtrl"
        }
    }
}
これでも問題なく動作する。
ちなみにAngularJSの話からはやや脱線するがJavaScriptでプロパティ名に上記のような空文字やハイフン、アットマークなんかを使ったりするときは二重引用符(")で囲まないとエラーになる。
で、何でそんなことを書くかと言うと、viewsのプロパティでアットマークを使えば、表示対象のviewの位置を親と関係なく絶対指定することができるためだ。

viewの絶対・相対指定

画面遷移とURLの階層は常に対応するわけではない。
ファイラーとか常に階層化された画面構造ならともかく、一番上のサンプルと同じように社員の一覧から、社員の詳細を選び、その社員の投稿した画像一覧をほぼ画面全域に展開したい、なんてことがありうる。その場合、URL上は親子関係があっても、画像一覧の画面は親の表示位置を上書きすることになる。
(画像一覧ページが階層的に詳細の下に入る必要はないとかいうことはさておく)
(さておくけど、うまい設計なら良い感じに階層化できるのかもしれない)

ともかく、viewsのプロパティ名は単純に名前だけ指定すると、親の持つビュー名になる。
だがビュー名@ステート名のように書けば、直接表示対象のビューを指定できる。つまり絶対指定だ。
ちなみにビュー名@で終わる場合は、一番上位の階層のui-viewになる。
一番上位階層のビューが名前無しの場合、ただ"@"で指定することができる。
同じく、各ステートの名前無しビューは、"@ステート名"という形で指定する。
.state("emp.detail.picture"),{
    url:"/all"
    ,views:{
        "@emp":{
            templateUrl:"picture.html"
            ,controller:"PictureCtrl"
        }
    }
}
このコードだと、empの持つ名前無しのビューに表示される。
つまり現在emp.detailが表示されている位置が差し替えられる。

その他設定

設定側で書きたいことはだいたい書けたのであとは適当に列挙。
詳しくは公式のWikiを見てね。

  • 抽象(abstract)指定で親になるが指定はできないステートとか作れる。
  • urlは{id:[0-9]{5}}みたいに正規表現指定できる。
  • urlは^から始めると絶対指定で最上位から始められる。
    • これ移動時にstate指定するときの^(親のステート)と同文字で意味が違ってわかりにくい気がする。

ルートの利用

今まで設定したルートを利用する場合、つまりアンカータグ(a)なんか移動するときは、ui-srefディレクティブを使う。
もちろん#/hogeみたいにhrefとかng-hrefに書いても正しく動作するんだけど、
それだとhtml5Modeを切り替えたりURLの構造を変えた時に全部書きなおすことになる。
ui-srefはステートとパラメータを渡しておけば、あとのURL構造の変更とかhtml5Modeへの対応なんかは勝手にやってくれる。

使い方はこんな感じ。
<a href="#/emp/hoge/1234" .. → <a ui-sref=".hoge({id:empID})"...>
これも相対パスと絶対パスがあって、この例だと現在のstate直下のhogeというstateに移動する。ドットが頭に付くと相対パスで、ドットが無ければ絶対パスで、^から始まると親のステートとか、そんな感じ。詳しくはWikiを参照のこと。
引数で渡すオブジェクトのプロパティ名は当然設定したときにurlが受け取るパラメータ名と一致していないとならない。

ui-sref-activeにそのステートがアクティブなときに付けたいクラスを指定すると、上のサンプルみたいにbootstrapのactiveなんかを簡単に利用できるので何かと楽。ng-classと違って、普通のclass指定とも共存するので便利。

ちなみにコード上から移動するときは$state.go()を利用する。

以上、そんな感じ。

2013年12月21日土曜日

AngularJSのscopeの継承とかを理解する

まえおき

本稿はAngularJS Startup AdventCalendar2013の21日目です。
20日目:AngularJSをTypeScriptで書くときのあれこれ - Qiita [キータ]

scopeについて

scopeはAngularJSでビュー側に値を渡すときに利用するサービス。
たぶん知ってると思う。これがビューとコントローラーがデータをやりとりする。
で、今回はちょっとAngularJSをさわり始めると公式ドキュメントなんかあちこちに出てくる、新しいscopeが作られるっていう部分について書く。
ディレクティブなんかのAPIを読んでると出てくる。このディレクティブは新しいscopeを作りますって、あれってなんなのか。

scopeの生成と継承

scopeは一番上位に位置するrootScopeの下に階層化されて作られていく。
scopeはJavaScriptのレキシカルスコープみたいにブロックを持ち、親子関係を持つ。
なのだけど、少しだけjavaScriptのスコープとは違う部分がある。ここがちょっとだけわかりにくい。

一般的にディレクティブを作るときにscopeを作ることが多いので、それを例に取って説明する。
ディレクティブの作成時にscopeの生成には、false(作らない:デフォルト)、true(作る)、オブジェクト(親を継承せず作る)ってのがある。
一応比較対象としてfalseの作らないやつからfiddleに例を作った。

AngularJS Scope-1 - JSFiddle

これは見たまんまでディレクティブ内でいきなり親スコープのプロパティを読んでるけど、問題なく表示されているし更新も出来る。念のために書いておくと、これはサンプルなのでそういうことをしているけど、普通にこんなことをするとモジュール間の結合度が上がりすぎて最終的に動かないプログラムを作ることになるのでやってはいけない。
ダメ絶対。

普通にやる場合はlinkの第三引数(attrs)からタグに設定された属性の中身を読みとってscope.$watchで受け取ったりする。
なんでそう書いてないのかというとコードを短くしたいってことと、この後のサンプルの意味が分かりづらくなるから。

というわけで、trueを設定した場合。

AngularJS Scope-2 - JSFiddle

trueとfalseが変わっただけだけど、子+を押すと親+は変更されず、以後はまったく独立してカウントされるようになる(なので先に親+から押してください)。これがAngularJSのscopeがJavaScriptのスコープと違う部分で、つまり親を継承して子スコープを作った場合は、親の値は参照可能だけど上書きすることはできないということ。
上書きしようとすると変数名などはそのままに、別の値として作成されてしまう。
これを知らないで居ると全く意味不明な挙動に見えるときがある。

たとえばng-includeなどでもこんな風にscopeは作成される。そのため、たとえば親のscopeの値をそのまま利用して、かつng-include内部でちょこっと数値を変えるようなものを書いてしまうと、親の方で値を操作しているうちは良いけど、ng-include以下で操作すると急に親から値が読めないなどということが起きたりする。

で、最後がscopeにobjectを指定した場合。

AngularJS Scope-3 - JSFiddle

これは親を継承しないので、親の値は全く参照することが出来ない。
つまり動きません。お使いのPCは正常です。

ただこのobjectに親から継承したいものを設定することができる。ここではすべては詳しく書かないけど、親のscopeの変数を変更可能な状態で受け取ることとか、子のDOM内にのみ入れることとか、親のcontextで実行させるとか(ng-click="hoge()"みたいなやつのこと)、ができる。

値を受け取る場合はプロパティ名が子scopeでプロパティ名にしたい名前にして、その値を"=自分のDOMの属性名"にする。つまり<div hoge="***">みたいな場合、scope:{myValue:"=hoge"}というようなこと。
ちなみに子scope内で属性名と同じ名前を使うときは属性名は省略可能。
{hoge:"="}ってな感じ。

いちおうサンプル。

AngularJS Scope-4 - JSFiddle

まとめ

具体的にどういうときに使うんだって言う例を作っておきたかったけど、時間がなかった。実際にバリバリ書いてると絶対に分かってないとダメな局面があったはずなんだけど、なかなかちゃんと再現するコードを書くのはむずかしい。
でもjQuery拡張とかをディレクティブに放り込んでAngularJS対応にしてたりとか、複数のディレクティブをタグに付けまくったりしたりすると、なんか値が上手く変わらんとか思うことがあった。でもこのあたりを理解してからは問題なくなった。

scope作るときに実際どれ使えば良いかっていうので言うと、普通に使うだけならオブジェクトを渡して必要なプロパティだけ共有するのがおすすめ。安全だしミスを起こさないので良いと思う。

余談

$watchで値を受け取るとき、受け取った値がオブジェクトとか配列とかだと参照渡しされているのでscope:trueに設定してあってもそのオブジェクトのプロパティへの変更とかは通常通り行われて親scope側の値に影響する。
まあでも関数の引数と同じ。

2013年12月15日日曜日

AngularJSでゲームをつくる「艦隊くりっかー」

まえおき

これはAngularJS Startup AdventCalendar の15日目の記事です。
14日目:AngularJSとAngularJSモジュールでi18nを実現する(2) - Qiita [キータ]

本稿ではAngularJSでゲームを作ったときに気づいたこととかを元に、どんな感じでアプリケーションを構成するかをざっくり書きます。実際のゲームを作る上でのTipsとかってよりは、基礎的なディレクティブとかサービスの使い方中心ですが、ゲームの実例も多少書くのできっとどう分けるかを理解できるはず。で、それが把握できればゲームをどう作るかもわかるはず。
一応ある程度AngularJSの基礎用語くらいは聞いたことあるくらいの人対象です。
それのどこがスタートアップなんだとか気にしない。泣かない。
ちなみにAngularJSはゲームを作るのに適したフレームワークではないです。
もっとも数値を操作するだけのゲームならAngularJSでも十分です。
そういうわけでAngularJSやろうぜ!

自己紹介

ばかおもちゃ本店という名前でニコニコ動画に電子工作とかの動画をあげています。
全自動スカートめくり機とか、念力スカートめくり機とか、そういうのを作りました。人様に言いづらい。

そして本稿で説明する艦隊くりっかーという二次創作ではない(重要)ゲームを作りました。

艦隊くりっかー?

艦くり - Kantai Clicker

CookieClickerのクローンです。
本当は更に艦隊これくしょんの二次創作のつもりでしたが、ゲームは禁止されているので、
キャラクター要素を削って良くわからない艦隊これくしょん便乗商品のようなものになった。
数値を増やすために数値を増やすという超単純なゲームです。

(本文と関係のない画像です)

AngularJSアプリの大まかな構成

AngularJSのアプリケーションは、ざっくり以下の様な構成になる。

directive
ディレクティブ。HTML内に埋め込んで表示を制御する。値の実際の入出力を処理する。
配列渡されたらフォーマットしてリスト出したり、入力値を値にセットする。

controller
コントローラー。ng-controllerで呼ぶとそのタグの階層以下のscopeを管理できる。
scopeってのはAngularJSでビューと値をやりとりするときのオブジェクトのこと。

service
サービス。値を保存したり読み込んだり、それに伴う通信したり。
もちろん値への操作も入れちゃって構わない。いわゆるモデル。

filtter
フィルター。リストの内容を変えたり値を整形したりするディレクティブの補助的なもの。
今回使ってないのでもうこれっきり出てきません。

つまりAngularJSはHTMLに利用するコントローラーとか含めてdirectiveでテンプレートを書いて、
controllerがそのdirectiveとserviceの間の値のやり取りをscopeってのを使ってやる。
なるほど、かんたん。

各部の詳細とか

以下、詳細というか使ってる時に思ったこととか、注意点みたいなのをまとめる。

controller
AngularJSの公式のサンプルなんかを見て、モジュールを分けることを意識しないで書くとファットコントローラーになって手に負えない状況になりがち。見た目は全部ディレクティブ、値の操作はサービスでする。
ここで値を直接変更してたり、見た目に関連するフラグの変更とかやってるならたぶん間違ってる。
コントローラー間での値の受け渡しとかで悩んでたりするなら考え方が根本的に違う可能性が高い。

directive
雑に言うと、この中でならjQueryとか素で使ってもOK。
なのでjQueryの拡張とかをそのまま使うときもこの中に閉じ込める。
directiveのcompileの第一引数とかlinkの第二引数とかはディレクティブがつけてある要素そのものなのだけど、これはもともとjQueryオブジェクトの形で渡される。
ちなみにAngularJSよりjQueryを先に読み込んでないとjqLiteという機能縮小版になる。
(たしかfindとかなかったりremoveが無かったりして不便なときが多い)

余談だけど、linkのサンプルコードで第一引数のscopeに$マークがないのは、ここは名前で引数が決定されるわけじゃなくて、普通に順番で引数が決定するからだとか何とか。
ECMA3で機械的に挿入されたコードのみで$の利用が許可されるとか言ってるのと関係有るかも(ないかも)。


書き始めると終われないので飛ばしていくけど、directiveはscope周りが一番面倒なので注意深く勉強するといいかも。
親の値をそのまま変更可能だったり(scopeが未設定かfalse)、親の値が上書きのみ別の値になるか指定したもののみ上書きも可(scopeがtrueかオブジェクトで指定)とかある。
ちなみに普通の引数と同じくオブジェクトの場合は参照で渡されるので、falseでもオブジェクトのプロパティの変更のみは出来たりするので混乱の元になる。
自分の作ったディレクティブにng-hideとかを組み合わせて使うつもりが無いなら、スコープは常に作ったほうが事故ることが少ないと思う。
逆に言うと、親スコープ内で使うつもりなら、スコープをそもそも使わないか、親スコープと絶対にかぶらないような名前で利用しない限り危険。うっかり名前かぶったりするとミスに気付きづらくて泣く。

service
複数画面のアプリケーションの場合もserviceはシングルトンなので、データは共有される。
これ重要。
angular.('モジュール名',[依存モジュール...]).factory('ディレクティブ名',[利用サービス...,function(){
    return サービスのオブジェクト;
}]);
みたいな感じで作るわけだけど、もちろん、
var MyService = function(){
    this.initialize.apply(this,arguments);
}
MyService.prototype = {
    initialize:function()...
}
return MyService;
みたいにクラス渡して、コントローラー側で、
['$scope','MyService',function($scope,MyService){
    var svc = new MyService();
}];
てな感じで使ってもいい。
私はこういう感じのクラスにデータ取得するメソッドとキャッシュとか持たせて使ったりしてる。
var MyService = function(){
    this.initialize.apply(this,arguments);
}

MyService.prototype = {
    initialize:function()...
}

MyService.cache = {};

MyService.getData = function(id){
    if(this.cache[id]){
        //キャッシュにあればそれを返す
        return this.cache[id];
    }

    //なければバックエンドとかからデータ持ってくる
    var svc = new MyService(バックエンドから持ってきたデータ);
    this.cache[id] = svc;
    return svc;
}
return MyService;
でMyService.getData(4);みたいな感じで呼び出す。
factoryが呼ばれるのは最初の一回だけなので、複数画面とかならどの画面を最初に開いても、データが既に読み込まれてたらキャッシュを呼ぶし、そうでないならバックエンドからデータを持ってくることになる。
一覧と詳細のリストを持ってて、詳細を開くと追加データを読んでキャッシュするとかそういう使い方に便利。有効期限とかキャッシュの強制削除とかそういうのもいるかも。

小休止

何回、controller、directive、serviceって繰り返すんだよクソっ! って気になってきたかと思いますので本文とは特に関係ない挿絵を御覧ください(あと一回繰り返します)。

艦隊くりっかーでの全体の構成

敵のパラメーターと説明、施設のパラメーターと設定なんかはjsonに書いてある。

controller
データの保存とかロードをやってるだけ。
上記のjsonのデータを呼び出すserviceとかクッキーの入出力serviceとかの実行。でその値をscopeに入れるだけ。

directive
ディレクティブとして作ったのは、体力とか経験値のバーと船の部分、施設の部分。
今見直すと、施設の部分はディレクティブとして分ける必要性が薄いし、船の部分も含めてビジネスロジックまで書いててまずい。
体力とか経験値のバーは与えられた数値で要素の長さを変えてバーっぽく見せてるだけ。
船の数値のやつは、$rootScopeのイベントを見張って攻撃されたりクリックしたら渡された数値を表示するだけ。
この辺後述するけど、$watchをどこでも使うよりイベントのがいい感じの時も多い。

service
ディレクティブと同じく分離がうまく言ってない箇所があって構成が説明しづらい。
本来だとユーザー関連と敵関連と施設関連を分離してそれぞれがクッキーへの入出力サービスに依存してるような感じにすべきだったと思う。で、クッキーへの入出力モジュールがロード時にデータをキャッシュしたり、一定時間おきにデータの変更を確認して保存すれば良かった。
ちなみにAngularJSのクッキーモジュールはブラウザ閉じるまで以外の有効期限を設定できないので、
クッキーへの書き出しとか読み込みとかは自前で作った。

だいたい終わった

というあたりで大まかな構造はだいたい語れたので満足した。
つうかそんな複雑な構造してない。
あとその他開発中に思ったことを列挙。

・クッキーすげえ壊れる

クッキーは壊れる。ポッケに入れて膝に矢を受けたら真っ二つ。
結構長いデータをJSONにしてbase64にしただけっていう横着なやり方だからか結構壊れた報告を受けた。
受けたから、テキストで保存できるようにしたのだけど、根本的に直す方法は良く分からない。
確認した限りでは全部真っ白になるわけではなくて、後半データが切れるみたいな壊れ方が大半だったので、容量に余裕があるなら一つ前の状態も保存して、壊れてたら前のデータ参照みたいなやり方ならまだ被害は少ないかもしれない。

・イベント利用($emit,$on)のすすめ

普通AngularJS的には、各ディレクティブ内で見た目とかが変わるときは依存性として渡してあるモデルを$watchしてその状況に応じて変更するみたいなやりかたが常道っぽい。
ただこれだと、っていうかゲームのようなDOMの見た目変更が複数条件で決定されかつ複雑なときには、ディレクティブに片っ端からサービス渡したりすることになって現実的じゃない。
しかも今回は小規模なので問題になってないけど、私がやっちゃったみたいにディレクティブ内でサービスのデータ変えたりするともう目も当てられない。
どこで何が変わっているのか、どこに何が依存しているのかの把握がむずかしくなる。
でっかいオブジェクトなんかをまるごと$watchするのがあちこちにあるとこれもまた怖い。
そのへん、イベントは投げたところと受け取るところがはっきりするので、ある程度統制を取りやすい気がする。

自データサービスは敵に攻撃されたことをイベントで受け取って、引数の攻撃力に応じて自分の艦数を減らして、
もし0になったら敗北イベントを発生させる。
敵サービス側は攻撃状態にある間は一定時間おきに攻撃イベントを発生させて、プレイヤー側の攻撃、またはクリックのイベントを受けて自分の艦数を減らして、もし0になったら敗北イベントを発生させて攻撃状態を解除、あるいはプレイヤーの敗北イベントが発生しても攻撃状態を解除する。

というような感じ。

・ぐるぐる回る

そもそも上記のように$watchを多用してオブジェクトとかを監視しまくる構成になると、把握が難しいならまだしも最悪の場合はループするし、そうでなくても無駄な呼び出しが増える傾向にある。
これは大変に厄い。
最初のうち何も考えずに$watchを活用しまくって適当に書いていると、なんか知らないけどこのプロパティ変えただけで、関係ない処理が山ほど流れるなんてことになったことがあった。
filterなんかとくにそうなりやすい。console.logで出力してみると何故かちょこっと動かすたびにフィルタ動いてるみたいになる。
オブジェクトのプロパティも監視対象にできるのであんまり大きなオブジェクト全体を見張らないで、必要な部分だけにすべきなのだと思う。
(わざわざ第三引数にtrue渡さないとプロパティを含めたオブジェクト全体は見張らないし、もともとそういう想定なのだと思う)

まとめ

個人的な印象で言うと、アクションゲームみたいな動きの多いものでなければ、AngularJSでゲームを作るのは結構簡単で楽しいのではないかと思いました。$watch周りとか$scope周りはややこしいこともあって、混乱することもありますが、その辺をある程度整理したり、こういうことはすべきではないということが分かれば、本当に楽に書けて良いと思います。
時間と場が残っていればこのAdventCalendar内で、scopeあたりについても書ければいいなと思います。

宣伝

艦くりは今年年末から来年初くらいまでの間にアップデートを予定しています。
ちなみに私が作ったゲームやおもちゃその他や、それらの制作動画のお知らせはこのブログ(RSS)か@sashimizakanaに掲載しています。
君もばかおもちゃの最新情報をチェックだ!

2013年7月22日月曜日

AngularJSでselect2のTagを利用する(AngularUI未使用)

AngularJSでSelect2を利用するときはAngularUIにSelect2がディレクティブ化されたものがあるので、それを使えばいいんだけど、タグ機能がIE8では動かない。もうそりゃぴくりとも動かない。
そもそもAngularUI-Select2の単体テストすらIE8では動作しない。かたやSelect2単体はIE8でも普通動く。
じゃあもうSelect2を自分でディレクティブにしてしまえと思った。

ディレクティブを作るのは再利用できるようにしたりとか、ちゃんとやるとコード量も増えてなかなか難しいけど、使用方法なんかをある程度決めうちしてしまえば割と簡単にできる。ただディレクティブのスコープ周りとかapply、parse周辺は本当にこれで良いのかやや不安なので、より良い方法があるなら是非ご教授いただきたい。

コードは以下の通り。

angular.module('tag',[]).directive("tag", function($parse)
{
    return {
        restrict : 'A'
        ,require:['ngModel']
        ,link : function(scope, element, attr)
        {
            //選択候補
            var tags = [];
            var result = $parse(attr.ngModel);
            //Select2の作成対象
            var input = element.children('input');

            //Select2を作成する。idは比較するときの対象なので
            //textにしておかないと、選択候補と同じタグを新たに作成できてしまう
            input.select2(
            {
                  tags:function(){return tags}
                  ,id:function(i){return i.text}
            });

            input.on("change",function()
            {
                //選択結果を渡す
                result.assign(scope,input.select2('data'));
                //変更結果を適用する
                if(scope.$$phase)
                {
                    scope.$eval(attr.ngModel);
                }
                else
                {
                    scope.$apply(attr.ngModel);
                }   
            });

            //選択候補の監視
            scope.$watch(attr.tag,function(newValue)
            {
                tags = newValue;
            },true);

            //選択状態の監視
            scope.$watch(attr.ngModel,function(newValue)
            {
                input.select2("data",newValue);
            },true);

        }
        ,template : '<input type="text" />'
    };
});

これを<div tag="tags" ng-model="selected">みたいな感じで使う。
本当は<input tag=...>みたいに出来た方がスマートな感じで、AngularUIではそうしているのだけど、Select2が追加するタグのせいでng-showなんかを使うためには一階層上に付けなくてはならなかったりして、結局不便になっているのでこうした。

2013年5月11日土曜日

たのしいAngularJS1 用語について

AngularJSのことがやっと何となく分かってきて楽しくなってきたので書く。
とりあえず良くあるサンプル的な書き方から一歩進んで、複数画面を持つアプリケーションを作成できるように、ルートの設定のこととか、サービスの作成の仕方のさわりまでを書くつもり。今回はまず前置きと、AngularJSで出てくる用語についてまとめておく。
ちなみにAngularJSってなに? みたいなレベルの話は書かない。
ざっくり試してみてコントローラーをどう書くのかくらいは分かる人対象。



基本的にサンプルとして良くあるのは、<html ng-app>みたいに書いてあって、コントローラーをwindow直下に書くようなやつだけど、複数のviewとコントローラを持ったようなものを書いていくには、angular.module()でモジュールを作成して、それにサービスとか作ったカスタムモジュールを登録していくような方法になる。こんなやつ。
angular.module('appName', [] , function($routeProvider) {
    $routeProvider
        .when("/", { templateUrl: "/hoge.html", controller: "HogeCtrl" })
        .when("/fuga", { templateUrl: "/fuga.html", controller: "FugaCtrl" })
        .otherwise({redirectTo:"/"});
    }]);
で、StackOverflowとかで調べるとこういうのが普通に書いてあるんだけど、第一印象が「うわぁややこしそう!」って思う。なんかライブラリ読み込んで、htmlにng-appって書いて、コントローラー名の関数作成するだけで良いのだと思ったらなんじゃこれ! とかなる。
でも、実際は別に難しいことはなくて、というかこのあたりのことをちゃんと分かっておかないと、何画面かあるアプリケーションなんて書くとすぐコードがちらかってくる。

で、これから学習を始める前に用語について、適当にまとめておく。
とりあえず良くつかうものだけ。この辺がざっくり分かるだけでも、ずいぶん効率が上がる。

Controller(コントローラー): 
ng-controller=""っていう書き方をしているあれ。基本的にDOM操作とかは入れちゃだめ。あとフィルタとかソートも元々あるやつ使うので書くなとか。大きいリストの全体をソート+フィルタしてから、20件ずつスクロール毎表示みたいなのをどうやって書けば良いのかよく分からないのでついコントローラーにぶち込んでしまったのは私です。たぶんなんかやり方はあるんだと思う。

Service(サービス):
$httpとか$scopeとかのあれ。なんで普通のwindowやlocationと似た$windowとか$locationとかあるのかってと、テストを行うときに依存性があると難しいから。依存性? なに? ということはここでは長すぎるので書かかない。で、ここからが重要で試験に出る部分、サービスはシングルトンで複数のコントローラーにまたがったときも同一ということ。複数の画面を持つアプリを作成するときに、一覧画面から個別編集画面に移動して、また一覧に戻るようなとき、当然データは同じものを参照した方が手っ取り早いわけだけど、その二つのコントローラー間でデータを渡すときにサービスを利用する方法がある($routeScopeを利用する方法もある)。
もちろんそのためには自分でサービスを書く必要がある。
サービス自体は、外部に依存性をもつもの(データの呼び出しとか)をまとめるためのものという理解でたぶん良いんだと思う。

Directive(ディレクティブ) :
ng-repeatとかng-showみたいなやつのこと。 DOM要素の操作はこれでやれ、ということで、もちろん自分でも作れる。

Module(モジュール)
Angularのモジュール。ドキュメントによると、サービスとかフィルターとかディレクティブとかコンフィグのあつまり、ということらしい。コントローラーもこの中に含まれる。コントローラーのみしか書かないサンプルによくある書き方をしていると意識しづらいけど、アプリケーションそのものの設定とかを行うことが出来る。ちなみにWebサービス全体のことだけじゃなくて、たとえばng-infinite-scrollみたいなスクロールすると増えてくリストみたいなものもディレクティブを持ったモジュールとして作成されていて、それをアプリケーション本体のモジュールから呼び出すイメージ。

というわけで今日はここまで。
以下、AngularJSを書く上で役に立ったサイト。

参考:
Androiderに贈るAngularJS概説
Androiderじゃなくても必読。現時点で日本語で見た中で一番良いまとめだと思う。
というかこれを全部読めば私の以降の記事を読む必要は無い。まじで無い。

AngularJSのDIの仕組み、minify対策は覚えておこう!
なんでng-controllerみたいなのでグローバル関数指定するのでは駄目か非常にわかりやすい解説。

pochi / angular-doc-ja
一部分だけだけど公式の日本語訳。英語苦手なので大変ありがたい。

StackOverflowの記事沢山。
ここ見てればどうにかなる感。でもIEでの微妙な挙動関連は同じ報告はあっても解決はしない感。