angular-ui-router
angular-ui-router は、AngularJS のルーティング、およびビュー分割プラグインです。
セットアップ
CDNJSを利用して、必要なファイル angular.js
、angular-ui-router.js
を読み込みます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<title>myApp</title>
</head>
<body>
</body>
</html>
JavaScript コンソール上で、ui-router が読み込まれていることを確認できます。
angular.module('ui.router')// Object
module名が ui.router
であることに注意してください。
myAppの初期化
myApp
├── index.html
├── index.js
└── top
├── index.html
└── index.js
アプリケーションを初期化します。
DEMO
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<title>myApp</title>
<script src="index.js"></script>
<script src="top/index.js"></script>
</head>
<body ng-app="myApp">
<div ui-view></div>
</body>
</html>
// Dependencies
angular.module('myApp',[
'ui.router',
])
// Routes
.config(function($stateProvider){
$stateProvider.state('top',{
url: '/',
templateUrl: 'top/index.html',
controller: 'topCtrl as top',
})
})
<a ui-sref="top">{{top.title}}</a>
// Dependencies
angular.module('myApp')
// Publish
.controller('topCtrl',function(){
this.title= 'hello world'
})
index.html
を開く1と、myApp を初期化して top が読み込まれます。
$stateProvider.state(name, stateConfig) で、ステートと呼ばれる画面単位を追加します。
ui-sref ディレクティブは、設定したステートのURI
を自動発行してhref
属性を追加します。この例では、自分自身のステートを指しているため、クリックしても画面が変わらないのが正常です。
ui-view ディレクティブは、直接
<ui-view></ui-view>
と書くこともできますが、inline要素で表示されることに注意してください。これはdisplay:block
スタイルで修正できます。
9月25日追記: coffee-scriptの場合、
.controller
の第二引数がコンストラクタ関数であること、つまりthis
以外をreturnしないように注意して下さい。# 以下のコンパイル結果は`return this.title= 'foo';` # `this`ではなく'foo'を返す app= angular.module('myApp',[]) app.controller 'myCtrl',-> this.title= 'foo'
リロードすると白紙になる
DEMO の hello world
リンクをクリックして、画面をリロードすると分かりますが、白紙になります。
ui-router は起動時に、 URI の #
以降をurlとして解釈します。つまり
-
index.html
-> url:''
-> state:top
- クリック -> state:
top
->index.html#/
- リロード ->
index.html#/
-> url:'/'
-> state:undefined
この問題2は、下記のように修正します。
// Dependencies
angular.module('myApp',[
'ui.router'
])
// Routes
.config(function($stateProvider){
$stateProvider.state('top',{
url: '/',
template: 'top/index.html',
controller: 'topCtrl as top',
})
})
// Default
.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/')
})
これで、index.html
、index.html#/
、両方で state が top と解釈されます。
子ステート
myApp
├── index.html
├── index.js
└── top
├── index.html
├── index.js
├── search.html
└── search.js
ステート名$stateProvider.state(name)
をドットで区切ると、ステートに所属するステートを作成します。
DEMO
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Getting Started</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<script src="index.js"></script>
<script src="top/index.js"></script>
<script src="top/search.js"></script>
</head>
<body ng-app="myApp">
<div ui-view></div>
</body>
</html>
// Dependencies
angular.module('myApp',[
'ui.router',
])
// Routes
.config(function($stateProvider){
$stateProvider.state('top',{
url: '/',
templateUrl: 'top/index.html',
controller: 'topCtrl as top',
})
})
.config(function($stateProvider){
$stateProvider.state('top.search',{
url: 'search',
templateUrl: 'top/search.html',
controller: 'topSearchCtrl as topSearch',
})
})
// Default
.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/')
})
<a ui-sref="top">{{top.title}}</a>
<a ui-sref=".search">search</a>
<div ui-view></div>
// Dependencies
angular.module('myApp')
// Publish
.controller('topCtrl',function(){
this.title= 'hello world'
})
{{topSearch.title}}
// Dependencies
angular.module('myApp')
// Publish
.controller('topSearchCtrl',function(){
this.title= 'would like?'
})
top/index.html の ui-sref=".search"
ディレクティブ をクリックすることで、自身がもつ ui-view ディレクティブに top.search
の子ビューを読み込みます。
子ステートの url は、親ステートの url を継承することに注意してください:例 state:
top.seach
->'/'
+'search'
-> url:'/search'
ビューの分割
myApp
├── index.html
├── index.js
├── root
│ ├── index.js
│ ├── header.html
│ ├── header.js
│ ├── footer.html
│ └── footer.js
└── top
├── index.html
├── index.js
├── search.html
└── search.js
ヘッダ、コンテナ、フッタといったお決まりの要素を、それぞれ別のビュー・コントローラーで管理できます。
DEMO
<!DOCTYPE html>
<html lang="ja" ng-app="myApp" ng-controller="rootCtrl as root">
<head>
<meta charset="UTF-8">
<title ng-bind="root.title"></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<script src="index.js"></script>
<script src="root/index.js"></script>
<script src="root/header.js"></script>
<script src="root/footer.js"></script>
<script src="top/index.js"></script>
<script src="top/search.js"></script>
</head>
<body>
<header ui-view="header"></header>
<div ui-view="container"></div>
<footer ui-view="footer"></footer>
</body>
</html>
// Dependencies
angular.module('myApp',[
'ui.router',
])
// Routes
.config(function($stateProvider){
$stateProvider.state('root',{
views: {
header: {
templateUrl: 'root/header.html',
controller: 'headerCtrl as header',
},
footer: {
templateUrl: 'root/footer.html',
controller: 'footerCtrl as footer',
},
},
})
})
.config(function($stateProvider){
$stateProvider.state('root.top',{
url: '/',
views: {
'container@': {
templateUrl: 'top/index.html',
controller: 'topCtrl as top',
}
}
})
})
.config(function($stateProvider){
$stateProvider.state('root.top.search',{
url: 'search',
templateUrl: 'top/search.html',
controller: 'topSearchCtrl as topSearch',
})
})
// Default
.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/')
})
- ステートに
views
プロパティを設定した場合、controller*
,template*
プロパティは無視されます。 -
views
プロパティは key-value ペアで、 keyにはui-view
ディレクティブの属性値を、value
には、ステートに設定していたcontroller*
,template*
を設定します。 - 子ステートから親ステートのビューを変更する場合、
views
の key の末尾に@
を足し、そのビューを持つステート名を指定します(対象がルート ui-view の場合@
の後に何も書きません)。 - 前述の理由により、root ステートにコントローラーを設定できません。
ng-controller
ディレクティブを利用して、ui-view
よりも親の要素html
に設定しました。これにより$rootScope
を使わずにステート共通のプロパティやメソッドを、このファイルに隔離できます3。
参考:(英語)angularjs ui-router - how to build master state which is global across app
外部リソースを待つ
myApp
├── index.html
└── index.js
データベースや外部からデータを受け取るまで、ビューを表示させたくない場合は、ステートに resolve
プロパティを設定します。
以下の例では、$httpサービスを使用して、iTunesAPI から jsonp を待ち、その後でビューを表示します。
DEMO
<!DOCTYPE html>
<html lang="ja" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<script src="index.js"></script>
</head>
<body>
<div ui-view></div>
</body>
</html>
// Dependencies
angular.module('myApp',[
'ui.router',
])
// Routes
.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/')
})
.config(function($stateProvider){
$stateProvider.state('top',{
url: '/',
resolve: {
itunes: function($http){
var query= '漁港 鮪'
var api= 'https://itunes.apple.com/search'
var params= [
'term='+query,
'country=jp',
'media=music',
'entity=album',
'lang=ja_jp',
'limit=10',
'callback=JSON_CALLBACK'
]
var uri= api+'?'+params.join('&')
// available promise at resolve
return $http.jsonp(uri)
},
},
template: [
'<section ng-repeat="result in top.results">',
'<a ng-href="{{result.collectionViewUrl}}">',
'<img ng-src="{{result.artworkUrl100}}" alt="{{result.collectionName}}" />',
'</a>',
'<pre>{{result|json}}</pre>',
'</section>',
].join(''),
controller: 'topCtrl as top',
})
})
// Publish
.controller('topCtrl',function(itunes){
this.results= itunes.data.results
})
- resolve は key-value ペアで、key にDI名を、value にプロミスを返す関数4を定義します。
- controller に注入することで、注入したコントローラが起動する前に value の関数を実行します。
- 実行した関数がプロミスを返した場合、そのプロミスが解決するまで
template*
,controller*
は実行されません。
プロミスの詳しい書き方については、azu 氏のJavaScript Promiseの本や、たいが氏の $q サービスで覚える Promiseを参照ください。
url
のパース
myApp
├── index.html
└── index.js
#/search/澤野弘之
のようなURIへアクセスした時、search
ステートに澤野弘之
を変数として渡したい場合があります。その場合、ステートをurl:'search/:query'
と書くと、resolve
やcontroller*
、template*
に$stateParams
を注入することで、$stateParams.query
から澤野弘之
を取得することができます。
DEMO
<!DOCTYPE html>
<html lang="ja" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>iTunes Finder</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<script src="index.js"></script>
</head>
<body>
<div ui-view></div>
</body>
</html>
// Dependencies
angular.module('myApp',[
'ui.router',
])
// Routes
.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/search/澤野弘之')
})
.config(function($stateProvider){
$stateProvider.state('top',{
url: '/',
template: [
'<form ng-submit="top.submit(query)">',
'<input ng-model="query" autofocus required/>',
'<button>search</button>',
'</form>',
'<div ui-view/>',
].join(''),
controller: 'topCtrl as top',
})
})
.config(function($stateProvider){
$stateProvider.state('top.search',{
url: 'search/:query',
resolve: {
itunes: function($stateParams,$http){
var query= $stateParams.query
var api= 'https://itunes.apple.com/search'
var params= [
'term='+query,
'country=jp',
'media=music',
'entity=album',
'lang=ja_jp',
'limit=10',
'callback=JSON_CALLBACK'
]
var uri= api+'?'+params.join('&')
console.log('Request to',uri)
return $http.jsonp(uri)
}
},
template: [
'<section ng-repeat="result in search.results">',
'<a ng-href="{{result.collectionViewUrl}}">',
'<img ng-src="{{result.artworkUrl100}}" alt="{{result.collectionName}}" />',
'</a>',
'<pre>{{result|json}}</pre>',
'</section>',
].join(''),
controller: 'searchCtrl as search',
})
})
// Controllers
.controller('topCtrl',function($state){
this.submit= function(query){
$state.go('top.search',{query:query})
}
})
.controller('searchCtrl',function($scope,$stateParams,itunes){
$scope.$parent.query= $stateParams.query// remember :query via URI
this.results= itunes.data.results
})
外部リソースを待つのサンプルコードに、検索機能を加えています。
$state.go は、ui-sref
ディレクティブをコントローラーから使用するための API です。
url
はUrlMatcher
を介して$stateParamsに変換されます。 参考:(英語)UI Router: UrlMatcher
例外処理
myApp
├── index.html
└── index.js
$stateProvider
、はアクセス時のURIを、.state
で追加した順番にurl
を評価し、はじめに一致したステートを実行します。一致しなかったとき、表示するビューが無いため白紙になります。
url:'/:path'
を設定したステートを一番最後に追加する5ことで、一致しなかった全ての url を捕まえることができます。
DEMO
<!DOCTYPE html>
<html lang="ja" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<script src="index.js"></script>
</head>
<body>
<div ui-view></div>
</body>
</html>
// Dependencies
angular.module('myApp',[
'ui.router',
])
// Routes
.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/')
})
.config(function($stateProvider){
$stateProvider.state('top',{
url: '/',
template: [
'<h1>top</h1>',
'<a href="#/foo">foo</a>',
'<a href="#/bar">bar</a>',
'<a href="#/baz">baz</a>',
'<a ui-sref="invalid-state">error</a>',
].join('')
})
$stateProvider.state('error',{
url: '/:path',
template: '<strong>404</strong><a ui-sref="top">back</a>'
})
})
url:'/:path'
はui-sref
ディレクティブに適用されません。
これは、$rootScope.$on('$stateNotFound',callback)
で補足します。
同様に、ステートの resolve プロパティ が解決されなかった場合($http.get
で 404が返る、など)は、$rootScope.$on('$stateChangeError',callback)
で補足します。
DEMO
callback の第一引数 event の event.preventDefault() を実行することで、ui-router の処理(エラー表示など)を中断できます
参考にした記事
- (英語)公式リファレンス…UI Router: API Reference
- (英語)よくある質問…Frequently Asked Questions · angular-ui/ui-router Wiki
- (英語)UI-Router: Why many developers don’t use AngularJS’s built-in router
- AngularUI Router Wiki - Home.md の日本語訳
- 初心者向けAngularJS - その2 - albatrosary's blog
- AngularUI Routerのつかいかた
- Angular ui-router tips - まぐねっとのブログ
- ui-router を使ってログイン必須のstateを作る
- angular-ui-router が便利すぎてやばいという話
ui-router以外の参考にした記事
- AngularJS styleguide 日本語訳
- AngularJSのパフォーマンス改善入門
- JavaScript Promiseの本
- [AngularJS] $q サービスで覚える Promise
- ウェブデザイナーがはじめるAngularJS:Promise(Deferred)をつかった非同期処理 | Webデザイン、フロントエンド系の技術に関する備忘録 - whiskers
- AngularJS 地味だけど知っておきたい filter 機能 - Can I do web?
この記事では触れなかった関連する内容の記事
- プロバイダーに関して (factory, service, provider) | AngularJS - angular.jsメモ
- AngularJS の $locationProvider.html5Mode について
- angularでng-annotateを利用してminify対策を自動化する
- (英語)angular-animate + animate.cssを使ったアニメーションの実装例
-
XMLHttpRequestを使用する関係上、
file://
上では開発が難しいです。Windows であれば、XAMPP などを使用して、myAppディレクトリを localhost で開けるように設定すれば良いでしょう。Macはターミナルで SimpleHTTPServer を使えば、すぐに動作を確認できます。 ↩ -
https://github.com/angular-ui/ui-router/issues/739#issuecomment-31702635 ↩
-
「プレゼンテーションロジックのみ (MVVM): controller 内ではプレゼンテーションロジックのみとし、ビジネスロジックは service に委譲する」も参照ください ↩
-
関数ではなく静的な値も注入できますが、resolve 以外の手段(プロバイダなど)を使用するケースが殆どでしょう ↩
-
$urlRouterProvider.otherwise を使用してリダイレクト先を設定することもできます ↩