Angular でフォーム実装してみた

1. Overview

フォームの実装をとおして, Angular (Angular 2 〜) の基礎 (の一部) を学んでみました. その備忘録です.

2. Syntax highlighting of TypeScript by Vim

と … その前に, 私はエディタに Vim を利用しています. Angular では, アプリケーションコードを TypeScript で記述する (Angular 自体も TypeScript で実装されています) ので, TypeScript のシンタックスハイライトを導入します.

プラグインマネージャーは Vundle を利用します.

まずは, 以下のリポジトリを clone します.

$ git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim

.vimrc に以下の記述を追加します.

filetype off

set rtp+=~/.vim/bundle/Vundle.vim

call vundle#begin()

Plugin 'gmarik/Vundle.vim'
Plugin 'leafgarland/typescript-vim'

call vundle#end()

filetype plugin on

 

そして, 以下のコマンドを実行します.

vim +PluginInstall +qall

3. Angular CLI

Angular は, クライアントサイドを (あえて) MVC に分類すると, すべてを担う, フルスタックフレームワークです (ちなみに, React は View のみとシンプル). そのため, (個人的主観ではありますが) ファイル構成が複雑で, 環境構築にもコストを要します. アプリケーションを実装するたびにこれらの作業をこなすのは現実的ではありません.

Angular CLI はこの問題を簡単に解決してくれます. Angular CLI を利用することで, コマンド 1 つで, 最低限必要なファイルの作成や環境構, つまり, アプリケーションの雛形生成を自動でおこなってくれます. Amngular CLI を利用するには, npm でインストールします.

$ npm install -g @angular/cli

アプリケーションの雛形を生成するには, ng new を実行します. 今回は, アプリケーション名を form とします.

$ ng new form
$ cd form

 

アプリケーション名でディレクトリが生成されるので, そのディレクトリに移動し, ls でファイルを確認すると, 多くのファイルやディレクトリが生成されていることがわかります.

% ls -A
.editorconfig     README.md         e2e               package-lock.json src               tslint.json
.gitignore        angular.json      node_modules      package.json      tsconfig.json

これは便利ですね.

3. Implement Form

準備ができたのでフォームを実装します. まずは, src/app/app.module.ts に FormsModule を追加します.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

次に, src/app/app.component.ts の AppComponent に, プロパティとメソッドを定義します.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  private title = 'Form by Angular';
  private user = {
    mail     : 'xxx@gmail.com',
    password : '',
    name     : 'rilakkuma',
    memo     : ''
  };

  show() {
    Object.values(this.user).forEach(value => alert(value));
  }
}

そして, テンプレートである, app.component.html を以下のように記述します.

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>
   <div>
     <label>メールアドレス : <input type="email" name="mail" [(ngModel)]="user.mail" #mail="ngModel" required email /></label>
     <span *ngIf="mail.errors?.required">メールアドレスは必須です</span>
     <span *ngIf="mail.errors?.email">メールアドレスを正しい形式で入力してください</span>
   </div>
   <div>
     <label>パスワード : <input type="password" name="password" [(ngModel)]="user.password" #password="ngModel" required minlength="6"></label>
     <span *ngIf="password.errors?.required">パスワードは必須です</span>
     <span *ngIf="password.errors?.minlength">パスワードは 6 文字以上で入力してください</span>
   </div>
   <div>
     <label>名前 : <input type="text" name="name" [(ngModel)]="user.name" #name="ngModel" required minlength="3" maxlength="10" /></label>
     <span *ngIf="name.errors?.required">名前は必須です</span>
     <span *ngIf="name.errors?.minlength">名前は 3 文字以上で入力してください</span>
     <span *ngIf="name.errors?.maxlength">名前は 18 文字以上で入力してください</span>
   </div>
   <div>
     <label>備考 : <textarea name="memo" [(ngModel)]="user.memo" #memo="ngModel" maxlength="10"></textarea></label>
     <span *ngIf="name.errors?.maxlength">備考は 10 文字以内で入力してください</span>
   </div>
   <div>
     <button type="submit" [disabled]="myForm.invalid">送信</button>
   </div>
</form>

詳細を解説していきます.

まず, form タグには, それぞれ ngFrom ディレクティブ, ngSubmit イベントバインディング, novalidate 属性 (ブラウザによるネイティブの検証を無効にする属性です. Angular 4 〜 では, 自動で付与してくれるので不要です) を指定しています.

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>

ngForm ディレクティブは, submit 時に, フォーム全体の検証をするために利用します.

<button type="submit" [disabled]="myForm.invalid">送信</button>

API が変わっているだけで, AngularJS での, ng-form, ng-submit と大差ないことに気づかれたでしょうか … ?

入力フォーム (email, password, text, textarea) です.

<label>メールアドレス : <input type="email" name="mail" [(ngModel)]="user.mail" #mail="ngModel" required email /></label>

Angular でのフォーム検証を有効にするためは, 双方向バインディング必要となるので, ngModel ディレクティブと要素を識別するための, name 属性が必須になります.

[(ngModel)]="user.mail"

では, AppComponent#user プロパティと双方向バインディングしています. また,

#mail="ngModel"

は, テンプレート参照変数で, ngModel ディレクティブを代入することで, フォーム要素の状態にアクセスできるようにします.

required, email, minlength, maxlength などは, 必要に応じて指定します.

そして, 不正な値であった場合のエラー表示に活躍するのが, *ngIf ディレクティブです (AngularJS での ng-if ディレクティブ).

<span *ngIf="mail.errors?.required">メールアドレスは必須です</span>

*ngIf ディレクティブは, その値が true であった場合のみ, 指定した要素を DOM に追加します. *ngIf ディレクティブと, フォームの検証の成否を組み合わせることで, 簡単にエラーメッセージの表示を実装することが可能です.

フォームの検証の成否の構文を一般化すると,

入力要素名 (テンプレート参照変数名).errors?.検証型

となります. ? は, Angular 2 〜 で利用可能な, Safe Navigation Operator で, errors が null や undefined であってもエラーを発生させることがありません (つまり, 安全にプロパティアクセス可能になります). 検証型には, フォームに指定した, required, email, minlength, maxlength などを指定します.

あとは, フォームに不正な値が入っていれば, submit ボタンが disabled となるように実装します.

<button type="submit" [disabled]="myForm.invalid">送信</button>

[disabled] は, プロパティバインティングです. そこに, (フォームを参照するテンプレート変数).invalid とすることで, それが実装できます. ここでは, myForm が, フォームを参照するテンプレート変数となるので, myForm.invalid と指定します.

最後に,

$ npm start

を実行することで, ng serve が実行され, 簡易サーバーが起動するので, localhost:4200 にアクセスして, フォームが機能していれば完成です !

React Context API

1. Overview

React v16.3 から Context API というのが導入されました. この API を利用することによって Redux と同じようなデータフローを簡単に実装可能になるようです.

2. Implement

重要になるコンポーネントが, React#createContext メソッドによって生成される, Provider と Consumer です. その名前のとおり, Provider の value props に渡した値が, Consumer に渡ってきます.

2 – 1. Provider

Redux での, Provider, createStore に相当する.

2 – 2. Consumer

react-redux の connect に相当する.

'use strict';

import React from 'react';

const { Provider, Consumer } = React.createContext();

const Counter = () => (
    <Consumer>
        {({ state, actions }) => (
            <React.Fragment>
                <span>{state.count}</span>
                <button type="button" onClick={actions.increment}>+1</button>
                <button type="button" onClick={actions.decrement}>-1</button>
            </React.Fragment>
        )}
    </Consumer>
);

export default class App extends React.Component {
    state = {
        count : 0
    };

    constructor(props) {
        super(props);

        this.onClickIncrement = this.onClickIncrement.bind(this);
        this.onClickDecrement = this.onClickDecrement.bind(this);
    }

    onClickIncrement() {
        this.setState(prevState => {
            return { count : (prevState.count + 1) };
        });
    }

    onClickDecrement() {
        this.setState(prevState => {
            return { count : (prevState.count - 1) };
        });
    }

    render() {
        return (
            <Provider
                value={{
                    state   : this.state,
                    actions : {
                        increment : this.onClickIncrement,
                        decrement : this.onClickDecrement
                    }
                }}
            >
                <Counter />
            </Provider>
        );
    }
}

3. Conclusion

小規模なアプリであれば, わざわざ Redux (や MobX) を導入しなくても, Context API でいけるのでは ? と思いました.

Context API のサンプルリポジトリ

react-router v4 で onEnter ライクなことを実装する

1. Overview

react-routerにはあった onEnter というなにかしらの <Route /> に入ったときのフックポイントが用意されていました. しかしながら, v4 からはルーターの機能に特化したものとなり, onEnter などのフックポイントが廃止されました. しかしながら, onEnter ライクな処理をすることは可能なのでその実装方法をメモしておきます.

2. Implement routing by react-router v4

react-router v4 によるルーティングはおおよそ以下のような実装となります.

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import Home from './components/Home';
import PageA from './components/PageA';
import PageB from './components/PageB';
import PageC from './components/PageC';

export default () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/a" component={PageA} />
        <Route exact path="/b" component={PageB} />
        <Route exact path="/c" component={PageC} />
      </Switch>
    </Router>
  );
};

3. onEnter

onEnter ライクな処理となる関数を実装します.

const render = (Component) => ({ history, match }) => {
  // ページローディングなどを表示する: 例 YouTube のページ上部に表示されるプログレスバーなど

  return <Component history={history} match={match} />;
};

ちょっと見づらいかもしれませんが, 関数が関数を返す, いわゆるクロージャ (高階関数) となっています.
history は, ネイティブの history オブジェクトではなく react-router が定義する history オブジェクトで, match は パスからパラメータを取得するためのオブジェクトです.

そして, ルーティングを以下のように実装します.

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import Home from './components/app';
import PageA from './components/PageA';
import PageB from './components/PageB';
import PageC from './components/PageC';

const render = (Component) => ({ history, match }) => {
  // ページローディングなどを表示する: 例 YouTube のページ上部に表示されるプログレスバーなど

  return <Component history={history} match={match} />;
};

export default () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" render={render(Home)} />
        <Route exact path="/a" render={render(PageA)} />
        <Route exact path="/b" render={render(PageB)} />
        <Route exact path="/c" render={render(PageC)} />
      </Switch>
    </Router>
  );
};

注目すべきは, component props ではなく, render props になっていることです (ちなみに, component props と render props は共存できません).

これで onEnter ライクな処理が可能になります.

Node.js + React で SSR ~ SSR の実装 ~

Overview

前回の記事で SSR の環境を構築できたので, さっそく, React を利用した簡単な SSR の実装をしてみましょう.

1. Install packages

$ npm install --save react react-dom babel-cli babel-core babel-preset-es2015 babel-preset-react express

前回の記事でも述べましたが, 本来は –save-dev でインストールすべきパッケージも –save でインストールしていることに注意してください (Heroku のデプロイで dependencies のパッケージがインストールの対象とならないので).

2. Implement React Component

SSR で利用する, React コンポーネントを実装します. といっても, とりあえず <h1 /> で Hello SSR と表示する単純なコンポーネントです.

components/App.js

import React, { Component } from 'react';

export default class App extends Component {
    render() {
        return <h1>Hello SSR</h1>:
    }
}

また, ビルド後の App.js は, ルート直下の App.js にデプロイすることにします.

3. Implement render function

前回の記事では, express のルーターに直接レンダリング処理を記述していましたが, それだとコードの見通しがよくないので, SSR をする専用の関数を実装します.

render.js

const React          = require('react');
const ReactDOMServer = require('react-dom/server');
const App            = require('./App').default;

function render(req, res) {
    const content = ReactDOMServer.renderToString(React.createElement(App));
    const html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Hello SSR</title>
</head>
<body>
    <section id="app">
        ${content}
    </section>
</body>
</html>`;

    res.status(200).send(html);
};

module.exports = render;

ポイントは, ReactDOMServer.renderToString でコンポーネントを HTML 文字列に変換している処理です. あとは, その文字列を HTML に埋め込むだけです.

render 関数が実装できたので, app-server.js を以下のように書き換えます.

app-server.js

'use strict';

const express = require('express');
const app     = express();
const path    = require('path');
const render  = require('./render');

const port = process.env.PORT || 8080;

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res, next) => {
    render(req, res);
});

app.listen(port, () => {
    console.log(`Listening on port ${port} ...`);
});

4. npm scripts

package.json に以下のスクリプトを追加します.

  // ...
  "scripts": {
    "build": "babel components/App.js --out-file App.js",
    "start": "node app.js"
  },
  // ...

5. Edit Procfile

Procfile を以下のように変更します.

web: npm run build && npm start

 

以上で, SSR の実装ができました. 変更したファイルをすべてコミットして,

$ git push heroku master

を実行し, https://nodejs-ssr-sample.herokuapp.com/ にアクセスして, 「Hello SSR」が表示されていれば OK です.

ちなみに, SSR された HTML のソースは以下のようになっています.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Hello SSR</title>
</head>
<body>
    <section id="app">
        <h1 data-reactroot="" data-reactid="1" data-react-checksum="-601091822">Hello SSR</h1>
    </section>
</body>
</html>

AngularJS キャッシュをクリアする

AngularJSでキャッシュが邪魔になる場合の対処法

(特に, IEでは無駄にキャッシュが残りやすい…)

var app = app.module('app', []);

app.controller('Controller', ['$scope', '$cacheFactory', function($scope, $cacheFactory) {
    $cacheFactory('cacheId').destroy();
}]);

AngularJS $httpサービスでのクロスオリジン通信

昨日の記事で, XMLHttpRequest Level 2を利用したクロスオリジン通信を記載しましたが, その方法はAngularJSでのクロスオリジン通信にも使えます.

ただし, Cookieを送信するために, 直接XMLHttpRequestインスタンスにアクセスすることはできないので, $httpサービスのオプションで指定します.

こんな感じです.

 $http({
     // ....,
     withCredentials : true,
     // ....
}).success(function(data, status, headers, config) {
     // do something ....
}).error(function(data, status, headers, config) {
     // do something ....
});

サーバーが返すレスポンスは昨日の記事で記載したままでOKです.

ただし, Ajaxを表すヘッダー, つまり, X-Requested-With: XMLHttpRequest を指定すると, クロスオリジン通信ができなくなるので注意してください.

$http({
     // ....,
     withCredentials : true,
     // ....,
     headers : {
          // いらない
          'X-Requested-With' : 'XMLHttpRequest'
     }
}).success(function(data, status, headers, config) {
     // do something ....
}).error(function(data, status, headers, config) {
     // do something ....
});

AngularJS $qサービス

AngularJSには, 非同期処理を同期処理のように記述し, 可読性を向上させるための$qサービスというのがあります.

jQueryのDeferredオブジェクトと多少APIは異なりますが, その使い方や目的は同じです.

こんな感じ.

AngularJS カスタムサービスの作成 2

昨日の記事で, valueとconstantによるカスタムサービスの作成について記載しましたが, valueとconstantは他のサービスがインジェクションできないという制限がありました.

他のサービスをインジェクトしたサービスを作成したい場合は, (Moduleインスタンスの) factoryかserviceメソッドを利用します.

valueとconstantと同様, factoryとserviceの使い分けはと言いますと, ずばり, プリミティブ型 (数値, 文字列, 配列, プレインオブジェクト) を共通のサービスとして利用したい場合はfactoryメソッドを, クラス (コンストラクタ関数) をサービスとして利用したい場合はserviceメソッドを利用します.

var app = angular.module('app', []);

// factoryはプリミティブ型の登録が可能
app.factory('factoryService', ['$http', function($http) {
    var text = '';

    $http.get('sample.txt')
              .succes(function(data) {
                  text = data;
              });

    // 戻り値がサービスとして登録される
    return text;
}]);

angular.service('serviceClass', ['factoryService', function(factoryService) {
    this.model = factoryService;

     this.getModel = function() {
          return this.model;
     }
}]);

// カスタムサービスのインジェクション
angular.controller('Controller', ['$scope', 'serviceClass', function($scope, serviceClass) {
    // インスタンス化してインジェクションされる
    // シングルトンなので, インジェクションしたすべてのコントローラーやディレクティブで共通して参照される
    console.log(serviceClass.getModel());
}]);

AngularJS カスタムサービスの作成

AngularJSでカスタムサービス, つまり, コントローラーやディレクティブで共通に利用したい, 値や関数, オブジェクトを作成するためには, Moduleインスタンス (angular.moduleメソッドの戻り値) の以下のメソッドを利用します.

  • value
  • constant
  • factory
  • service
  • provider

この記事では,  最も単純なvalueとconstantについてまとめておきます.

valueとconstantの使いどころとしては, 他のサービスをインジェクションする必要がない場合です (逆の視点で表現すると, この2つのメソッドには他のサービスをインジェクションすることができません).

では, valueとconstantの使い分けは ? というのは, 登録したカスタムサービスをModule.configメソッドでも利用した場合は, constantメソッドを, そうでなければvalueメソッドを利用します.

Module.configメソッドは, 主に, 標準サービスのコンフィグレーションをするためのメソッドです. そのため, サービスのインスタンスが生成される前に呼び出されるメソッドなので, インジェクション可能なサービスが限定されるからです.

最後に, 利用例を記述しておきます.

var app = angular.module('app', []);

// 関数をサービスとして登録
// Module.configメソッド以外であればインジェクション可能
app.value('commonService', function() {
    return 'value';
});

// プレインオブジェクトをサービスとして登録
// Modue.configメソッドにもインジェクション可能
app.constant('options', {a : 'a', b : 'b});

// カスタムサービスのインジェクション
app.config(['options', function(options) {
    var a = options.a;  // -> 'a'
    var b = options.b;  // -> 'b'
}]);

app.controller('Controller', ['$sope', 'commonService', 'options', function($scope, commonService, options) {
     var r = commonService(); // -> 'value'
     var a = options.a; // -> 'a'
     var b = options.b; // -> 'b'
}]);