LoginSignup
63
64

More than 5 years have passed since last update.

入門 angular-ui-router

Last updated at Posted at 2015-07-08

angular-ui-router

angular-ui-router は、AngularJS のルーティング、およびビュー分割プラグインです。

セットアップ

CDNJSを利用して、必要なファイル angular.jsangular-ui-router.jsを読み込みます。

index.html
<!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 が読み込まれていることを確認できます。

⌘+⌥+J
angular.module('ui.router')// Object

module名が ui.router であることに注意してください。

myAppの初期化

tree
myApp
├── index.html
├── index.js
└── top
    ├── index.html
    └── index.js

アプリケーションを初期化します。
DEMO

index.html
<!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>
index.js
// Dependencies
angular.module('myApp',[
  'ui.router',
])

// Routes
.config(function($stateProvider){
  $stateProvider.state('top',{
    url: '/',
    templateUrl: 'top/index.html',
    controller: 'topCtrl as top',
  })
})
top/index.html
<a ui-sref="top">{{top.title}}</a>
top/index.js
// 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は、下記のように修正します。

index.js
// 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.htmlindex.html#/ 、両方で state が top と解釈されます。

子ステート

tree
myApp
├── index.html
├── index.js
└── top
    ├── index.html
    ├── index.js
    ├── search.html
    └── search.js

ステート名$stateProvider.state(name)をドットで区切ると、ステートに所属するステートを作成します。
DEMO

index.html
<!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>
index.js
// 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('', '/')
})
top/index.html
<a ui-sref="top">{{top.title}}</a>
<a ui-sref=".search">search</a>

<div ui-view></div>
top/index.js
// Dependencies
angular.module('myApp')

// Publish
.controller('topCtrl',function(){
  this.title= 'hello world'
})
top/search.html
{{topSearch.title}}
top/search.js
// 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'

ビューの分割

tree
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

index.html
<!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>
index.js
// 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

外部リソースを待つ

tree
myApp
├── index.html
└── index.js

データベースや外部からデータを受け取るまで、ビューを表示させたくない場合は、ステートに resolve プロパティを設定します。
以下の例では、$httpサービスを使用して、iTunesAPI から jsonp を待ち、その後でビューを表示します。
DEMO

index.html
<!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>
index.js
// 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のパース

tree
myApp
├── index.html
└── index.js

#/search/澤野弘之 のようなURIへアクセスした時、searchステートに澤野弘之を変数として渡したい場合があります。その場合、ステートをurl:'search/:query'と書くと、resolvecontroller*template*$stateParamsを注入することで、$stateParams.queryから澤野弘之を取得することができます。
DEMO

index.html
<!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>
index.js
// 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 です。

urlUrlMatcherを介して$stateParamsに変換されます。 参考:(英語)UI Router: UrlMatcher

例外処理

tree
myApp
├── index.html
└── index.js

$stateProvider、はアクセス時のURIを、.stateで追加した順番にurlを評価し、はじめに一致したステートを実行します。一致しなかったとき、表示するビューが無いため白紙になります。
url:'/:path'を設定したステートを一番最後に追加する5ことで、一致しなかった全ての url を捕まえることができます。
DEMO

index.html
<!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>
index.js
// 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以外の参考にした記事

この記事では触れなかった関連する内容の記事


  1. XMLHttpRequestを使用する関係上file://上では開発が難しいです。Windows であれば、XAMPP などを使用して、myAppディレクトリを localhost で開けるように設定すれば良いでしょう。Macはターミナルで SimpleHTTPServer を使えば、すぐに動作を確認できます。 

  2. https://github.com/angular-ui/ui-router/issues/739#issuecomment-31702635 

  3. プレゼンテーションロジックのみ (MVVM): controller 内ではプレゼンテーションロジックのみとし、ビジネスロジックは service に委譲する」も参照ください 

  4. 関数ではなく静的な値も注入できますが、resolve 以外の手段(プロバイダなど)を使用するケースが殆どでしょう 

  5. $urlRouterProvider.otherwise を使用してリダイレクト先を設定することもできます 

63
64
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
64