angular雑感 2014-04-30
シングルページのWebアプリケーションにはとても相性が良いです。サーバー側でAPIだけ定義しておけば、$resource
で簡単にCRUDするモデルを扱うことができ、$routeProvider
を使えばルーティングができます。
これまでjQueryでごりごり書いてきた人も多いと思います。ただ残念ながらこれまでのjQuery資産の9割は使えないと思います。うまくプラグイン化されていれば使えなくはないです。どうしても使いたい場合は、$apply
を呼び出したりして、angularの世界に合わせる必要があります。
学習コストは最初は低いですが、ある程度複雑なことをやろうとすると、一気に高くなります。フレームワークというのは往々にしてそういうものですね。特にDI、スコープ、ディレクティブが肝であり鬼門だと思います。
利点の1つとして、JSファイルの読み込み順を気にしなくて良いというのがあります。DIの依存性解決のおかげです。gruntでディレクトリ以下のJSファイルをそのままconcatしても、簡単に動作させることができます。
IE8以下必須の場合には使わない方が良さそうです。1.3ではサポートしないそうです。https://docs.angularjs.org/guide/ie
現時点、使えそうで使えない標準モジュールも結構あります。touchとかcookieとか。。。(ngTouchは1.2で使えるようになってるかもしれないです。自分が試した1.1では微妙な動作だった記憶があります)cookieはexpires指定ができません。
プロバイダーに関して (factory, service, provider)
constant, value, service, factory, providerの使い分け
- constant: プロバイダーに注入したい時や、configフェーズで使いたい場合。絶対不変な値。decoratorできない
- value: factory, serviceに注入したい時や、runフェーズで使いたい場合。値が変わる可能性がある場合。
- service: CoffeeScriptのclassで定義したようなものを使いたい場合
- factory: runフェーズで注入したい場合
- provider: インスタンス化する前にconfigフェーズで設定する必要がある場合
value
もfactory
もservice
も、結局はprovider
の機能制限版のようなものなので、よくわからない場合はfactory
だけ使っていれば済むと思います。
config, run
angularの実行フェーズは2段階あります。config と run です。それぞれ注入できるサービスが違います。この表が分かりやすいです。
provider
var app = angular.module('sample', []);
app.provider('api', function(){
return {
$get: function(){
return 'api';
},
connect: function(){
// 処理...
}
};
});
app.provider('person', function(apiProvider){
apiProvider.connect();
return {
init: function(){
// 設定とか...
},
$get: function(){
return 'person';
}
};
});
// config: ここでは主にproviderの設定を行う
// 例えば $routeProvider
app.config(function(personProvider){
personProvider.init();
console.log(personProvider);
});
// run: $get()した結果を取得
// 例えば $route
app.run(function(person){
console.log(person);
});
app.config()
にはapp.provider()
かapp.constant()
で登録したものしか注入できません。プロバイダーを config で注入する場合にはProvider
を付ける必要があります。
app.run()
にはapp.constant()
や、プロバイダーが$get()
した戻り値であるインスタンスを注入できます。
factory
var sample = angular.module('sample', []);
// core factoryを定義
sample.factory('person', function(){
return {
name: 'akkunchoi'
};
});
// 依存性注入により、上で定義したオブジェクトが入る
sample.run(function(person){
console.log(person.name); // => 'akkunchoi'
});
service
var sample = angular.module('sample', []);
var MyUserService = (function(){
var number = 0;
MyUserService = function(){
this.number = ++number;
};
MyUserService.prototype.getNumber = function(){
return this.number;
};
return MyUserService;
})();
sample.service('person', MyUserService);
// 依存性注入により、上で定義したオブジェクトが入る
sample.run(function(person){
console.log(person.getNumber()); // => 1
});
sample.run(function(person){
console.log(person.getNumber()); // => 1
});
MyUserServiceクラスをnewした結果が、注入するオブジェクトになります。そのため、functionやプリミティブな値を注入することはできません。
ディレクティブ
簡単な例
<div sample-code>Content</div>
app.directive('sampleCode', function(){
return function(scope, element, attrs){
console.log(scope, element, attrs);
};
});
- scopeはng.$rootScope.Scope
- elementはjQuery(またはjQuery Lite)
- attrsは ng.$compile.directive.Attributes
属性名はコロン、アンダーバー、ハイフンで単語を区切りますが、 directive定義する時はキャメルケースになることに注意!
ディレクティブのオプション
- restrict: デフォルトは'A'。'AEC'を組み合わせて指定する。restrict指定せず要素名をディレクティブとして書いても無反応なのが気づきにくい。
-
scope: {}
を指定すると isolated scope を作成し、ディレクティブ内での名称 => 属性名のマッピングを行う。
'=' は 属性名同じで値を解釈する
'&' は 関数として実行可能
'@' は 常に文字列として解釈-
scope: {}
のスコープ(isolate)は、どこが隔離されているかというと、templateの内部とlink()で受け取るscope。DOM要素の子は隔離されていない(この辺りの仕組みはよく理解できいない) http://jsfiddle.net/LqzxP/2/ -
scope: true
のスコープはプロトタイプ継承を行う。プリミティブな値や親で未定義の値を変更しても、親には反映されない
-
- template: HTMLを記述する
- templateUrl: URLを指定する
- link: function link(scope, element, attrs) { ... }
- transclude: trueにするとng-transcludeが使えるようになる。ディレクティブのDOM内部のデータが、テンプレート内で"ng-transclude"した場所に埋め込まれるようになる。
- controller: 実行したいコントローラーを指定する。
- controllerAs: コントローラの省略名
- require: linkの第四引数に受け取るコントローラを指定。ディレクティブ名(ng-contoller用に登録したコントローラは指定できない??)。先頭に
^
をつけると親からも探索する。?
をつけると存在しない場合でもエラーにならない。 - replace: テンプレートで内容を置き換える(非推奨になるらしい??)
- priority: 大きいほど先に実行
- terminal: trueならそこで終わり
その他
- 同じ名前のディレクティブでも何度も定義できる。その場合上書きにはならず、優先度順に実行していく。
- 未定義のディレクティブをテンプレートに記述してもエラーにはならない。
- decoratorする場合は "〜Directive"
-
element.on('$destroy', ...)
またはscope.$on('$destroy', ...)
でクリーンアップするコードを書く必要がある(??)
当然ですが、ng-controllerもdirectiveのひとつです。内部でcontrollerオプションを指定しているだけです。
スコープ
スコープ階層はJavaScriptのプロトタイプ継承そのものです。
別の記事にしました。
$on が受け取る特殊なイベント
- $viewContentLoaded: ng-viewによって中身の読み込みが完了した時に呼ばれます。jQueryMobileの
pageinit
的なもの。
$scope.$on('$viewContentLoaded', function() {
//call it here
});
- $includeContentLoaded: ng-includeによって中身の読み込みが完了した時。
その他のイベント
- $locationChangeStart
- $locationChangeSuccess
- $routeUpdate
- $routeChangeStart
- $routeChangeSuccess
- $routeChangeError
- $destroy
$emit vs $broadcast
- $emit: 上位のスコープにイベントの発火を伝えます
- $broadcast: 下位のスコープにイベントの発火を伝えます
$watch
値が変更した時に何か実行したい時使います。オブジェクトや配列の中身が変わった時を知りたい場合は $watchCollection というのもあります。
ビュー周り
ng-classである条件の時だけクラスをつけたい
flag
にbooleanが入るようして、selected
というCSSクラスを付けたい場合
<span ng-class="{selected: flag]">項目</span>
こういう書き方もできる
<span ng-class="{true: 'js-selected'}[flag]">項目</span>
デフォルト値
{{gallery.date || 'Various'}}
ng-attr-*
IE11でtextareaにplaceholderを使うと「Error: 引数が無効です」というエラーが出ました。起きない場合もあり、動的にtextareaを生成をしているのが条件かもしれないですが、よくわかってません。
https://github.com/angular/angular.js/issues/5025
http://stackoverflow.com/questions/20647572/angularjs-v1-2-5-script-error-with-textarea-and-placeholder-attribute-using-ie11/20649762#20649762
調べた所 ng-attr-placeholder
にすると解決できました。
はて、ng-attr-ってなんだろ?APIリファレンスにはありません。ディレクティブの ngAttr attribute bindingsに書いてありました。
ブラウザが属性値を参照して何か特別な動作をする場合にはng-attr-
というプレフィックスを付けるとangularが解釈した後の値に変えてくれます。例えばSVGの属性を指定する時などにもこれが必要になるようです。
select の ng-options
select要素の生成にng-optionsがどうも思ったように動作しない。具体的に言うとselectのvalue属性が指定できない、変な空白要素が出てくる、など。
改めてAPIリファレンスを見るとこのようなNoteが書かれていました。
ngModelは値ではなく参照で比較します。これはオブジェクトの配列をバインドする際に重要なポイントです。このjsfiddleを参照下さい
つまり、このようにして使います。
$scope.genres = [
{label: '日本', group: 'アジア'},
{label: '中国', group: 'アジア'},
{label: 'アメリカ', group: '欧州'}
];
// genreに label の名称を入れるのは間違い。参照を設定する。
$scope.genre = $scope.genres[0];
<select ng-options="genre.label group by genre.group for genre in genres" ng-model="genre">
その他
angular.module
登録したモジュールは ng-app
で指定すると実行されます。
<html ng-app="sample">
angular.module
の第二引数では依存モジュールを指定できます。
循環した場合でも起点(ng-app)があるのでエラーは出ないです。
依存モジュールを指定した場合、注入できるのは先に読んだモジュールで定義されたproviderだけのようです。
http://jsfiddle.net/akkunchoi/2DLKc/
実行順
モジュールの実行順は、基本は依存モジュール(第二引数)の指定順ですが、常に必要とされているモジュールから実行されます。以下の例ではまず plugin1
が実行され、plugin2
が必要としている plugin3
が実行され、plugin2
が実行されます。
angular.module('sample', ['plugin1', 'plugin2', 'plugin3']);
angular.module('plugin1', []);
angular.module('plugin2', ['plugin3']);
angular.module('plugin3', []);
// plugin1
// plugin3
// plugin2
// sample
setTimeout, window, location, document
setTimeout, window, location, documentは使わないようにします。特にsetTimeoutは 内部の関数でスコープ内の値を変更する場合は必ず$timeoutにする必要があります。$timeout等にすることで、angularは内部でスコープのどの値が変更されたかを確認して、変更されていればビューに通知するなどの処理を行います(いわゆるdigestループ)。ここがangularを使う上での重要なポイントです。
- setTimeout => $timeout
- window => $window
- location => $location
- document => $document
難読化する前に ng-annotate (ngmin)
Angularのコードを難読化(uglify,minify)すると動かないことがあります。一番シンプルなインジェクションの方法では、引数名でどのサービスを注入するか判断します。しかし難読化すると引数名が変わってしまうのでインジェクションできません。
// 難読化すると動かない例
angular.module('sample')
.controller('SampleCtrl', function($scope){
$scope.foo = 'bar';
});
// 難読化するとこのように変数名が短くなる。よく"a"が見つかりません的なエラーになるのはこのため。
angular.module('sample')
.controller('SampleCtrl', function(a){
a.foo = 'bar';
});
$injectや配列記法にすると面倒です。そういう時に ng-annotate を使います。.controller()
等はもちろん、functionの前に /* @ngInject */
をつけると任意の関数を配列記法にしてくれます。
ngminというのもありますが、以下の場合に変換してくれません。
- angular.moduleして後で、別の変数に変更した場合
-
$routeProvider
のresolve
-
$provide.decorator
の$delegate
注入時
Provider一覧
$injectorを直接調べました。(1.2)
$anchorScrollProvider: Constructor
$animateProvider: Constructor
$browserProvider: Constructor
$cacheFactoryProvider: Constructor
$compileProvider: Constructor
$controllerProvider: Constructor
$documentProvider: Constructor
$exceptionHandlerProvider: Constructor
$filterProvider: Constructor
$httpBackendProvider: Constructor
$httpProvider: Constructor
$injector: Object
$interpolateProvider: Constructor
$intervalProvider: Constructor
$localeProvider: Constructor
$locationProvider: Constructor
$logProvider: Constructor
$parseProvider: Constructor
$provide: Object
$qProvider: Constructor
$rootElementProvider: Object
$rootScopeProvider: Constructor
$sceDelegateProvider: Constructor
$sceProvider: Constructor
$snifferProvider: Constructor
$templateCacheProvider: Constructor
$timeoutProvider: Constructor
$windowProvider: Constructor
currencyFilterProvider: Object
dateFilterProvider: Object
filterFilterProvider: Object
formDirectiveProvider: Object
inputDirectiveProvider: Object
jsonFilterProvider: Object
limitToFilterProvider: Object
lowercaseFilterProvider: Object
ngBindDirectiveProvider: Object
ngBindHtmlDirectiveProvider: Object
ngBindTemplateDirectiveProvider: Object
ngBlurDirectiveProvider: Object
ngChangeDirectiveProvider: Object
ngCheckedDirectiveProvider: Object
ngClassDirectiveProvider: Object
ngClassEvenDirectiveProvider: Object
ngClassOddDirectiveProvider: Object
ngClickDirectiveProvider: Object
ngCloakDirectiveProvider: Object
ngControllerDirectiveProvider: Object
ngCopyDirectiveProvider: Object
ngCutDirectiveProvider: Object
ngDblclickDirectiveProvider: Object
ngDisabledDirectiveProvider: Object
ngFocusDirectiveProvider: Object
ngFormDirectiveProvider: Object
ngHideDirectiveProvider: Object
ngHrefDirectiveProvider: Object
ngIfDirectiveProvider: Object
ngIncludeDirectiveProvider: Object
ngInitDirectiveProvider: Object
ngKeydownDirectiveProvider: Object
ngKeypressDirectiveProvider: Object
ngKeyupDirectiveProvider: Object
ngListDirectiveProvider: Object
ngModelDirectiveProvider: Object
ngMousedownDirectiveProvider: Object
ngMouseenterDirectiveProvider: Object
ngMouseleaveDirectiveProvider: Object
ngMousemoveDirectiveProvider: Object
ngMouseoutDirectiveProvider: Object
ngMouseoverDirectiveProvider: Object
ngMouseupDirectiveProvider: Object
ngNonBindableDirectiveProvider: Object
ngOpenDirectiveProvider: Object
ngOptionsDirectiveProvider: Object
ngPasteDirectiveProvider: Object
ngPluralizeDirectiveProvider: Object
ngReadonlyDirectiveProvider: Object
ngRepeatDirectiveProvider: Object
ngRequiredDirectiveProvider: Object
ngSelectedDirectiveProvider: Object
ngShowDirectiveProvider: Object
ngSrcDirectiveProvider: Object
ngSrcsetDirectiveProvider: Object
ngStyleDirectiveProvider: Object
ngSubmitDirectiveProvider: Object
ngSwitchDefaultDirectiveProvider: Object
ngSwitchDirectiveProvider: Object
ngSwitchWhenDirectiveProvider: Object
ngTranscludeDirectiveProvider: Object
ngValueDirectiveProvider: Object
numberFilterProvider: Object
optionDirectiveProvider: Object
orderByFilterProvider: Object
requiredDirectiveProvider: Object
scriptDirectiveProvider: Object
selectDirectiveProvider: Object
styleDirectiveProvider: Object
textareaDirectiveProvider: Object
uppercaseFilterProvider: Object