2017年5月29日 星期一

AngularJS的UI-Router練習

今天要來練習AngularJS很多人使用的外掛,UI-Router,UI-Router以各種不同的 State (狀態) 來管理AngularJS的路由,擴充了AngularJS原生路由的使用功能 (只單純以Url來組織路由),所以網站是以一個State到另一個State的方式去設計。

以下是今天演示的需求:

  1. 設計三個State, home、heroPanel、heroDetail,
    home為首頁,heroPanel顯示英雄(hero)的 id 和 name 的列表,點列表的某一個 hero 會進到heroDetail,顯示 Detail 頁面。
  2. 頁面結構分成三個區塊,header、body和footer,三者的內容皆會跟據State而有所改變。

成品就像下面影片這樣:




首先先來看一下設計的檔案結構:

index.html為主要頁面,且此例也只有一個頁面,並在同一頁中利用UI-Router切換內容。

因為這邊我用npm的方式下載安裝AngularJS和UI-Router,所以有package.json,AngularJS和UI-Router都裝在node_modules裡,當然自己去官網下載也OK。

app資料夾下的為主要JS程式,第一層以app開頭的JS為主要AngularJS Module及其相關設訂,代表著 "home" 的狀態。
其他的資料夾, heroPanel 和 heroDetail 代表 heroPanel 和 heroDetail 兩個狀態,為ui-view是body時的內容設定。
common  資料夾下放著跟 footer 和 header 兩個 ui-view相關的設定

main.css 為 header、 body、footer 標上外框以利識別,也為 class="active" 的元素標上底色以利識別現在狀態。

接下來來看各檔案裡的程式內容



main.css :
.header, .footer, .body{
    border : 1px solid red;
}
.active{
    background-color: #58ff93;
}

index.html :
<!DOCTYPE html>
<html ng-app="app">
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        
        <link rel="stylesheet" href="css/main.css">
        
        <script src="node_modules/angular/angular.js"></script>
        <script src="node_modules/angular-ui-router/release/angular-ui-router.js"></script>
        
        <!-- app -->
        <script src="app/app.js"></script>
        <script src="app/app.service.js"></script>
        <script src="app/app.run.js"></script>
        <script src="app/app.route.js"></script>
        
        <!-- header -->
        <script src="app/common/header/header.module.js"></script>
        <script src="app/common/header/header.controller.js"></script>
        
        <!-- footer -->
        <script src="app/common/footer/footer.module.js"></script>
        <script src="app/common/footer/footer.controller.js"></script>
        
        <!-- heroPanel -->
        <script src="app/heroPanel/heroPanel.module.js"></script>
        <script src="app/heroPanel/heroPanel.route.js"></script>
        <script src="app/heroPanel/heroPanel.controller.js"></script>
        
        <!-- heroDetail -->
        <script src="app/heroDetail/heroDetail.module.js"></script>
        <script src="app/heroDetail/heroDetail.route.js"></script>
        <script src="app/heroDetail/heroDetail.controller.js"></script>
    </head>
    <body>
        <div ui-view="header" class="header"></div>        
        <div ui-view="body" class="body" autoscroll></div>
        <div ui-view="footer" class="footer"></div>
    </body>
</html>

可以看到JS檔案分門別類的在<head>中被加載進了html中,並且在<body>中設定了三個 ui-view ,分別為 header, body, footer。

以下是跟 app 有關JS的程式碼:

app.js :
(function(){
    angular.module("app", ["ui.router", "app.header", "app.footer", "app.heroPanel", "app.heroDetail"]);
})();
設定了 app module,並將 ui-router 的module和 header, footer, heroPanel, heroDetail module引入。

app.route.js :
(function() {
    angular.module("app").config(config);

    config.$inject = [ '$stateProvider' ];

    function config($stateProvider) {
        $stateProvider.state('home', {
            url : '',
            params: {
            },            
            data: { 
            },
            views : {
                'body@' : {
                    template : "這是主頁"
                },
                'header@' : {
                    templateUrl : '/app/common/header/header.html',
                    controller : 'headerController',
                    controllerAs : 'ctrl'
                },
                'footer@' : {
                    templateUrl : '/app/common/footer/footer.html',
                    controller : 'footerController',
                    controllerAs : 'ctrl'
                }
            }         
        });
    }
})();
利用$stateProvider設定了State為home時的route設定,包括了 body, header, footer 三個 ui-view 使用的templateUrl, controller, controllerAs。

app.run.js :
(function() {
    angular.module('app').run(run);

    run.$inject = [ '$transitions' , 'stateService'];

    function run($transitions, stateService) {
        //detect any "to" stage success
        $transitions.onEnter({to: true }, function($transition){            
            console.log($transition.to().name);
            stateService.setState($transition.to().name);
            console.log(stateService.getState());
        });
    }

})();
AngularJS的run()可在執行controller之前執行,在此時例用$transitions來監聽State轉換事件(新版的 ui-router 用法),利用自製的stateService (請看app.service.js) 設定儲存State狀態。

app.service.js :
(function(){
    angular.module("app").factory("stateService", function(){
        var state = "none state";
        return {
            setState : function(newState){
                state = newState;
            },
            getState : function(){
                return state;
            }
        };
    });
    
    angular.module("app").factory("heroService", function(){
        var heroList = [{
                id : 1,
                name : "Hero1",
                superPower : "SuperPower1"
        },{
                id : 2,
                name : "Hero2",
                superPower : "SuperPower2"
        },{
                id : 3,
                name : "Hero3",
                superPower : "SuperPower3"
        },{
                id : 4,
                name : "Hero4",
                superPower : "SuperPower4"
        },{
                id : 5,
                name : "Hero5",
                superPower : "SuperPower5"
        }];
    
        return {
            getHeroList : function(){
               return heroList;
            },
            getHeroById : function(id){
                var hero;
                var i;
                for (i = 0 ; i < heroList.length; i++){
                    console.log(typeof id);
                    console.log(typeof heroList[i].id);
                    console.log(id === heroList[i].id);
                    if (id === heroList[i].id){
                        hero = heroList[i];
                        break;
                    }
                }                
                return hero;
            }
        };
    });
})();
設計stateService用來取得或設定目前State狀態,heroService只是提供hero 列表資訊的Servic。

再來看比較簡單的 footer 和 header,因為內容基本一樣,所以只介紹 header, footer可以以此類推。

header.module.js:
(function() {
    angular.module("app.header", []);    
})();
只是一個module宣告。

header.controller.js :
(function () {
    angular.module('app.header').controller('headerController',
            [
                "stateService",
                function headerController(stateService) {
                    var self = this;
                    self.stateString = stateService.getState();
                }
            ]
    );
    /* 以下為另一種寫法
    headerController.$inject = ["stateService"];

    function headerController(stateService) {
        var self = this;
        self.stateString = stateService.getState();
    }
    */
})();
header的controller設定,引進stateService來獲取現在State並設定在自己scope中的變數上。

header.html :
<a ui-sref="home" ui-sref-active="active">主頁</a>
<a ui-sref="heroPanel" ui-sref-active="active">Hero Panel</a>
<b ng-class="{'active' : ctrl.stateString == 'heroDetail'}">Hero Detail</b>
這是Header,現在狀態為 : {{ctrl.stateString}}
放上前往State為home及heroPanel的連結、"主頁"、"Hero Panel"、"Hero detail"會在當下為其State時標上active的class,並且用文字顯示現在的stateString。

接著來看 State 為 heroPanel 的程式碼:

heroPanel.module.js :
(function() {
    angular.module("app.heroPanel", []);    
})();
module的宣告

heroPanel.route.js :
(function() {
    angular.module("app.heroPanel").config(config);

    config.$inject = [ '$stateProvider' ];

    function config($stateProvider) {
        $stateProvider.state('heroPanel', {
            url : '/heroPanel',
            params: {
            },            
            data: { 
            },
            views : {
                'body@' : {
                    templateUrl : '/app/heroPanel/heroPanel.html',
                    controller : 'heroPanelController',
                    controllerAs : 'ctrl'
                },
                'header@' : {
                    templateUrl : '/app/common/header/header.html',
                    controller : 'headerController',
                    controllerAs : 'ctrl'
                },
                'footer@' : {
                    templateUrl : '/app/common/footer/footer.html',
                    controller : 'footerController',
                    controllerAs : 'ctrl'
                }
            }         
        });
    }
})();
heroPanel的route設定,設定了header, body, footer要使用的templateUrl, controller, controllerAs。

heroPanel.controller.js :
(function() {
    angular.module('app.heroPanel').controller('heroPanelController', heroPanelController);

    heroPanelController.$inject = ["stateService", "heroService"];

    function heroPanelController(stateService, heroService) {
        var self = this;
        self.stateString = stateService.getState();        
        self.heroList = heroService.getHeroList();
    }
})();
heroPanel的controller設定,由stateService和heroService取得目前State和 hero 列表。

heroPanel.html:
<div>我是{{ctrl.stateString}}</div>
<div ng-repeat="hero in ctrl.heroList">
    <a ui-sref="heroDetail({heroId : hero.id})">{{hero.id}} : {{hero.name}}</a>
</div>
heroPanel狀態時body的ui-view內容,顯示 hero 列表,並為每個 hero 加上 ui-sref 連結,連到heroDetail狀態,並帶上一個 heroId 參數。

最後是 heroDetail 相關程式碼。

heroDetail.module.js :
(function() {
    angular.module("app.heroDetail", []);    
})();
heroDetail狀態時的module宣告。

heroDetail.route.js :
(function() {
    angular.module("app.heroDetail").config(config);

    config.$inject = [ '$stateProvider' ];

    function config($stateProvider) {
        $stateProvider.state('heroDetail', {
            url : '/heroDetail/{heroId:int}', //{hero : int } is wrong, can't have space
            views : {
                'body@' : {
                    templateUrl : '/app/heroDetail/heroDetail.html',
                    controller : 'heroDetailController',
                    controllerAs : 'ctrl'
                },
                'header@' : {
                    templateUrl : '/app/common/header/header.html',
                    controller : 'headerController',
                    controllerAs : 'ctrl'
                },
                'footer@' : {
                    templateUrl : '/app/common/footer/footer.html',
                    controller : 'footerController',
                    controllerAs : 'ctrl'
                }
            },
            resolve : {
                hero : ["$stateParams", "heroService", function($stateParams, heroService){
                    return heroService.getHeroById($stateParams.heroId);
                }]
            }         
        });
    }
})();
heroDetail狀態的route宣告,指定了ui-view要使用的 templateUrl, controller, controllerAs,並且注意在 url 設定中指定了會有一個 int 的 heroId 參數輸入,其中 {heroId:int} 不可寫成中間有空格的 {heroId : int},否則會出錯 (原因不明)。
resolve 可以為 heroDetail 指定的 controller 設定可用的 service,在這裡設定了一個叫做 hero 的 service,並用 heroService(heroId) 回傳 hero的detail資訊,可以注意到使用了$stateParams來讀取 url 傳進的 heroId 參數。

heroDetail.controller.js :
(function() {
    angular.module('app.heroDetail').controller('heroDetailController', heroDetailController);

    heroDetailController.$inject = ["stateService", "hero"];

    function heroDetailController(stateService, hero) {
        var self = this;
        self.stateString = stateService.getState();
        self.hero = hero;
    }
})();
heroDetail的controller設定,設定了目前的stateString和 hero 詳細資訊。

heroDetail.html :
<div>我是{{ctrl.stateString}}</div>
<div>id : {{ctrl.hero.id}}</div>
<div>name : {{ctrl.hero.name}}</div>
<div>superpower : {{ctrl.hero.superPower}}</div>
顯示stateString 和 hero 的詳細資訊。

原始碼下載:
angularJS_ui-router.7z

參考資料:
  1. UI-Router (官網)
  2. UI-Router (Github)
  3. 初談 ui-router
  4. $stateChangeStart not being fired on v1.0 #2720 (提到新版UI-Router監測State變化的用法)

沒有留言 :

張貼留言