中安拓也のブログ

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

【Java8】正規表現で半角数字だけの文字列を特定する

はじめに

Javaの正規表現を使って、半角数字だけで構成された文字列かどうかをチェックする方法について説明します。

環境

  • java version "1.8.0_45"

文字列が正規表現のパターンに適合するかチェックする

半角数字を1文字以上の(小数点やマイナス記号も含まない)正規表現([0-9]+)を使って、半角数字だけの文字列を特定していきます。

App.java

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class App {
    public static void main(String[] args) throws Exception {
        // (1) 正規表現のパターンを生成
        Pattern pattern = Pattern.compile("[0-9]+");

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence1 = "12";

        // (3) 正規表現処理をおこなうためのクラスを取得
        Matcher matcher1 = pattern.matcher(sentence1);

        // (4) 正規表現のパターンに適合するかチェック
        System.out.println(matcher1.matches());

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence2 = "1a2";

        // (3) 正規表現処理をおこなうためのクラスを取得
        Matcher matcher2 = pattern.matcher(sentence2);

        // (4) 正規表現のパターンに適合するかチェック
        System.out.println(matcher2.matches());

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence3 = "1.0";

        // (3) 正規表現処理をおこなうためのクラスを取得
        Matcher matcher3 = pattern.matcher(sentence3);

        // (4) 正規表現のパターンに適合するかチェック
        System.out.println(matcher3.matches());

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence4 = "-1";

        // (3) 正規表現処理をおこなうためのクラスを取得
        Matcher matcher4 = pattern.matcher(sentence4);

        // (4) 正規表現のパターンに適合するかチェック
        System.out.println(matcher4.matches());

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence5 = "485617";

        // (3) 正規表現処理をおこなうためのクラスを取得
        Matcher matcher5 = pattern.matcher(sentence5);

        // (4) 正規表現のパターンに適合するかチェック
        System.out.println(matcher5.matches());
    }
}

実行結果

true
false
false
false
true

処理の流れとしては、下記のようになります。

  1. 正規表現のパターンを生成(Pattern pattern = Pattern.compile("[0-9]+"))
  2. 正規表現のパターンに適合するかをチェックする文字列を定義(String sentence1 = "12";)
  3. 正規表現処理をおこなうためのクラスを取得(Matcher matcher1 = pattern.matcher(sentence1);)
  4. 正規表現のパターンに適合するかチェック(System.out.println(matcher3.matches());)

もっと簡単に正規表現を使う

先ほどの例と同じ処理をPatternクラス、Matcherクラスを使用せずに実施することもできます。

App.java

public class App {
    public static void main(String[] args) throws Exception {
        // (1) 正規表現の文字列を定義
        String regex = "[0-9]+";

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence1 = "12";

        // (3) 正規表現のパターンに適合するかチェック
        System.out.println(sentence1.matches(regex));

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence2 = "1a2";

        // (3) 正規表現のパターンに適合するかチェック
        System.out.println(sentence2.matches(regex));

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence3 = "1.0";

        // (3) 正規表現のパターンに適合するかチェック
        System.out.println(sentence3.matches(regex));

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence4 = "-1";

        // (3) 正規表現のパターンに適合するかチェック
        System.out.println(sentence4.matches(regex));

        // (2) 正規表現のパターンに適合するかをチェックする文字列を定義
        String sentence5 = "485617";

        // (3) 正規表現のパターンに適合するかチェック
        System.out.println(sentence5.matches(regex));
    }
}

実行結果

true
false
false
false
true

Stringクラスのmatchesメソッドを使うことで最初の例よりも短い処理で正規表現が実装できていることがわかります。

Stringクラスのmatchesメソッドを使うことでPatternクラス、Matcherクラス使う場合よりも簡単に正規表現の処理を実装することができますが、デメリットとしてStringクラスのmatchesメソッドを使うと内部でその度にPatternクラスやMatcherクラスを生成するため、何度も繰り返して処理をするような場合だと動作が遅くなってしまいます。

そのため、大量の文字列を繰り返し処理する必要があるような場合は、Patternクラス、Matcherクラスを使って正規表現の処理を実装したほうが良いでしょう。

参考文献

【Angular】クロスフィールドバリデーションを使用した時にフォーム全体をエラーにする

はじめに

Angularでクロスフィールドバリデーション(複数項目にまたがるバリデーション )を使った時に、フォーム全体をエラーにする方法を説明します。

なお、クロスフィールドバリデーションでフォームのなかのある項目のみをエラーにする方法についてはこちらの記事をご参照ください。

環境

SPAフレームワークのAngular/TypeScriptを使用しています。

  • Angular: 8.2.14
  • Node: 12.13.1

やりたいこと

本記事では、タイトル or 本文 のどちらかの項目に文字を入力しないと保存ボタンが有効にならないバリデーションを実装します。

f:id:l08084:20200513092345p:plain
タイトルか本文に何か入力しないと保存できない

f:id:l08084:20200513092853p:plain
本文に文字を入力したので保存ボタンが有効になる

バリデーションの実装

HTMLテンプレート

まず、HTMLテンプレートの実装について説明します。HTMLでは、タイトル欄、本文欄、保存/更新ボタンを定義しています。

upsert-form.component.html

<form class="example-form" (ngSubmit)="onSubmit(upsertNgForm)" [formGroup]="createFormGroup" #upsertNgForm="ngForm">
  <!-- タイトル欄 -->
  <mat-form-field class="example-full-width">
    <input matInput class="title" placeholder="タイトル" formControlName="title" />
  </mat-form-field>
  <!-- 本文欄 -->
  <mat-form-field class="example-full-width">
    <textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="10" cdkAutosizeMaxRows="30" placeholder="本文"
      formControlName="description"></textarea>
  </mat-form-field>
  <!-- 保存/更新ボタン -->
  <button type="submit" class="login-button" mat-raised-button [disabled]="!createFormGroup.valid" color="primary">
    <ng-container *ngIf="selectedMemoId; else createLabel">更新</ng-container>
    <ng-template #createLabel>保存</ng-template>
  </button>
</form>

ポイントとしては、[disabled]="!createFormGroup.valid"と記載することで、FormGroup内でバリデーションエラーが発生している時には、ボタンが非活性になるようにしているところです。

コンポーネントクラス

続いて、コンポーネントクラスの実装について説明します。コンポーネントクラスでは、フォームグループ、フォームコントロールの設定とバリデーションの設定をしています。

upsert-form.component.ts

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

/**
 * メモの新規作成・更新フォーム
 *
 * @export
 * @class UpsertFormComponent
 * @implements {OnInit}
 * @implements {OnChanges}
 */
@Component({
  selector: 'app-upsert-form',
  templateUrl: './upsert-form.component.html',
  styleUrls: ['./upsert-form.component.scss']
})
export class UpsertFormComponent implements OnInit, OnChanges {
  // FormGroup定義
  public createFormGroup: FormGroup;
  // タイトルフォームのコントロール定義
  public titleControl: FormControl;
  // 本文フォームのコントロール定義
  public descriptionControl: FormControl;

  constructor(private fb: FormBuilder) {
    this.createForm();
    this.folderControl = this.createFormGroup.get('folder') as FormControl;
    this.titleControl = this.createFormGroup.get('title') as FormControl;
    this.descriptionControl = this.createFormGroup.get(
      'description'
    ) as FormControl;
  }

  /**
   * フォーム設定の作成
   *
   */
  private createForm() {
    this.createFormGroup = this.fb.group(
      {
        title: ['', []],
        description: ['', []]
      },
      {
        validators: CustomValidator.titleOrDescriptionRequired
      }
    );
  }
}

フォームグループとタイトルと本文のどちらかを必須にするバリデーション を設定している部分です。

    this.createFormGroup = this.fb.group(
      {
        title: ['', []],
        description: ['', []]
      },
      {
        validators: CustomValidator.titleOrDescriptionRequired
      }
    );

validators: CustomValidator.titleOrDescriptionRequiredという風に書くことで、クロスフィールドバリデーション (項目をまたがったバリデーション )を実装することができます。

カスタムバリデーター

最後にタイトルまたは本文のどちらかを入力しないとバリデーションエラーを返すバリデーターの実装について説明します。

custom-validator.ts

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

  /**
   * 「タイトルと本文のどちらかは必須」バリデーション
   *
   * @static
   * @param {*} ac
   * @param {*} AbstractControl
   * @memberof CustomValidator
   */
  public static titleOrDescriptionRequired(
    control: FormGroup
  ): ValidationErrors | null {
    const title = control.get('title').value;
    const description = control.get('description').value;
    return !title && !description
      ? { notTitleOrDescriptionRequired: true }
      : null;
  }
}

タイトルと本文の値をそれぞれ取得して、どちらも未入力の場合は、{ notTitleOrDescriptionRequired: true }を返しています。このようにすることで、ValidationErrorsを返されたFormGouptinvalidな状態になり、保存ボタンが非活性になります。

参考サイト

https://angular.jp/guide/form-validation

【Angular】自作コンポーネントにngModelを使用する

はじめに

自作コンポーネントで双方向バインディング機能(ngModel)を使えるようにしたい。

環境

CSSフレームワークとしてAngular Materialを使用しています

  • Angular: 8.2.14
  • Node: 12.13.1
  • Angular Material: 8.2.3

今回作るもの

双方向バインディング機能([(ngModel)])を持った分数入力用のコンポーネントを作成します。

f:id:l08084:20200517222300p:plain
分数入力コンポーネント

分数入力コンポーネントの実装

カスタムコンポーネントに[()]構文を使用するには、@Inputプロパティx@OutputプロパティxChangeを実装する必要があります。

fraction-input.component.ts

import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core';
import { FormGroup, FormControl, FormBuilder } from '@angular/forms';
import { Fraction } from 'src/app/entity/fraction';

/**
 * 分数入力用コンポーネント
 *
 * @export
 * @class FractionInputComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-fraction-input',
  templateUrl: './fraction-input.component.html',
  styleUrls: ['./fraction-input.component.scss'],
})
export class FractionInputComponent implements OnInit {
  @Input() fraction: Fraction = new Fraction();
  @Output() fractionChange = new EventEmitter<Fraction>();

  public fractionFormGroup: FormGroup;
  // 分子
  public numeratorControl: FormControl;
  // 分母
  public denominatorControl: FormControl;

  constructor(private fb: FormBuilder) {}

  public ngOnInit() {
    this.createForm();
    this.numeratorControl = this.fractionFormGroup.get(
      'numerator'
    ) as FormControl;
    this.denominatorControl = this.fractionFormGroup.get(
      'denominator'
    ) as FormControl;
  }

  /**
   * reactive formの設定
   *
   * @private
   * @memberof FractionInputComponent
   */
  private createForm() {
    this.fractionFormGroup = this.fb.group({
      numerator: ['', []],
      denominator: ['', []],
    });
  }
}

fraction-input.component.html

<form [formGroup]="fractionFormGroup">
  <div class="fraction-group">
    <!-- 分子 -->
    <mat-form-field>
      <input matInput class="input" placeholder="分子" formControlName="numerator" [(ngModel)]="fraction.numerator">
    </mat-form-field>
    <div class="split">/</div>
    <!-- 分母 -->
    <mat-form-field>
      <input matInput class="input" placeholder="分母" formControlName="denominator" [(ngModel)]="fraction.denominator">
    </mat-form-field>
  </div>
</form>

app.component.html

<div class="wrapper">
  <mat-card class="form-card">
    <div>分数</div>
    <!-- 分数コンポーネント -->
    <app-fraction-input [(fraction)]="fraction"></app-fraction-input>
    {{fraction | json}}
  </mat-card>
</div>

FractionInputComponent@Input() fraction@Output() fractionChangeを実装することでAppComponentとの間に双方向バインディングを実現しています([(fraction)])。

なお、AppComponentFractionInputComponent間ではFractionオブジェクトを双方向にバインドしています。

fraction.ts

/**
 * 分数オブジェクト
 *
 * @export
 * @class Fraction
 */
export class Fraction {
  // 分子
  public numerator: number;
  // 分母
  public denominator: number;

  constructor() {
    this.numerator = undefined;
    this.denominator = undefined;
  }
}

作成したコンポーネントを動作させると次のキャプチャーのようになり、子コンポーネントに入力した値が双方向バインディングによって親コンポーネントに反映されていることがわかります。

f:id:l08084:20200517225149p:plain
AppComponentの動作例: 双方向バインディングが機能していることがわかる

参考サイト

双方向バインディング(ngModel)対応のComponentを自作する - Qiita

[Angular] カスタムコンポーネント(Custom Component)で ngModel を使う - Qiita

How to create custom input component with ngModel working in angular 6? - Stack Overflow

https://angular.jp/guide/template-syntax#ngmodel-%E3%81%A8%E5%80%A4%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B5

https://angular.jp/guide/template-syntax#%E5%8F%8C%E6%96%B9%E5%90%91%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0-

window.open()で開いたウィンドウにデータを渡す

はじめに

window.open()で開いたウィンドウに親ウィンドウからデータを渡す処理を仕事で書いた時に色々と苦労したので、備忘録としてプライベートでも似たような実装をしてみました。

環境

SPAフレームワークのAngular/TypeScriptを使用しています。

  • Angular: 8.2.14
  • Node: 12.13.1
  • TypeScript: 3.5.3

今回やりたいこと

1.あるウィンドウからwindow.open()で別のウィンドウを開く

f:id:l08084:20210130184507p:plain
親ウィンドウからwindow.open()で子ウィンドウを開く

2.親ウィンドウから子ウィンドウにデータを渡す

f:id:l08084:20210130184551p:plain
親ウィンドウから子ウィンドウにデータを渡す

今回の記事では、2. の親ウィンドウから子ウィンドウへの値の渡し方についてフォーカスを当てて説明します。1. のwindow.open()で別ウィンドウを開く実装については、こちらの記事をご参照ください。

ウィンドウにデータを渡す二つの方法

ウィンドウにデータを渡す方法についてですが、本記事では下記の二つの方法について説明します。

1. ローカルストレージを用いる方法

ブラウザにデータを保存する技術の一つであるローカルストレージ(localStorage)を使用してウィンドウ間でデータの受け渡しをします。

f:id:l08084:20210130184639p:plain
localStorageを経由することでウィンドウを跨いでデータを連携する

2. window.postMessage()を用いる方法

Windowオブジェクト間で通信する技術の一つであるwindow.postMessage()を使用して、親ウィンドウから子ウィンドウにデータを送信します。

f:id:l08084:20210130184851p:plain
window.postMessage()でデータを送信する

ウィンドウにデータを渡す実装

ローカルストレージを採用した場合とwindow.postMessage()を採用した場合の二つに分けてwindow.open()で開いたウィンドウにデータを渡す処理の実装について説明していきます。

今回は下記のオブジェクトを親ウィンドウから子ウィンドウに渡します。

export class Message {
  id: number;
  description: string;
}

ローカルストレージを採用した場合

localStorageを使用して、親ウィンドウから子ウィンドウにデータ(オブジェクト)を渡す処理を実装します。

親ウィンドウの実装

親ウィンドウでは、MessageオブジェクトをlocalStorageに格納した後、window.open()で子ウィンドウを開いています。

// 子ウィンドウに渡したいデータを作成
const message = new Message();
message.id = 5;
message.description = 'データ受信成功';

// ローカルストレージにデータを格納します
localStorage.setItem('key', JSON.stringify(message));

// 子ウィンドウを開く
const rootPath = window.location.origin + window.location.pathname;
window.open(rootPath + 'matrix', '_blank', 'location=no,scrollbars=yes');

localStorageにはstring型のデータしか格納できないため、JSON.stringify(message)とすることによってオブジェクトからJSON型文字列に変換してからlocalStorageに格納しています。

子ウィンドウの実装

子ウィンドウでは、親ウィンドウで格納したデータをlocalStorage.getItem()で取り出すことによってデータを受け取っています。

// ローカルストレージ経由でデータを受け取る
const message: Message = JSON.parse(localStorage.getItem('key'));
this.id = message.id;
this.description = message.description;

JSON.parse()を使用することで、JSON型文字列からオブジェクトに変換しています。

window.postMessage()を採用した場合

続いて、window.postMessage()を使用してウィンドウ間でデータを渡す場合の実装について解説していきます。

親ウィンドウの実装

親ウィンドウでは、子ウィンドウを開いた後に、window.postMessage()でデータを送信しています。

// 子ウィンドウに渡したいデータを作成
const message = new Message();
message.id = 5;
message.description = 'データ受信成功';

// 子ウィンドウを開く
const rootPath = window.location.origin + window.location.pathname;
const popup = window.open(
  rootPath + 'matrix',
  '_blank',
  'location=no,scrollbars=yes'
);

// 子ウィンドウにメッセージを送信
popup.onload = () => popup.postMessage(message, window.location.origin);

ポイントとしては、window.onloadイベントハンドラ内でwindow.postMessage()を呼び出すことによって、子ウィンドウが開く前にpostMessageが送信されることを防いている部分です。

注意点:IEブラウザを使う場合

window.postMessage()でデータを送信している処理popup.onload = () => popup.postMessage(message, window.location.origin);ですが、IEブラウザでは正常に動作しません。下記のコードに書き換える必要があります。

popup[popup.addEventListener ? 'addEventListener' : 'attachEvent'](
  // ビルドエラー防止のため、例外的に文字列によるプロパティアクセスを使用している
  // tslint:disable-next-line:no-string-literal
  (popup['attachEvent'] ? 'on' : '') + 'load',
  () => popup.postMessage(message, window.location.origin),
  false
);
子ウィンドウの実装

子ウィンドウでは、window.addEventListener('message', (event) => {を使用することで、受信したメッセージを受け取っています。

なお、if (event.origin === window.location.originの部分では、メッセージ送信元のオリジンを確認することで、不正なメッセージの受信を防いでいます。今回は同一オリジン間でのメッセージのやり取りなので、送信元オリジンがwindow.location.originと一致していれば、OKとしています。

Angular以外の場合

    window.addEventListener('message', (event) => {
      if (event.origin === window.location.origin && event.data.id) {
        const message: Message = event.data;
        this.id = message.id;
        this.description = message.description;
      }
    });

受信したメッセージは、event.dataの部分になります。

Angularの場合

Angularを使用している場合は、window.addEventListener@HostListenerに書き換えることができます。

  @HostListener('window:message', ['$event'])
  public onPostMessage(event) {
    if (event.origin === window.location.origin && event.data.id) {
      const message: Message = event.data;
      this.id = message.id;
      this.description = message.description;
    }
  }

参考サイト

javascript - Problems with window.postMessage on Chrome - Stack Overflow

https://qrunch.net/@tercel/entries/hazZdGPlAaoAHGWD

javascript - Detecting the onload event of a window opened with window.open - Stack Overflow

Angular(SPA)でwindow.open()を使う

f:id:l08084:20210130180431p:plain

はじめに

仕事でAngularを使ってアプリケーションを作成している時に、window.open()で別ウィンドウを表示する要件があったので、備忘録としてプライデートでも似たような実装をしてみました。

環境

SPAフレームワークのAngularを使用しています。

  • Angular: 8.2.14
  • Node: 12.13.1

作ったもの

サンプルとして、親ウィンドウの「マトリクス図を開く」ボタンを押下すると新規ウィンドウとしてマトリクス画面(子ウィンドウ)を開くアプリケーションを作りました。

f:id:l08084:20200503182208p:plain
親ウィンドウ

f:id:l08084:20210130180629p:plain
子ウィンドウ

実装

サンプルアプリケーションの実装について順を追って説明していきます。

ルーティング設定

まずルーティングの設定から説明します。親(top)、子(matrix)のルーティングを下記の通り設定しました。

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { TopComponent } from './component/top/top.component';
import { MatrixComponent } from './component/matrix/matrix.component';

const routes: Routes = [
  { path: '', component: TopComponent, pathMatch: 'full' },
  { path: 'top', redirectTo: '' },
  { path: 'matrix', component: MatrixComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

親ウィンドウの実装

親ウィンドウの実装について説明します。ボタンを押下した時に呼び出されるopenMatrix()メソッドで、window.open()に子ウィンドウのルーティングURLを設定して別ウィンドウとして子ウィンドウであるマトリクス画面を表示しています。

top.component.html

<div class="wrapper">
  <button (click)="openMatrix()">マトリクス図を開く</button>
</div>

top.component.ts

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

/**
 * 親ウィンドウコンポーネント
 *
 * @export
 * @class TopComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-top',
  templateUrl: './top.component.html',
  styleUrls: ['./top.component.scss'],
})
export class TopComponent implements OnInit {
  constructor() {}

  ngOnInit() {}

  /**
   * 別ウィンドウでマトリックス画面を開く
   *
   * @memberof TopComponent
   */
  public openMatrix() {
    const rootPath = window.location.origin + window.location.pathname;
    window.open(rootPath + 'matrix', '_blank', 'location=no,scrollbars=yes');
  }
}

子ウィンドウの実装

子ウィンドウ(マトリクス画面)については、特別な実装が必要ないので説明を省略します。

データの受け渡しについて

本サンプルアプリケーションでは、親ウィンドウから子ウィンドウへのデータの受け渡しは実施していませんが、仕事で作ったアプリケーションではlocalStorageを使って、データの受け渡しを実現しました。

実装方法としては、親ウィンドウ側でlocalStorage.setItem()でデータを格納し、子ウィンドウ側でlocalStorage.getItem()でデータを取得することで親ウィンドウからデータを受け取るといった感じで実装しています。

参考サイト

Location - Web API | MDN

https://qrunch.net/@tercel/entries/hazZdGPlAaoAHGWD

HTMLとCSSでマトリックス図を描く

f:id:l08084:20210130175314p:plain
作成したマトリクス図

はじめに

仕事でCSSフレームワークを使わずにテーブルを作成する機会があったので、備忘録としてプライベートでもマトリクス図を作ってみました。

環境

SPAフレームワークのAngularを使用しています。

  • Angular: 8.2.14
  • Node: 12.13.1

マトリクス図の作成

サンプルとして、年収と年齢から、その人がお客さんになってくれそうかどうかを判定するマトリクス図を作ってみます(トップの画像が完成図)。

CSSリセット

デフォルトで<table>タグを使うと、セルとセルの間に余白ができてしまうので、下記のCSSを追加してセルの隙間を消します。

table {
  border-collapse: collapse;
  border-spacing: 0;
}

マトリクス図の全コード

完成したマトリクス図の全コードになります。Angualrフレームワークを使用しない場合は、matrix.component.tsは必要ないので無視してください。 レイアウトの設定はSCSSを使用しています。

matrix.component.html

<div class="wrapper">
  <!-- 顧客分類マトリクス -->
  <div class="title-wrapper">
    <!-- テーブルのタイトル -->
    <div class="group-title">顧客分類マトリクス</div>
    <!-- 凡例 -->
    <div class="usage-guide">
      <div class="item space">
        <div class="square origin"></div>
        <div class="text"> : 初回入力値</div>
      </div>
      <div class="item">
        <div class="square adjust"></div>
        <div class="text"> : 再入力値</div>
      </div>
    </div>
  </div>
  <!-- マトリクス図 -->
  <table class="matrix" border="1">
    <thead>
      <tr>
        <th colspan="2" rowspan="2"></th>
        <th class="repayment-ratio" colspan="5">年齢</th>
      </tr>
      <tr>
        <th class="percent">〜30</th>
        <th class="percent">〜35</th>
        <th class="percent">〜40</th>
        <th class="percent">〜45</th>
        <th class="percent">45超</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th class="total-income" rowspan="3">年収
        </th>
        <th class="income">250万円以上<br>400万円未満</th>
        <td>A</td>
        <td>A</td>
        <td class="not-lend adjust">X</td>
        <td class="not-lend">X</td>
        <td class="right-edge not-lend">X</td>
      </tr>
      <tr>
        <th class="income">400万円以上<br>500万円未満</th>
        <td>A</td>
        <td>A</td>
        <td>A</td>
        <td class="not-lend">X</td>
        <td class="right-edge not-lend">X</td>
      </tr>
      <tr>
        <th class="income">500万以上</th>
        <td class="bottom">
          A</td>
        <td class="bottom origin">
          B</td>
        <td class="bottom">
          B</td>
        <td class="bottom">
          B</td>
        <td class="bottom right-edge not-lend">
          X</td>
      </tr>
    </tbody>
  </table>
  <!-- 見込みテーブル -->
  <table class="example" border="1">
    <tbody>
      <tr>
        <th>見込み</th>
        <td>
          <div>
            A
            <span class="text">
              :一般客
            </span>
          </div>
          <div>
            B
            <span class="text">
              お得意様
            </span>
          </div>
          <div>
            <span class="not-lend">X</span>
            <span class="text">
              :マーケティング対象外
            </span>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

matrix.component.scss

.wrapper {
  align-items: center;
  display: flex;
  flex-direction: column;
  padding: 48px;

  .title-wrapper {
    display: flex;
    width: 364px;

    .group-title {
      font-size: 16px;
      font-weight: bold;
      letter-spacing: .2px;
    }

    .usage-guide {
      align-items: center;
      display: flex;
      font-weight: normal;
      justify-content: flex-end;
      width: 184px;

      &.valuation {
        width: 265px;
      }

      .item {
        display: flex;

        .square {
          margin: 2px;

          &.origin {
            height: 12px;
            width: 12px;
            background-color: rgba(84, 141, 255, .25);
            border: solid 1px #a3a3a3;
          }

          &.adjust {
            height: 10px;
            width: 10px;
            border: solid 3px #548DFF;
          }
        }

        .text {
          font-size: 11px;
          margin-left: 2px;
        }
      }

      .space {
        margin-right: 8px;
      }
    }
  }

  table {

    &.matrix,
    &.valuation {
      border: solid 1px #545454;
      font-size: 10px;
      height: 160px;
      margin: 16px 0;

      th {
        background: #e6e6e6;
        border: solid 1px #545454;
        text-align: center;

        &.total-income {
          padding: 5px;
          width: 30px;
        }

        &.income {
          height: 36px;
          width: 80px;
        }

        &.repayment-ratio,
        &.percent {
          height: 28px;
        }
      }

      td {
        border: solid 1px #a3a3a3;
        height: 36px;
        text-align: center;
        width: 52px;

        &.bottom {
          border-bottom: solid 1px #545454;
        }

        &.right-edge {
          border-right: solid 1px #545454;
        }

        &.origin {
          background-color: rgba(84, 141, 255, .25);
        }

        &.adjust {
          border: solid 3px #548DFF;
        }
      }
    }

    &.example {
      border: solid 1px #545454;
      font-size: 10px;
      height: 22px;
      margin-bottom: 40px;

      th {
        text-align: center;
        width: 69px;
      }

      td {
        align-items: center;
        border: 0;
        display: flex;
        font-size: 11px;
        height: 100%;
        justify-content: space-around;
        padding: 0 12px;
        width: 299.8px;

        .text {
          font-weight: normal;
        }
      }

      &.interest-rate {
        margin-top: 16px;

        th {
          width: 109px;
        }

        td {
          padding: 0 26px;
          width: 259.8px;
        }
      }
    }

    &.valuation {
      margin: 4px 0;
    }
  }

  .not-lend {
    color: #a3a3a3;
  }

  .table-title {
    font-size: 14px;
    margin-top: 16px;
    text-align: left;
    width: 364px;
  }
}

matrix.component.ts

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

@Component({
  selector: 'app-matrix',
  templateUrl: './matrix.component.html',
  styleUrls: ['./matrix.component.scss'],
})
export class MatrixComponent {
  constructor() {}
}

参考サイト

flexboxを使った中央寄せについて - Qiita

CSSで作図する - Qiita

Angular + Firebase でFacebook認証

はじめに

AngularとFirebaseを使用して、Facebook認証機能を実装します。

なお、AngularとFirebaseによるメールアドレス/パスワードの認証とTwitter認証は実装ずみで、下記の記事で説明もしています。

  • メールアドレス/パスワードの認証

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

AngularでFirebase認証(その2) Angular Materialを使ったログイン画面の作成 - 中安拓也のブログ

AngularでFirebase認証(その3) Firebase Authentication の呼び出し - 中安拓也のブログ

Angular + Firebase でアカウント登録画面の作成 - 中安拓也のブログ

  • Twitter認証

Angular + Firebase でTwitter認証 - 中安拓也のブログ

環境

フロントエンドのフレームワークにはAngular/TypeScriptを、CSSフレームワークにはAngular Materialを使用しています。

Firebase関連のライブラリのバージョンは下記となります。

  • Angular CLI@8.3.20
  • Node@12.13.1
  • OS: darwin x64
  • Angular@8.2.14
  • firebase@6.3.4
  • angular/fire@5.2.1

Facebook for Developersアカウントを作成

Facebook認証を実装するにはFacebookのアプリケーション IDとアプリ シークレットを取得する必要があるため、Facebookの開発者向けサイトに移動して、Facebookの開発者向けアカウントを作成します。

f:id:l08084:20200422214931p:plain

アプリの登録

Developersアカウントの作成が完了すると、そのままアプリの作成画面が表示されるのでアプリを作成します。

f:id:l08084:20200422215027p:plain

f:id:l08084:20200422220712p:plain

アプリを作成すると作成したアプリの[設定] > [ベーシック]からアプリケーションIDとアプリシークレットを確認できるようになります。

FirebaseコンソールでFacebook認証を有効にする

Firebaseコンソールに移動してFacebookを認証方法として有効にします。なお、有効にする際には先ほど取得したアプリケーション IDとアプリ シークレットを入力する必要があります。

f:id:l08084:20200422005212p:plain
Facebookを認証方法として有効にする

また、Firebaseコンソールに表示されているコールバックURLもFacebookの開発者向けサイトに登録したアプリに設定する必要があります。

f:id:l08084:20200422221509p:plain
コールバックURLをコピーする

[製品を追加] > [Facebookログイン] > [設定] を選択して、有効なOAuthリダイレクトURI欄にコピーしたコールバックURLを設定します。

f:id:l08084:20200422222651p:plain
コールバックURLを設定する

これで「Firebase構成オブジェクトの転記」が完了してる場合は、Firebase側の設定はすべて完了です。「Firebase構成オブジェクトの転記」が完了していない場合は、下記の記事を参考にしてください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

「Facebookでログイン」ボタンを作成する

続いてFacebookログイン用のボタンを作成していきます。FacebookのアイコンはFont AwesomeのFacebookアイコンを使用します。

f:id:l08084:20200423005855p:plain
「Facebookでログイン」ボタン

Angular Materialを使用しているので、<mat-icon class="sns-icon fab fa-facebook-f"></mat-icon>を使用することでFacebookアイコンをボタンに埋め込むことができます。

下記のようにHTMLとSCSSを記載すればFacebookボタンが完成します。

  • login.component.html
      <button (click)="signInWithFacebook()" class="facebook" mat-raised-button>
        <mat-icon class="sns-icon fab fa-facebook-f"></mat-icon>Facebookでログイン
      </button>
  • login.component.scss
    .sns-icon {
      font-size: 20px;
      position: absolute;
      left: 15px;
      top: 25%;
    }

    .facebook {
      color: #FFF;
      background-color: #3b5998;
      border-color: #3b5998;
      font-weight: bold;
      width: 100%;
      margin-bottom: 10px;
    }

Facebook認証機能の実装

それではFirebaseと@angular/fireライブラリを使用して、Facebook認証機能の実装をやっていきます。@angular/fireライブラリのインストールが完了していない場合は、下記の記事を参考に実施してください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

  • login.component.html
      <button (click)="signInWithFacebook()" class="facebook" mat-raised-button>
        <mat-icon class="sns-icon fab fa-facebook-f"></mat-icon>Facebookでログイン
      </button>
  • login.component.ts
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';

export class LoginComponent implements OnInit {
  constructor(
    private router: Router,
    private authenticationService: AuthenticationService
  ) {}

  /**
   * Facebook認証でログイン
   *
   * @memberof LoginComponent
   */
  public async signInWithFacebook() {
    try {
      await this.authenticationService.signInWithFacebook();
      // ログインに成功したらホーム画面に遷移する
      this.router.navigate(['/home']);
    } catch (error) {
      console.log(error);
    }
  }

}
  • authentication.service.ts
import { AngularFireAuth } from '@angular/fire/auth';
import * as firebase from 'firebase/app';

export class AuthenticationService {
  constructor(public afAuth: AngularFireAuth) {}

  public signInWithFacebook(): Promise<auth.UserCredential> {
    return this.afAuth.auth.signInWithPopup(
      new firebase.auth.FacebookAuthProvider()
    );
  }
}

Facebookの認証APIを呼び出しているのは、下記の部分のコードとなります。

this.afAuth.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider());

上記のソースコードを実装して「Facebookでログイン」ボタンを押下すると、下記のようにポップアップでFacebook認証のページが表示され、Facebook認証に成功するとログインが完了し、ホーム画面に遷移します。

f:id:l08084:20200426165135p:plain
「Facebookでログイン」ボタンを押下するとポップアップが表示される

補足: Facebook認証を利用したアカウント登録機能の実装

f:id:l08084:20200426180116p:plain
アカウント登録画面

Facebook認証を使用して、アカウント登録機能を実装する場合もFacebook認証でログインを実施する場合と全く同じ実装で実現できます(ログインと同様にthis.afAuth.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider());を呼ぶことでアカウント登録も実装できる)。

というのも、Firebaseを使ったFacebook認証を実施した場合、Facebook認証を実施した時にそのユーザーが存在しなければ、Firebaseにアカウントが新規登録されるため、ログインもアカウント登録も同様のFirebase APIを呼び出すことで実現できるからです。

参考サイト

JavaScript で Facebook ログインを使用して認証する  |  Firebase