albatrosary's blog

UI/UXとエンタープライズシステム

Angular2 for JavaScript

Angular2もベータ版となり、触りだす方も多くなったと思います。しかし、Angular2のサイトではTypeScriptのサンプルは豊富ですが、ノーマルなJavaScriptで記述されているものがあまり見当たりません。

angular.io

あまりAngular2を触ってませんが、学習目的で次のことを行いました。

  • コンポーネントの利用
  • ルータの定義と利用
  • サービスの依存性注入
  • カスタムフィルタの作り方

作業フォルダーを作る

mkdir Angular2Study && cd $_

必要なライブラリをインストール

npmを利用した場合

npm init -y
npm install angular2@2.0.0-beta.1 --save
npm install rxjs@5.0.0-beta.1 --save

HTMLとして次のように定義しライブラリを読み込ませます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Angular2 Study</title>
</head>
<body>
  <!-- 1. Display the application -->
  
  <!-- 2. Load libraries -->
  <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
  <script src="node_modules/rxjs/bundles/Rx.umd.js"></script>
  <script src="node_modules/angular2/bundles/angular2-all.umd.js"></script>
  
  <!-- 3. Load our 'modules' -->
</body>
</html>

cdnの場合

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Angular2 Study</title>
</head>
<body>
  <!-- 1. Display the application -->
  
  <!-- 2. Load libraries -->
  <script src="https://code.angularjs.org/2.0.0-beta.1/angular2-polyfills.min.js"></script>
  <script src="https://code.angularjs.org/2.0.0-beta.1/Rx.umd.min.js"></script>
  <script src="https://code.angularjs.org/2.0.0-beta.1/angular2-all.umd.min.js"></script>
  
  <!-- 3. Load our 'modules' -->
</body>
</html>

※ UMD (Universal Module Definition)

Angular2を動かしてみる

index.htmlのbodyタグに追加しAngular2を実行します

<!-- 1. Display the application -->
<my-app>Loading...</my-app>

<!-- 2. Load libraries -->
・・・

<!-- 3. Load our 'modules' -->
  <script>
(function(app) {
  app.AppComponent =
    ng.core
      .Component({
        selector: 'my-app',
        template: '<h1>My First Angular {{1+1}} App</h1>'
      })
      .Class({
        constructor: function() {}
      });
      
  document.addEventListener('DOMContentLoaded', function() {
    ng.platform.browser.bootstrap(app.AppComponent);
  });
})(window.app || (window.app = {}));
  </script>

live-server 等を実行すると"My First Angular 2 App" という文字がブラウザに表示されます。 ng-appというのは無くなりAngularを実行するのはbootstrapで行います。 参考までに、DOMContentLoaded というのはDOMの生成完了時に実行されるイベントです。

npm i live-server -g
live-server

デコレータ

下記部分を見なおしてみます

  app.AppComponent =
    ng.core
      .Component({
        selector: 'my-app',
        template: '<h1>My First Angular {{1+1}} App</h1>'
      })
      .Class({
        constructor: function() {}
      });

ComponentやClassはデコレータできるので次のようにも書くことが出来ます。

  app.AppComponent =
    ng.core
      .Class({
        constructor: function() {}
      });

  app.AppComponent =
    ng.core
      .Component({
        selector: 'my-app',
        template: '<h1>My First Angular {{1+1}} App</h1>'
      })(app.AppComponent);

Angular1であった変数として引き渡す場合、つまり下記「{{name}}」のようなパターンで利用したい場合は

  app.AppComponent =
    ng.core
      .Component({
        selector: 'my-app',
        template: '<h1>My First {{name}} {{1+1}} App</h1>'
      })(app.AppComponent);

.Classでthis.nameを記載すればバインドされます。

  app.AppComponent =
    ng.core
      .Class({
        constructor: function() {
          this.name = 'Angular';
        }
      });

Routing

ルーティングを行うためにいくつかのファイルを実装する必要があります。 まずコンポーネントとして

component/home/home.html
component/home/home.js
component/todo/todo.html
component/todo/todo.js

というファイルを作成します。

HTMLは下記の通りとします。

(home.html)

<h1>Home</h1>

(todo.html)

<h1>Todo</h1>

JavaScriptはそれぞれ

(home.js)

(function(app) {
  app.HomeComponent =
    ng.core
      .Component({
        selector: 'app-home',
        templateUrl: 'components/home/home.html'
      })
      .Class({
        constructor:  function () {}
      });
})(window.app || (window.app = {}));

(todo.js)

(function(app) {
  app.HomeComponent =
    ng.core
      .Component({
        selector: 'app-home',
        templateUrl: 'components/todo/todo.html'
      })
      .Class({
        constructor: function () {}
      });
})(window.app || (window.app = {}));

ルーティングの設定を行うJavaScriptは下記のように RouteConfig を使って定義することが可能です。ちなみに、Angular2のルーティングURLですが、デフォルトがハッシュバンではなくHTML5Modeです。

(scripts/main.js)

  app.AppComponent =
    ng.core
      .Class({
        constructor: function () {}
      });
  
  app.AppComponent = 
     ng.core
       .Component({
         selector: 'app-main',
         template: `
            <h1>Component Router</h1>
            <a [routerLink]="['Home']">Home</a>
            <a [routerLink]="['Todo']">Todos</a>
            <router-outlet></router-outlet>
        `,
         directives:[
           ng.router.ROUTER_DIRECTIVES
         ]
      })(app.AppComponent);
  
  app.AppComponent =
    ng.router
      .RouteConfig([
        {path: '/home', name: 'Home', component: app.HomeComponent, useAsDefault: true},
        {path: '/todo', name: 'Todo', component: app.TodoComponent}
      ])(app.AppComponent);
      
  document.addEventListener('DOMContentLoaded', function() {
    ng.platform.browser.bootstrap(app.AppComponent, [ng.router.ROUTER_PROVIDERS]);
  });

ソースを追いきれてないですが、routerLink で

<a [routerLink]="['Home']">Home</a>
<a [routerLink]="['Todo']">Todos</a>

<a [routerLink]="['home']">Home</a>
<a [routerLink]="['todo']">Todos</a>

とし、ルーティングの設定を

{path: '/home', name: 'home', component: app.HomeComponent, useAsDefault: true},
{path: '/todo', name: 'todo', component: app.TodoComponent}

としてもエラー(EXCEPTION: Error during instantiation of Router! (RouterLink -> Router).)が発生します。

最後にindex.htmlに必要なファイルとディレクティブを定義すると完成です。

  <!-- 1. Display the application -->
  <app-main>loading...</app-main>
  
  <!-- 2. Load libraries -->
  <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
  <script src="node_modules/rxjs/bundles/Rx.umd.js"></script>
  <script src="node_modules/angular2/bundles/angular2-all.umd.js"></script>

  <!-- 3. Load our 'modules' -->
  <script src="components/home/home.js"></script>
  <script src="components/todo/todo.js"></script>
  <script src="scripts/main.js"></script>

今回はTypeScriptを利用せず、モダンなライブラリも利用せず、Angular1に近いコーティングでAngular2を書き振る舞いを確認し書き進めてみました。

サービスの依存性注入

ビジネスロジックを記述するサービスを定義しComponentで利用します。Angularの依存性注入にはいくつか方法があり

  • アプリケーション全体での依存性注入
  • コンポーネントでの依存性注入

アプリケーション全体ではBootstrap時に行います。

ng.platform.browser.bootstrap(app.AppComponent, [Service]);

ここではコンポーネントでの依存性注入を見ていきます。依存性注入させるサービスを作成します。

(service/app.service.js)

(function(app) {
  var App = function () {
  }
  
  App.prototype.someMethod = function () {
    return 'Some Method!';
  }
  
  app.AppService =
    ng.core
      .Injectable()
      .Class({
        constructor: App
      });
})(window.app || (window.app = {}));

問題はこのサービスをコンポーネントに受け渡す方法ですが、コンポーネントで

  • providersを定義する
  • constructor句で引数渡しする
  • コンポーネントのfunction生成時に引数として取り込む

ですが、Angular1の$Injectを使ったときと似たような雰囲気です。コードは上記のhome.jsを利用すると

(home.js)

(function(app) {
  var Home = function (AppService) {
    this.name = AppService.someMethod();
  }
  
  /* Componentsの登録 */
  app.HomeComponent =
    ng.core
      .Component({
        selector: 'app-home',
        templateUrl: 'components/home/home.html',
        providers: [app.AppService]
      })
      .Class({
        constructor: [app.AppService, Home]
      });
})(window.app || (window.app = {}));

最後にindex.htmlにサービスを読みこませれば完了です。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <base href="/">
  <title>Document</title>
</head>
<body>
  <!-- 1. Display the application -->
  <app-main>loading...</app-main>
  
  <!-- 2. Load libraries -->
  <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
  <script src="node_modules/rxjs/bundles/Rx.umd.js"></script>
  <script src="node_modules/angular2/bundles/angular2-all.umd.js"></script>

  <!-- 3. Load our 'modules' -->
  <script src="service/app.service.js"></script>
  <script src="components/home/home.js"></script>
  <script src="components/todo/todo.js"></script>
  <script src="scripts/main.js"></script>
</body>
</html>

カスタムフィルタ

ngForを使って一覧表を作成し、簡易検索画面を作ってみます。まずHTMLは次の通り

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <!-- 1. Display the application -->
  <my-app>Loading...</my-app>
  
  <!-- 2. Load libraries -->
  <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
  <script src="node_modules/rxjs/bundles/Rx.umd.js"></script>
  <script src="node_modules/angular2/bundles/angular2-all.umd.js"></script>

  <!-- 3. Load our 'modules' -->
  <script>
(function(app) {
  app.AppComponent =
    ng.core
      .Component({
        selector: 'my-app',
        template: `
        <input type="text" [(ngModel)]="hoge">
        <ul>
          <li *ngFor="#data of demoData">{{data.name}} - {{data.age}}</li>
        </ul>
        `
      })
      .Class({
        constructor: function() {
          this.demoData = [
            {name: '山田', age: 24},
            {name: '田中', age: 28},
            {name: '佐藤', age: 18},
            {name: '井上', age: 32},
            {name: '高橋', age: 46}
          ]
        }
      });
      
  document.addEventListener('DOMContentLoaded', function() {
    ng.platform.browser.bootstrap(app.AppComponent);
  });
})(window.app || (window.app = {}));
  </script>
</body>
</html>

Angular1のデモでよく見かけたfilterは存在せず自分で作ります。フィルターはPipsで作成します。今回のフィルターは簡易検索のため次のように定義します

(pipes/search.pipe.js)

(function(app) {
  app.SearchPipe =
    ng.core
      .Pipe({
        name: 'search'
      })
      .Class({
        constructor: function () {},
        transform(value, hoge) {
          return value.filter((item)=>item.name.startsWith(hoge)) 
        }
      });

})(window.app || (window.app = {}));

もしくは

(function(app) {
  var Search = function () {
    
  }
  Search.prototype.transform = function (value, hoge) {
    return value.filter((item)=>item.name.startsWith(hoge)) 
  }
        
  app.SearchPipe =
    ng.core
      .Pipe({
        name: 'search'
      })
      .Class({constructor: Search});

})(window.app || (window.app = {}));

HTMLは次のように書き足します

<!-- 3. Load our 'modules' -->
  <script src="pipes/search.pipe.js"></script>
  <script>
(function(app) {
  app.AppComponent =
    ng.core
      .Component({
        selector: 'my-app',
        template: `
        <input type="text" [(ngModel)]="hoge">
        <ul>
          <li *ngFor="#data of demoData | search:hoge">{{data.name}} - {{data.age}}</li>
        </ul>
        `,
        pipes: [app.SearchPipe]
      })
      .Class({
        constructor: function() {
          this.demoData = [
            {name: '山田', age: 24},
            {name: '田中', age: 28},
            {name: '佐藤', age: 18},
            {name: '井上', age: 32},
            {name: '高橋', age: 46}
          ]
        }
      });
      
  document.addEventListener('DOMContentLoaded', function() {
    ng.platform.browser.bootstrap(app.AppComponent);
  });
})(window.app || (window.app = {}));
  </script>

<li *ngFor="#data of demoData | search:hoge">searchフィルターを使ってます。hogesearchフィルターに対する引数になります。対応するpipes/search.pipe.jsのtransform(value, hoge)部分は「|(パイプ)」で受けた値value<input type="text" [(ngModel)]="hoge">で定義されたhogeを引数として処理します。処理内容はpipes/search.pipe.jsを確認してください。あとはコンポーネントにpipesの定義を加えれば絞込検索を作ることができます。

超基本的なところまでは書き進められたかなと思います。もう少し綺麗に書けないかなと思うところもありますがひとまずこれでということで。ドキュメントとしてはCheat Sheetとか見るととても良かった。

angular.io

間違いとかココはこういうふうじゃない?とかありましたらご意見お願い致します