趣味で新しいWebアプリを作る時はいつもtwitterのBootstapになってしまうため、たまには気分を変えようと思いAngular Material Designというものを使ってみました。

いくつかはまったのでメモ。

まず、注意点。現時点でgithubの公式のドキュメント上には以下のようにまだまだ未完成であると記載されています。また、最近出たばかりのangular 1.3.0以降がrequirementとなっています。

This project is still in early preview. It is a complementary effort to the Polymer project’s paper elements collection.
Please note that using Angular Material requires the use of Angular 1.3.x or higher.

ベースのangularもmaterial designと密接な関係のあるpolymerもどんどん変更されていっているので、それらの変更に追従していくためにもまだまだ安定する気配はなさそうです。

さっそくbowerでインストールして試してみると、なかなか動かない!例えばSideNavを使おうと思って以下のページのdemoにあるコードそのまま使っても“$$hasClass is not defiend”みたいな妙なエラーが出たりしました。

bower install angular --save
bower install angular-material --save

上記で1.3.0を入れて
結局試行錯誤で気づいたことはリリースされたばかりのangular 1.3.0だと動かないけど、angular 1.3.0 RC-4だったら最新のmaterial designのコードで動くということが判明。(2014.10.21時点)

bower install angular#1.3.0-rc.4 --save
bower install angular-material#master --save

これに気づくのに何時間も取られてしまった。。
どんどんリポジトリーが更新されていっているので、意味分からないエラーに遭遇したらまずはバージョンをアップして試してみるのが良さそう。

では、実際にangular 1.3.0 RC4とangular material使ってSideNavボタンダイアログを記述していきます。

まずは、必要なJSリソースの読み込み。

  • angular/angular.js
  • angular-animate/angular-animate.js
  • hammerjs/hammer.js
  • angular-material/angular-material.js
  • angular-resource/angular-resource.js
  • angular-route/angular-route.js
  • angular-aria/angular-aria.js
  • angular-facebook/angular-facebook.js

上記のangular-routeやangular-facebookはmaterialが依存しているものではないですが、使うので記載。(angular-facebookについては別記事で書くと思います。)

あと、material用にCSSも必要です。bower installした中にある以下のCSSを読み込む

  • angular-material/angular-material.css

とりあえずhtml以下のように書くとsidenavがちゃんと動きます。

    <div class="main-content">
        <div layout="vertical">
            <section layout="horizontal" flex ng-controller="SidebarCtrl">
                <md-sidenav class="md-sidenav-left md-whiteframe-z2" component-id="left">
                    <md-toolbar>
                        <h1 class="md-toolbar-tools md-theme-light">My App</h1>
                        <div>
                            <a class="menu-item menu-sub-item" ng-href="#/" ink-ripple><span>Top</span></a>
                            <a class="menu-item menu-sub-item" ng-href="#/list" ink-ripple><span>List</span></a>
                            <a class="menu-item menu-sub-item" ng-href="#/new" ink-ripple><span>Create</span></a>
                        </div>
                    </md-toolbar>
                    <md-content class="md-content-padding">
                        <md-button ng-click="close()" class="md-button-colored" hide-md>Hide Menu</md-button>
                    </md-content>
                </md-sidenav>
                <md-content flex class="md-content-padding">
                    <div layout="vertical" layout-fill layout-align="center center">
                        <div>
                            <md-button ng-click="toggleLeft()"
                            class="md-button-colored" hide-md>Show Menu</md-button>
                        </div>
                        <div ng-view></div>
                    </div>
                </md-content>
            </section>
        </div>
    </div>

こんな感じ。

スクリーンショット 2014-10-21 16.31.41

スマホ用に領域を狭めると自動的にメニューが隠れてtoggleできるようになります。

Default
スクリーンショット 2014-10-21 16.32.33

Menu opened
スクリーンショット 2014-10-21 16.33.58

基本的にmaterial designのdirectiveの中で処理をしてくれるので、sidenavに関して記述した部分は開閉時の処理のみcontrollerに記載。
ngMaterial“をモジュールの依存に記述するのは忘れずに。

var controllers = angular.module("hoge.controllers", ["ngMaterial"]);

controllers.controller('SidebarCtrl', ["$scope", "$mdSidenav", function($scope, $mdSidenav) {
    $scope.toggleLeft = function() {
        $mdSidenav('left').toggle();
    };
    $scope.close = function() {
        $mdSidenav('left').close();
    };
}]);

左側のメニューがクリックされた時の処理は普通のangularのrouterを使っています。以下のような感じ。(facebookProviderらへんの認証処理については別記事で)

var app = angular.module("hoge", ["ngRoute", "ngAnimate", "ngMaterial", "facebook", "hoge.config", "hoge.controllers", "hoge.services"]);

app.config(["$routeProvider", "FacebookProvider", "fbClientId", function($routeProvider, FacebookProvider, fbClientId){
    // setup facebook auth
    FacebookProvider.init(fbClientId);

    $routeProvider.when("/", {
        controller: "TopCtrl",
        templateUrl: "/js/hoge/views/top.html"
    }).when("/list", {
        controller: "ListCtrl",
        templateUrl: "/js/hoge/views/list.html"
    }).when("/new", {
        controller: "NewCtrl",
        templateUrl: "/js/hoge/views/new.html"
    }).otherwise({redirectTo: "/"});
}]);

listがクリックされるとlist.htmlが読まれ、ng-viewに追加され表示されます。
list.html

<div>
    <md-content>
        <md-list>
            <md-item ng-repeat="item in items">
                <md-item-content>
                    <div class="md-tile-left">
                        <img ng-src="{{item.face}}" class="item_face" alt="{{item.who}}">
                    </div>
                    <div class="md-tile-content">
                        <h3>{{item.what}}</h3>
                        <h4>{{item.who}}</h4>
                        <p>{{item.notes}}</p>
                        <md-switch ng-model="item.enabled" aria-label="Switch">
                            Enabled: {{ item.enabled }}
                        </md-switch>
                    </div>
                </md-item-content>
            </md-item>
        </md-list>
    </md-content>
</div>

ListCtrlというcontrollerでセットした値をループでmd-listというコンポーネントを使って表示しています。ついでにmd-switchという部品も各アイテムに追加してみました。
最後にcontrollerでサーバーから返されるダミーデータを入れる。

controllers.controller('ListCtrl', ["$scope", "appConfig", "MultiItemLoader", "$mdDialog", function($scope, appConfig, MultiItemLoader, $mdDialog) {
    $scope.appConfig = appConfig;
    $scope.$watch("appConfig.accessToken", function(val){
        console.log("accessToken watcher:" + val);
        if(val){
            MultiItemLoader().then(function(items){ $scope.items = items; });
            $mdDialog.hide();
        }else{
            $scope.items = [];
            $mdDialog.show({templateUrl: '/js/hoge/views/dialog/login.html'});
        }
    });
}]);

MultiItemLoader

var services = angular.module("hoge.services", ["ngResource"]);

services.factory("Item", ["$resource", function($resource){
    return $resource("/api/list/:id", {id: "@id"});
}]);

services.factory("MultiItemLoader", ["Item", "$q", function(Item, $q){
    return function(){
        var delay = $q.defer();
        Item.query(function(items){
            delay.resolve(items);
        }, function(){
            delay.reject("An error occurs");
        });
        return delay.promise;
    }
}])

nodejs+expressで作られたapiは/api/listで以下のようなjsonを返します。

            [{
                face : '/img/list/60.jpeg',
                what: 'Brunch this weekend?',
                who: 'Min Li Chan',
                when: '3:08PM',
                notes: " I'll be in your neighborhood doing errands",
                enabled: true
            },...]

ちょっと色々飛ばしてcontrollerの部分にappConfig.accessTokenとか出てきていますが、これはangular-facebook使ってクライアント側で認証状況が変わってセットされるaccessTokenのことを表しています。
ユーザーのfacebookのアクセストークンがある場合ログイン済として、APIコールをし、トークンが無い場合はログイン用のダイアログを表示するようにしています。

ログインしていないと$mdDialogを使って以下のようなダイアログが表示されます。

スクリーンショット 2014-10-21 16.48.07

views/dialog/login.html

<md-dialog aria-label="Facebook Login" ng-controller="LoginDialogCtrl">
  <div class="dialog-content">
    <md-button class="md-button-colored" ng-click="IntentLogin()">Facebook Login</md-button>
  </div>
  <div class="dialog-actions" layout="horizontal" layout-align="end">
    <md-button ng-click="hide()">
      Cancel
    </md-button>
  </div>
</md-dialog>

LoginDialogCtrlは以下のようにangular-facebookのcontrollerを再利用しています。

controllers.controller("LoginDialogCtrl", ["$scope", "$controller", "$mdDialog", function($scope, $controller, $mdDialog){
    // reuse auth controlller
    var authCtrlScope = $scope.$new();
    $controller("AuthCtrl", {$scope: authCtrlScope});

    $scope.IntentLogin = function(){
        authCtrlScope.IntentLogin();
    };

    $scope.hide = function(){
        $mdDialog.hide();
    };
}]);

以上、後半登場するfacebook認証周りは別記事で詳しく掲載させてもらうとして、こんな感じで、polymerではなくangularでmaterial designのwebアプリが簡単に作れていく感じが伝わったかなと思います。
まだフレームワークが不安定でちょっとしたバージョン違いで全然動かなくなったりしますが、angularの最近の人気っぷりは絶大なのでこのangular material designは今後注目されるライブラリになるのではないでしょうか。

引き続きangular material designを使ったアプリ構築について随時更新していく予定です。


Categories: JS

Related Posts

JS

NodeでWordPress APIを使って投稿してみる

WordPressのNodeJS用モジュールを使ってみたのでメモ。

JS

Node.jsでEvernoteのリマインダー通知機能を実装する

趣味で作っているLunchTimerというアプリを公開してそろそろ1ヶ月くらい経つと思いますが、このアプリは今のところろそこそこ好評で自分のチームでは毎日みんなで投票したりしてランチを決めています。社内でも少しずつ口コミで広まってきてる感もあります。 さて、実際に運用し始めてみると、急遽必要とされている機能が見えてきました。 それは、うっかり投票のし忘れをなんとかならないかw という点です。 業務に集中してると、気づいたら投票の時間過ぎてたなんてことがしばしばあります。 そんな時は、携帯に通知できたら良いなということでEvernoteのリマインダーの機能を使って実装することにしました。

JS

node.js+expressのtemplates engineにunderscore templatesを使用する

nodejsとexpressを使用した時にデフォルトでテンプレートエンジンがjadeやejsになっていると思いますが、jadeなどはhtmlとかけ離れた書き方であるため新しくtemplate engine自体の学習が必要になります。 そこで単純にresponseに.htmlを使用したいケースがあると思いますので、htmlとしてviewを書き出し、必要な変数をテンプレートに埋め込むhtmlベースの簡易テンプレートとしてunderscore templatesを使った方法を紹介します。