中安拓也のブログ

プログラミングについて書くブログ

【Angular】複数項目にまたがるカスタムバリデーションを作る(パスワード・確認用パスワードなど)

f:id:l08084:20181125171015p:plain
パスワード・確認用パスワード入力フォームの例

はじめに

アカウント登録画面を作っていて出現頻度が著しく高い項目といえば............そう!パスワードの入力フォームですね。というわけで今回は、パスワードと確認用パスワードが一致しているか確認するバリデーションを実装していきます。

そもそもAngularでカスタムバリデーションをどう作っていいかわからねーよという人は下記の記事を!

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション) - 中安拓也のブログ

Angular Materialのインストール方法と使い方については、下記の記事を参照してください。

Angular Material の Tableを使う - 中安拓也のブログ

Angular Materialでログインフォームを作る - 中安拓也のブログ

バリデーション内容

パスワードについてのバリデーションを設定します。チェックする項目は下記とします。

  • パスワード欄と確認用パスワード欄で入力された内容が一致する

バージョン情報

JavaScriptのフレームワークとしてはAngularを、CSSのフレームワークとしてはAngular Materialを使用しています。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

パスワード入力フォーム作成

前回の記事で作成した入力フォームにコードを足していく感じで実装していきます。
コードが多くなってきて前回からの更新部分がわかりづらいですが、<!-- add this! -->とコメントされている行が追加された部分になります。

f:id:l08084:20181124201537p:plain
前回の記事で作成した入力フォーム

まずテンプレートに、パスワード入力フォームと確認用パスワードの入力フォーム、そしてパスワードが表示しないときに表示するエラーメッセージを表示します。

app.component.html

<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <!-- 氏名の入力フォーム -->
        <mat-form-field class="input-field">
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須項目です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- add this! -->
        <!-- パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'password' : 'text'" matInput
            placeholder="パスワード" id="password" name="password" [formControl]="nameRegisterForm.controls.password" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.password.errors?.required">パスワードは必須項目です</mat-error>
        </mat-form-field>
        <!-- 確認用パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'confirmPassword' : 'text'" matInput
            placeholder="確認用パスワード" id="confirmPassword" name="confirmPassword" [formControl]="nameRegisterForm.controls.confirmPassword" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.required">確認用パスワードは必須項目です</mat-error>
          <!-- add this! -->
          <!-- パスワードが一致しないときに表示するエラー -->
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.notMatchPassword">パスワードが一致しません</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

コンポーネントクラスにもパスワード入力フォームに関するコードを追加します。(テンプレートの時と同じく追加部分には// add this!とコメントしています)

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;
  // add this!
  public passwordControl: FormControl;
  public confirmPasswordControl: FormControl;
  public hide = true;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
    // add this!
    this.passwordControl = this.nameRegisterForm.get('password') as FormControl;
    this.confirmPasswordControl = this.nameRegisterForm.get('confirmPassword') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]],
      // add this!
      password: ['', [Validators.required]],
      confirmPassword: ['', [Validators.required]]
    }, {
      validator: CustomValidator.matchPassword
    });
  }
}

今回追加するのは複数項目に対するバリデーションなので、他のバリデーションと違ってvalidator: CustomValidator.matchPasswordという風にフォーム全体にバリデーションを定義しています。

最後にパスワード入力フォームに入力された内容と確認用パスワードに入力された内容が一致していることを確認するカスタムバリデーションmatchPasswordメソッドを追加します。

custom-validator.ts

import { ValidationErrors, FormControl, AbstractControl } from '@angular/forms';

export class CustomValidator {

  // 姓と名の間にスペースが入っているか確認する
  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }

  // add this!
  // パスワードと確認用パスワードが一致するかチェック
  static matchPassword(ac: AbstractControl) {
    const password = ac.get('password').value;
    const passwordConfirm = ac.get('confirmPassword').value;
    if (password !== passwordConfirm) {
      ac.get('confirmPassword').setErrors({ notMatchPassword: true });
    }
  }
}

単項目に対するバリデーションであるhaveBlankには引数としてFormControlを渡していますが、複数項目に対するバリデーションであるmatchPasswordにはAbstractControlを渡しています。

動作確認

書いたコードを動かしてみます

f:id:l08084:20181128091521p:plain
初期表示

一致しないパスワードを入力するとエラーが表示されます。

f:id:l08084:20181128091844p:plain
パスワード不一致のエラーメッセージが表示されている

一致するパスワードを入力するとエラーが表示されなくなるので、バリデーションが正しく機能していることがわかります。

f:id:l08084:20181128092002p:plain

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション)

はじめに

アカウント登録画面でよく見る姓と名の入力欄について、フォームが二つに別れているパターンと、フォームが一つになっていてスペースで姓と名を区切るパターンの二つがあると思います。

今回は後者を採用した場合に必要になるバリデーションについて書きます(なぜなら今作ってるシステムがそうなので)。

とはいえ、他のWebサービスを見ると基本的に姓と名でフォームを分けてる場合がほとんどですね。なんでフォームを一つにしちゃったんだろう。。。デザインの問題?画面が縦に長すぎるとまずいとか

f:id:l08084:20181118142046p:plain
姓と名が別れている入力フォームの例

バリデーション内容

氏名についてのバリデーションを設定します。チェックする項目は下記とします。

  • 氏名に少なくとも1つのスペース(半角もしくは全角)が入力されていること
  • ただし、スペースが文頭または文末に入力されている場合は、未入力の扱いとする
  • 複数スペースが入力された場合は、最初のスペースを姓名の区切りとして利用する

バージョンなどの情報

JavaScriptのフレームワークとしてはAngularを使用していますが、他フレームワークや生JavaScriptでもバリデーションのロジック部分は変わらないと思います。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

CSSフレームワークとして、Angular Material を使用しています。Angular Material自体のインストール方法や使い方は下記の記事を参照してください。

Angular Material の Tableを使う - 中安拓也のブログ

Angular Materialでログインフォームを作る - 中安拓也のブログ

Angularのバリデーション機能は、テンプレート(HTML)に検証ルールを記述するテンプレート駆動型と、コンポーネント側に検証ルールを記述するモデル駆動型に分かれていますが、本記事ではモデル駆動型の方を使用しています。

まず姓と名の間にスペースが入力されているかを判定するカスタムバリデーションを作成します。

  • custom-validator.ts
import { ValidationErrors, FormControl } from '@angular/forms';

export class CustomValidator {

  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }
}

実装内容としては、文字列の両端のスペースを取り除いた後に、半角または全角のスペースが入力されているかどうかをindexOf()メソッドを使用してチェックしています。(indexOf()メソッドの代わりにincludes()メソッドを使ってもOKです)

続いて、作成したカスタムバリデーションをフォームの方に設定してあげると、実装が完了します。

  • app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   * @private
   * @memberof AppComponent
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]]
    });
  }
}
  • app.component.html
<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <mat-form-field class="input-field">
          <!-- 氏名の入力フォーム -->
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須入力です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

スペースが入力されなかった場合のエラーメッセージについては、テンプレート(HTML)の方に記載しています。

動作確認

実際に動かしてみます。

f:id:l08084:20181124201537p:plain
初期表示

姓と名の間にスペースが入力されていない氏名を入力すると、ちゃんとエラーメッセージを表示してくれます。

f:id:l08084:20181124201731p:plain

姓と名の間にちゃんとスペースを入力してあげると、エラーメッセージが消えて登録ボタンもアクティブになりました。成功ですね f:id:l08084:20181124201929p:plain

おわりに

Angularのカスタムバリデーションですが、他にもディレクティブ機能を使う方法などがあり、また今回の例だとそもそもカスタムバリデーションを作らずに正規表現で解決する方法もありました。実際のシステム開発だと姓と名それぞれの文字数制限などのバリデーションが追加されるであろうことを考えると、ディレクティブを使ったり正規表現を使ったほうがキレイなコードが書けたのでは.....みたいな気持ちがあります。でも次回への課題ということでこの記事自体は終了します。

アカウント登録メールの実装をしています

f:id:l08084:20181110163634p:plain
アカウント登録メールの例

はじめに

Webサービスで会員登録の情報を入力した後に、メールアドレス確認のメールが飛んでくるサービスよくありますよね。
あれの実装をいま仕事でしています。よくある機能だと思うので、実装方針を備忘録として記録しました。

作成しているシステムについて

アカウント登録機能のあるWebアプリケーションで、採用している言語・ミドルウェアは下記の一覧を参照

  • Java
  • Angular
  • Ionic
  • PostgreSQL
  • AWS

メールの具体的な仕様

今回実装しているアカウント登録メールの大まかな仕様

  1. ユーザーが仮登録(メールアドレスなどを入力)を完了する
  2. 本登録画面へのURLリンクを含んだメールをユーザーに送信する
  3. URLリンクをクリックしたユーザーを認証して問題なければ、本登録画面に遷移させる

この記事では、URLリンクをクリックしたアカウントの認証(というか特定)方法に絞って解説します。
HTMLメールのレイアウトなどについては、こちらの記事をご参照ください。

アカウント登録のためのURLリンクを作成する

アカウント登録メールでは、アカウント本登録画面に遷移するURLだけではく、クリックしたユーザーを特定する情報が必要になります。

本システムでは、メールを送信する前の画面で入力させる情報がメールアドレスしかなかったため、URLのパラメーターにメールアドレスを含めることでアカウントを特定することにしました。

メールに載せるURLのイメージとしてはこんな感じです。
https://[システムのドメイン]/[アカウント本登録画面のパス]/[暗号化したメールアドレス]

メールアドレス暗号化の手順

URLに生のメールアドレスをそのまま載せるのはセキュリティ的にアレだよね...ってなったので暗号化することにしました。

  1. サーバサイド(Java)でAWS Key Management Service (KMS)を利用してメールアドレスを暗号化する
  2. 暗号化したテキストをBase64エンコードする
  3. 2.のテキストをURLエンコードする

3.URLエンコードを最初忘れていたせいで、暗号化したテキストにURLに使用できない文字が混じって認証に失敗したりしました。

ユーザーがURLリンクをクリックした時の動作としては、URLから本登録画面に遷移した後、WebAPIを呼び出して暗号化したテキストをデコードして元のメールアドレスに戻し、DBを参照してアカウントを特定するという流れにしています。

言いたいこと

完全に霊感で作ったので、その実装ヤバくね?みたいなやつがあったらコソッと教えてください。まだ間に合います

【Vue.js】Nuxt.jsのひな形を作成する

Nuxt.jsに入門したいので、ひな形の作成からはじめました。

バージョン情報

  • npm 5.6.0
  • yarn 1.5.1

手順

ターミナルを開いて、下記のコマンド実行する。

$ npm i -g @vue/cli @vue/cli-init

Vue CLIが正しくインストールされていることを確認する。

$ vue -V
3.0.3

vue initコマンドでVue.jsのひな形を作成する。
vue initコマンド後に色々と聞かれるが、特にこだわりがなければ全部EnterでOK。

$ vue init nuxt-community/starter-template <project-name>

上記のコマンドでは、nuxt-community/starter-templateというスターターテンプレートを使用して、Vue.jsのプロジェクトを作成している。

$ cd <project-name>
$ yarn # npm iでも可、パッケージのインストールを行なっている
$ yarn dev # npm run dev でも可

上記のコマンド実行後に、ブラウザでhttp://localhost:3000を開くと下記のNuxt.jsのデフォルト画面が表示される。

f:id:l08084:20181021171754p:plain
Nuxt.jsのデフォルトの画面

参考サイト

インストール - NuxtJS

【JavaScript】return なしでもPromiseはメソッドチェーンできる

はじめに

then関数に新しいPromiseオブジェクトを返す機能があるので、then内でPromiseをreturnする処理を書かなくても、Promiseはメソッドチェーンが可能だということを最近知ったのでメモ。

Promiseチェーンの色々な例

はじめにで述べたことを理解するために、色々なタイプのPromiseをメソッドチェーンで繋ぎます

returnなし

then()内でreturnしない場合でも、thenが新しいPromiseを返してくれるのでメソッドチェーンが可能です

// new Promise(resolve => resolve()) と同じ
const promise = Promise.resolve();

// 実行結果
//    A
//    B
//    C
promise.then(console.log('A'))
       .then(console.log('B'))
       .then(console.log('C'));

returnあり

チェーンの次の処理に前の処理の結果を渡したい場合は、then()内に渡したい値をreturnする処理を書く必要があります。
returnした値はthenの機能でPromiseオブジェクトに変換されるので、数値や文字列だけでなく、オブジェクトでもPromiseでもどの値を返しても、Promiseはメソッドチェーンが可能です

数値を伝搬するPromiseチェーン

数値をreturnで次の処理に渡しています。returnした数値はthenの機能でPromiseオブジェクトに変換して渡されます

// new Promise(resolve => resolve()) と同じ
const promise = Promise.resolve();

// 実行結果
// 6
// arrow functionを使っているのでreturnは省略されているが、
// 実際には数値がreturnされている
promise.then(() => 1)
       .then(value => value + 2)
       .then(value => value + 3)
       .then(value => console.log(value));
Promiseを伝搬するPromiseチェーン

PromiseをPromiseチェーンを使って次の処理に渡すこともできます。
次の例では、戻り値がPromiseのサードパーティ製ライブラリのメソッドをreturnしています。

// 戻り値がPromiseのメソッド
this.keychainTouchId.isAvailable()
    .then((res: any) => {
        // 戻り値がPromiseのメソッド
        return this.keychainTouchId.has(BioAuthService.KEY_A);
    }).then((res: any) => {
        // 戻り値がPromiseのメソッド
        return this.keychainTouchId
            .verify(BioAuthService.KEYCHAIN_KEY, `ロックを解除してください`);
    }).then((res: any) => {
        this.password = res;
        // 戻り値がPromiseのメソッド
        return this.storage.get(BioAuthService.KEY_B);
    }).then((res: any) => {
        this.userId = res;
        const params = {
            loginId: this.userId,
            password: this.password,
            deviceToken: null
        };
        this.action.login(params);
    }).catch((error: any) => {
        // catchは一つでよい
        console.error(error);
    });

結論

何も考えずにthenでつないどけば、Promiseはメソッドチェーンが可能

参考サイト

JavaScript Promiseの本

Promiseを使う - JavaScript | MDN

HTMLメールのCSSなどレイアウト設定について

はじめに

最近仕事でHTMLメールのレイアウトを設定する機会があったので、HTMLメールを作成するときの注意点や参考サイトなどをまとめました

ベースになるテンプレート

HTMLメールを作るのが初めての人は、そもそもHTMLメールのHTMLってどう書けばいいの?ってなると思うんですが、それについてはGmailの公式サイトの方で説明してくれています。

CSS Support  |  Gmail Sender Resources  |  Google Developers

  • Gmail公式が紹介しているHTMLメールのテンプレート
<html>
  <head>
    <style>
      .colored {
        color: blue;
      }
      #body {
        font-size: 14px;
      }
    </style>
  </head>
  <body>
    <div id='body'>
      <p>Hi Pierce,</p>
      <p class='colored'>This text is blue.</p>
      <p>Jerry</p>
    </div>
  </body>
</html>

Gmailについては上記のテンプレートでいいですが、メールクライアントによっては、インラインCSSでないとダメなクライアントもあると思います(調べてませんが)。
そのようなメールクライアントへの対応が必要な場合は、下記のようなCSSをインラインCSSに変換するサービスを使うと便利です。

CSSをインラインCSSに変換してくれるWebサービス「Inline styler」 | ネットショップ運営の気になる備忘録

メールクライアントごとの違い

メールクライアント(Gmail, Outlook, Yahoo!メール...)ごとに、使用できるCSSやHTMLタグが大きく異なるため特に注意が必要です。

HTMLメールのレイアウトを検証できるサービス

メールアドレスとHTMLメールのテンプレートを入力するとメールを送信してくれるサービスもあります。メールを送信する環境を構築しなくてもレイアウトの確認ができるので便利

New Email Test — Litmus PutsMail

【HTML】押すと電話がかかるボタンを作る

はじめに

押すと、電話がかかるボタンを作りたい

TELリンクを使う

a要素href="tel:[電話番号]"を設定すると、電話発信用のリンクを作ることができる。

<a href="tel:090XXXXXXXX">電話をかける(090XXXXXXXX)</a>

f:id:l08084:20180901153652p:plain
実際の表示

TELリンクをボタン風に表示する

TELリンクを設定したa要素(アンカータグ)でボタンを囲むことによって、クリックすると、電話を発信するボタンが完成する

<a href="tel:090XXXXXXXX">
  <button ion-button>電話をかける(090XXXXXXXX)</button>
</a>

f:id:l08084:20180901154743p:plain
実際の表示

PCでこのボタンを押すと、FaceTimeだったりSkypeだったりを呼び出してくれる

f:id:l08084:20180901154901p:plain

参考サイト

HTML5/テキスト/a要素 電話発信用のリンクを設定する - TAG index