ふるてつのぶろぐ

福岡在住のエンジニアです。

写真提供:福岡市

今夜は社内AWSもくもく会 - 無料利用枠超え

今日すること

こんにちはふるてつです。
今回は社内 AWS もくもく会の内容です。
とはいえ今日AWSから下記のようなメールが来ました。
どうやら利用無料枠の85%以上を使ってしまったようです。
f:id:tetsufuru:20190522200346p:plain:w400
というわけでどの辺の理由で超えたのかを確認したいと思います。

マイ請求ダッシュボード

まずはマイ請求ダッシュボードを見てみます。
f:id:tetsufuru:20190522202903p:plain:w500
ぱっと見ると利用料は発生していないようです。
少し画面を下げると「無料利用枠サービス」を確認できます。
f:id:tetsufuru:20190522203116p:plain:w500
こちらに書いてありました、残念ながら相当使ってます。
一番多く使用しているのは「1 GB of Amazon Elastic Block Storage snapshot storage」と書いてあります。
次に「30 GB of Amazon Elastic Block Storage in any combination of General Purpose (SSD) or Magnetic」。
そういえばEC2の2台目を作成するときに1台目のsnapshotを作ってました。
こちらを消しておかないとならなかったのですね。
f:id:tetsufuru:20190522202726p:plain:w500

次はEC2インスタンスが2つ、停止はしていたのですが、ディスク代?がかかっていた模様です。
早速2台目は削除しました。
しかし削除しても急には消えないのですね(Terminatedの状態で残ったまま)
f:id:tetsufuru:20190522204417p:plain:w500

なるほど。 いずれにしても全部消すしかなさそうです…

感想

不注意で無料利用枠を使い切ってしまいました。
しまったという感じです。
これまでの振り返りになりますが本は下の写真のここまで行きました。
幅1.5cmくらいですが、ここまでは無料枠でできるぞ、と逆に言えます。
f:id:tetsufuru:20190522205118p:plain:w500
あと半分どうするかは少し考えようと思います。

それではまた

Angular7 で Web アプリを作ろう - Material Icon

今日すること

こんにちは、ふるてつです。
今回のテーマもAngular Materialですが、今日はあまり多くありません。
以前、Angular Materialを Install したときに、Material Iconのところを実施せずに飛ばしていました。
今日は立ち帰ってMaterial Iconのインストールについて書きます。

Material Iconsのインストール

今回行うのは、下のリファレンスでいうところの「Step 6 (Optional): Add Material Icons」に当たります。
f:id:tetsufuru:20190520200128p:plain:w500
https://material.angular.io/guide/getting-started

こちらではindex.htmlに下記を追加せよと解説されています。

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

普通に使う分には良いですけど、これでは外部のCDNに依存することになるので、やはり内部に持っておきたいところです。
そこでnpmコマンドでinstallすることにしました。
コマンドは下記の「Material Icons Guide」を参考にしました。
https://google.github.io/material-design-icons/
リファレンス中の「Material Icons Guide.」リンクをクリックしてもたどり着けます。
f:id:tetsufuru:20190520202246p:plain:w500
上段のあたりにnpmコマンドが書かれています。
f:id:tetsufuru:20190520234446p:plain:w500
$ npm install material-design-icons
Angularにいれるにこのコマンドでは--saveが足りないですね。
そこを足して下記コマンドでinstallします。

npm install --save material-design-icons

そしてもう一つ、リファレンスやガイドからは見つけきれなかったのですが、styles.cssに下記cssのインポートが必要でした。
styles.cssに追加した内容

@import '~material-design-icons/iconfont/material-icons.css';

これはこちらのサイトを参考にしました。
http://neos21.hatenablog.com/entry/2018/11/27/080000

アイコン一覧

使用したいmaterial Iconの一覧は下記URLになります。
https://material.io/tools/icons/?style=baseline
f:id:tetsufuru:20190521001649p:plain:w500
使用したいアイコンをクリックすると画面左側の「Selected Icon」欄に使用方法が表示されます。

使用例

わたしの場合はこのようなアイコンを使用しています。
f:id:tetsufuru:20190521001002p:plain:w100
使用するときのhtmlは、下記のような感じになります。
「account_circle」と「exit_to_app」を使いました。

<button mat-button routerLink="/account-setting" class="btnAccount">
  <mat-icon>account_circle</mat-icon>
</button>

<button mat-button routerLink="/account-setting" class="btnSignOut">
  <mat-icon>exit_to_app</mat-icon>
</button>

今日の感想

今日はMaterial Iconのインストールについてでした。
Material Iconを少し多めに使用すると、なんとなくですが自分が作った画面でも良い感じに思えてきます。

では、今日もお疲れ様でした。

Angular7 で Web アプリを作ろう - mat-menu

今日すること

こんにちは、ふるてつです。
今回のテーマもAngular Materialで、<mat-menu>のお話です。
以前わたしがbootstrapで作っていたメニューと同じものをmaterialで作ろうと思います。
f:id:tetsufuru:20190519232232p:plain:w500
メニューは上記のような感じです(緑色の部分です)

mat-menu関連のリファレンスについて

リファレンスはこちらです。https://material.angular.io/components/menu/overview f:id:tetsufuru:20190519173040p:plain:w500
リファレンスを参考にして作っていきます。
わたしの場合はヘッダー用のコンポーネントを作っていて、その中にまず<mat-toolbar>を配置しています。 そしてその中に<mat-menu>mat-buttonを動的に入れています。
サインアウトなど必ず表示するアイコンなどは固定で配置します。

ヘッダー用のコンポーネントについて

まずヘッダーのhtmlですが、下記のようになります。
header.component.html

<div *ngIf="this.router.url!=='/signIn'">
  <mat-toolbar class="toolbar mat-elevation-z8">
    <mat-toolbar-row class="toolbar">
      <button mat-icon-button (click)="onToggleSidenav()">
        <mat-icon>menu</mat-icon>
      </button>
      <button mat-button routerLink="/account-setting" class="btnMenu">
        <div class="systemName">Sanrokumaru</div>
      </button>

      <div class="toolBarMenu">
        <ng-container *ngFor="let item of availableMenuListDtoLists">
          <button mat-button [matMenuTriggerFor]="appMenu" class="btnMenu">
            <div class="btnMenu">{{ item.propertyId | translate }}</div>
          </button>
          <mat-menu #appMenu="matMenu">
            <ng-container *ngFor="let subitem of item.availableMenuDto">
              <button mat-menu-item routerLink="/{{subitem.apiName}}">
                {{ subitem.propertyId | translate }}
              </button>
            </ng-container>
          </mat-menu>
        </ng-container>
      </div>

      <button mat-button routerLink="/account-setting" class="btnAccount">
        <mat-icon>account_circle</mat-icon>
      </button>

      <button mat-button routerLink="/account-setting" class="btnSignOut">
        <mat-icon>exit_to_app</mat-icon>
      </button>

    </mat-toolbar-row>
  </mat-toolbar>
</div>

順に<mat-icon>menu</mat-icon>はまずsidenav表示用のメニューアイコンです。
(ただし画面サイズが大きい時は表示されません)
次に<div class="systemName">Sanrokumaru</div>はシステム名の部分。
そして<ng-container *ngFor="let item of availableMenuListDtoLists">のところがメニューを動的に作る箇所です。
さらに下の<ng-container *ngFor="let subitem of item.availableMenuDto">のところはサブメニューを作る箇所で、メニューに対してネストして繰り返しています。
残りの<mat-icon>account_circle</mat-icon><mat-icon>exit_to_app</mat-icon>がそれぞれアカウントの設定と、サインアウトボタンです。

次にtsファイルの内容を書きます。
header.component.tsの内容

import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { AccountService } from 'src/app/service/common/account/account.service';
import { AvailableMenuListDto } from 'src/app/entity/dto/available-menu-list-dto';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
  // 親コンポーネントとの連係
  @Output() public sidenavToggle = new EventEmitter();

  // メニュー
  public availableMenuListDtoLists: AvailableMenuListDto[];

  constructor(
    private accountService: AccountService,
    public router: Router
  ) { }

  ngOnInit() {
    // メニューを取得する。
    this.getAvailableMenu();
  }
  /**
   * メニューを取得する。
   */
  private getAvailableMenu(): void {
    this.accountService.getAvailableMenu()
      .subscribe(availableMenuListDtoLists => this.availableMenuListDtoLists = availableMenuListDtoLists);
  }
  /**
   * イベントを発生させる。
   */
  public onToggleSidenav = () => {
    this.sidenavToggle.emit();
  }
}

public availableMenuListDtoLists: AvailableMenuListDto[];がサーバから取得したメニューの内容を格納する変数です。
これは初期表示時にgetAvailableMenu();で取得します。
以前sidenavで使用したものと同じ変数を使用します。https://tetsufuru.hatenablog.com/entry/2019/05/07/212627
public onToggleSidenav = () => {の周辺はsidenavを関連のメソッドです。

レスポンシブ対応

以前作ったsidenavだけで十分役に立つので、あえてメニューはなくても良いかなと思いましたが一旦、bootstrapのように画面サイズが小さい時はsidevanのみを表示しPCなどサイズが大きい時はメニューのみを表示するように切り替えました。
そのあたりの切り替えはわたしはMedia Queriesでおこなっています。

header.component.cssの内容(Media Queriesの部分のみ抜粋)

/* Tablet or PC  */
@media only screen and (min-width: 641px) {
  .mat-icon-button {
    display: none;
  }
}

/* SmartPhone */
@media screen and (max-width: 640px) {
  .toolBarMenu {
    display: none;
  }
}

640px以下の時はメニューが表示されないようにして、641px以上の時はsidenavが表示されないようにしています。

動かしてみる

では動かしてみます。 下のように動的に作った部分やそのサブメニューも出るようになりました。
f:id:tetsufuru:20190520000928p:plain:w500
画面サイズが小さい場合は下記のようにメニューアイコンに切り替わります。
f:id:tetsufuru:20190520001521p:plain:w400
最後にsidenavをクリックすると下記のようにサブメニューが表示されました。
f:id:tetsufuru:20190520002237p:plain:w400

今日の感想

今日は以前書いたsidenavの続きになりますが、mat-menuを表示する方法でした。
今回はそれほど苦労はしませんでした、sidenavで少し慣れていたからと思います。
せっかくマテリアルで作っているので、わざわざbootstrapと同じようなメニュー切替にする必要はなかったのですが、 せっかくsidenabmat-menuの両方を作ったので、一旦はこのまま使ってみようと思います。

では、今日もお疲れ様でした。

Angular7 で Web アプリを作ろう - mat-datepicker 年月のみ版

今日すること

こんにちは、ふるてつです。
今回もAngular Materialです。 mat-datepickerで年月のみのカレンダーを表示する方法について書きました。

mat-datepickerのリファレンスについて

年月だけの「datepicker」の作り方は「datepicker」のリファレンスの上段、「Datepicker emulating a Year and month picker」の所に記述があります。
https://material.angular.io/components/datepicker/overview
f:id:tetsufuru:20190518221605p:plain:w500
サンプルのソースにはhtmltsの例がありますので、それにしたがって書いていきます。
f:id:tetsufuru:20190518222234p:plain:w500

カレンダーのコンポーネントを追加

まずは下記のコマンドでコンポーネントを追加します。

ng generate component ./component/common/date/MatDatepickerMonth

まずhtmlですが、下記のようにリファレンスの内容をほぼそのまま写します。
mat-datepicker-month.component.htmlの内容

<form [formGroup]="mainForm">
  <mat-form-field>
    <input id="yearMonth" matInput [matDatepicker]="dp" placeholder="{{ this.placeholder | translate }}"
      formControlName="yearMonth" (dateInput)="addEvent('input', $event)" (dateChange)="addEvent('change', $event)">
    <mat-datepicker-toggle matSuffix [for]=" dp"></mat-datepicker-toggle>
    <mat-datepicker #dp startView="multi-year" (yearSelected)="chosenYearHandler($event)"
      (monthSelected)="chosenMonthHandler($event, dp)" panelClass="example-month-picker">
    </mat-datepicker>
  </mat-form-field>
</form>

全体を<form [formGroup]="mainForm"></form>で囲んだ点と、placeholderを親画面から指定できるようにした点がリファレンスとは少し違います。
placeholder="{{ this.placeholder| translate }}
あとmatInputを直接手で編集した場合に親画面と連携するためのイベントを追加しました。
(dateInput)="addEvent('input', $event)" (dateChange)="addEvent('change', $event)"

次にtsファイルの内容を書きます。
mat-datepicker-month.component.tsの内容

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FormBuilder, Validators, FormControl } from '@angular/forms';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatDatepicker } from '@angular/material/datepicker';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
// Depending on whether rollup is used, moment needs to be imported differently.
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
// the `default as` syntax.
import * as _moment from 'moment';
// tslint:disable-next-line:no-duplicate-imports
import { default as _rollupMoment, Moment } from 'moment';

const moment = _rollupMoment || _moment;

export const MY_FORMATS = {
  parse: {
    dateInput: 'YYYY/MM',
  },
  display: {
    dateInput: 'YYYY/MM',
    monthYearLabel: 'YYYY MMM',
    dateA11yLabel: 'LL',
    monthYearA11yLabel: 'YYYY MMMM',
  },
};

@Component({
  selector: 'app-mat-datepicker-month',
  templateUrl: './mat-datepicker-month.component.html',
  styleUrls: ['./mat-datepicker-month.component.css'],
  providers: [{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
  { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS }]
})
export class MatDatepickerMonthComponent implements OnInit {
  @Input() locale: String;
  @Input() placeholder: string;
  @Output() event = new EventEmitter();

  yearMonth = new FormControl(moment());
  mainForm = this.formBuilder.group({
    yearMonth: this.yearMonth,
  });

  constructor(
    private formBuilder: FormBuilder,
    private adapter: DateAdapter
  ) { }
  ngOnInit() {
    this.adapter.setLocale(this.locale);
    this.event.emit(this.yearMonth.value);
  }
  chosenYearHandler(normalizedYear: Moment) {
    const ctrlValue = this.yearMonth.value;
    ctrlValue.year(normalizedYear.year());
    this.yearMonth.setValue(ctrlValue);
  }
  chosenMonthHandler(normalizedMonth: Moment, datepicker: MatDatepicker) {
    const ctrlValue = this.yearMonth.value;
    ctrlValue.month(normalizedMonth.month());
    this.yearMonth.setValue(ctrlValue);
    this.event.emit(this.yearMonth.value);
    datepicker.close();
  }
  addEvent(type: string, event: MatDatepickerInputEvent) {
    this.event.emit(this.yearMonth.value);
  }
}

こちらも基本的にリファレンスの内容をそのままうつします。
異なる点は@Input() locale: String;を追加し、他言語化のlocaleを親画面から受け取れるようにしたのと、 @Input() placeholder: string;は名前のとおりplaceholderを親画面から指定できるようにした点です。
あと@Output() event = new EventEmitter<String>();は親画面にイベントを通知するために追加しました。
@Input()は直感的に解るのですが、@Output()はそれ用のメソッドを親子ともに作らないとならなく、ひとくせあるなあという感じです。

親画面との連携

親画面のhtmlは下記のようにしました。
量が多いので呼び出している個所だけ抜粋しました。

<div class="example">
  <app-mat-datepicker-month [locale]='locale' [placeholder]='displayNameMonthFrom'
    (event)="onReceiveEventFromChild($event)">
  </app-mat-datepicker-month>
</div>

次にtsですがこちらも抜粋です。

...
export class OyagamenComponent implements OnInit {
  public monthFrom = new FormControl('', []);
  ...
  public locale: String = 'ja-JP';
  public displayNameMonthFrom: String = 'evaluationResultScreen.monthFrom';

  constructor(
    ...) {
  }
  ngOnInit(
    ...) {
  }
  onReceiveEventFromChild(eventData: String) {
    this.monthFrom.setValue(eventData);
  }
}

public monthFrom = new FormControl('', []);が年月を子画面からうけとる変数です。
public locale: String = 'ja-JP';は子画面にわたすロケール(一旦親画面に直接書きました)
public displayNameMonthFrom: String = 'evaluationResultScreen.monthFrom';は子画面のplaceholderにわたすための変数です。
ちなみに'evaluationResultScreen.monthFrom'は他言語化のためのja.json中の固定値です。
onReceiveEventFromChildは子画面のイベントを受け取ったときに動くメソッドです。
このメソッドがmonthFromに最新の年月をセットします。

動かしてみる

実際に動かしてみると下記のようになります。
まず年を選択します。
f:id:tetsufuru:20190519010213p:plain:w500
年を選択すると次は月
f:id:tetsufuru:20190519010529p:plain:w500
月を選択するとカレンダーが閉じ、「開始年月」という項目にいま選択した年月が設定されました。
f:id:tetsufuru:20190519010836p:plain:w500

今日の感想

今日はmat-datepickerで年月のみのカレンダーを表示する方法でした。
マテリアルのリファレンス通りにすればわりと簡単にできます。
わたしはせっかくなので何度も使えるようコンポーネント化したのですが、そちらの方が少し時間がかかりました。
親画面に@Output()で値を渡すところで、なかなかイベントが伝わらずそこですこし手が止まりました。
一度動くようになったらああこんなものかと思いましたが、動くまでは若干試行錯誤した感じでした。

では、今日もお疲れ様でした。

topcoderの過去問 - ABC

今日すること

こんにちはふるてつです。
今日は Topcoder の過去問です。

問題について

今日の問題は ABC。
引数で数 N と数 K が与えられます。
もとめる文字列は "A"、"B"、"C" 3 種の文字を組合わせて作ります。
引数 N は文字列の桁数です。
引数 K はその文字列の中で "A" ⇒ "B" 、 "B" ⇒ "C" 、"A" ⇒ "C"の順にならぶ組合わせの総数です。
その総数が引数にちょうど一致する文字列を返します。

英語の原文はこちらです


Problem Statement
You are given two s: N and K. Lun the dog is interested in strings that satisfy the following conditions:

The string has exactly N characters, each of which is either 'A', 'B' or 'C'.
The string s has exactly K pairs (i, j) (0 <= i < j <= N-1) such that s[i] < s[j].
If there exists a string that satisfies the conditions, find and return any such string. Otherwise, return an empty string.

Definition
Class: ABC
Method: createString
Parameters: int, int
Returns: String
Method signature: String createString(int N, int K)
(be sure your method is public)
Limits
Time limit (s): 2.000
Memory limit (MB): 256
Constraints
- N will be between 3 and 30, inclusive.
- K will be between 0 and N(N-1)/2, inclusive.
Examples
0)
3
3
Returns: "ABC"
This string has exactly three pairs (i, j) mentioned in the statement: (0, 1), (0, 2) and (1, 2).
1)
3
0
Returns: "CBA"
Please note that there are valid test cases with K = 0.
2)
5
10
Returns: ""
Five characters is too short for this value of K.
3)
15
36
Returns: "CABBACCBAABCBBB"
Please note that this is an example of a solution; other valid solutions will also be accepted.
This problem statement is the exclusive and proprietary property of TopCoder, Inc. Any unauthorized use or reproduction of this information without the prior written consent of TopCoder, Inc. is strictly prohibited. (c)2003, TopCoder, Inc. All rights reserved.

わたしの解答

わたしの解答コードは下記になります。
少し前に AB という問題を解きましたが、それを少し変えただけでできました。
基本的に総当たりで1文字づつ変え条件にあう文字列を地道に探します。

package practice;

public class ABC {

    private static final String CHAR_A = "A";
    private static final String CHAR_B = "B";
    private static final String CHAR_C = "C";

    /**
     *
     * @param N 桁数
     * @param K 一致数
     * @return ABCの組合せ文字列
     */
    public String createString(int N, int K) {

        String abc[] = new String[N];
        setUpAB(N, abc);

        for (int i = 0; i < Math.pow(2, N); i++) {
            System.out.println(String.join("", abc));

            if (countABC(abc) == K) {
                System.out.println("HIT ABC:");
                return String.join("", abc);
            }

            incrementAB(abc, N);
        }
        return "";
    }

    private void setUpAB(int n, String[] abc) {
        for (int i = 0; i < n; i++) {
            abc[i] = CHAR_A;
        }
    }

    private void incrementAB(String[] abc, int n) {
        for (int i = abc.length - 1; i >= 0; i--) {
            if (CHAR_A == abc[i]) {
                abc[i] = CHAR_B;
                return;
            } else if (CHAR_B == abc[i]) {
                abc[i] = CHAR_C;
                return;
            }
            abc[i] = CHAR_A;
        }
    }

    private int countABC(String[] abc) {
        int count = 0;

        for (int i = 0; i < abc.length; i++) {

            for (int j = i + 1; j < abc.length; j++) {

                if (CHAR_A == abc[i] && CHAR_B == abc[j]) {
                    count++;
                } else if (CHAR_A == abc[i] && CHAR_C == abc[j]) {
                    count++;
                } else if (CHAR_B == abc[i] && CHAR_C == abc[j]) {
                    count++;
                }

            }

        }
        System.out.println("count:" + count);

        return count;
    }

}

下はテストコードです。

package test;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;
import practice.ABC;

class ABCTest extends ABC {
    @Test
    void test1() {
        assertThat(createString(3, 3), is("ABC"));
    }
    @Test
    void test2() {
        assertThat(createString(3, 0), is("AAA"));
    }
    @Test
    void test3() {
        assertThat(createString(5, 10), is(""));
    }
    @Test
    void test4() {
        assertThat(createString(15, 36), is("CABBACCBAABCBBB"));
    }
}

今日の感想

今日の問題の難易度は「Hard」でしたが、以前解いた AB の解答を少し変えただけですぐにできました。
とはいえこの問題1000ポイントのところ、わたしのコードでは300点しか取れませんでした。
もっと点の高い回答があったに違いないと感じています。
しかし今日もいい頭の体操になりました。

ではまた。

Angular7 で Web アプリを作ろう - sidenav

今日すること

こんにちは、ふるてつです。
残念ながらGW10連休はあっというまに終わりました。
今回のテーマもAngular Materialで、sidenavの使い方について少し書きたいと思います。

navigation関連のリファレンスについて

リファレンスではnavigation関連で 3 つのコンポーネントが記載されています。
sidenav toolbar menuです。
f:id:tetsufuru:20190506115749p:plain:w500

その中で一番大きなくくりはsidenavになると思います。
今回sidenavの記述はapp.componentに追加します。
そしてsidenav用のコンポーネントsidenav.componentを新たに追加しようと思います。

sidenav関連のモジュールを追加

今回sidenavを使用するにあたり下記のモジュールを使用しました。
MatSidenavModule MatToolbarModule MatMenuModule MatListModuleです。
これらはapp.module.tsに追加する必要があります。
わたしはmaterial.module.tsを別途作っていますので、今回そちらにに追加しました。

material.module.tsの内容

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatCardModule } from '@angular/material/card';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatCheckboxModule } from '@angular/material/checkbox';

import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    MatGridListModule,
    MatInputModule,
    MatFormFieldModule,
    MatCardModule,
    MatButtonToggleModule,
    MatDividerModule,
    MatIconModule,
    MatButtonModule,
    MatTableModule,
    MatProgressSpinnerModule,
    MatCheckboxModule,
    MatSidenavModule,
    MatToolbarModule,
    MatMenuModule,
    MatListModule
  ],
  exports: [
    MatGridListModule,
    MatInputModule,
    MatFormFieldModule,
    MatCardModule,
    MatButtonToggleModule,
    MatDividerModule,
    MatIconModule,
    MatButtonModule,
    MatTableModule,
    MatProgressSpinnerModule,
    MatCheckboxModule,
    MatSidenavModule,
    MatToolbarModule,
    MatMenuModule,
    MatListModule
  ]
})
export class MaterialModule { }

app.component.htmlを修正

ではまずapp.component.htmlを修正します。
Bootstrapで作っていた時はコンポーネントをヘッダー、フッター、ボディに分けているのみでした。

修正前のapp.component.html

<div class="container-fluid">
  <app-header></app-header>
  <router-outlet></router-outlet>
  <app-footer></app-footer>
</div>

修正後のapp.component.html

<mat-sidenav-container>
  <!-- sidenav -->
  <mat-sidenav #sidenav mode="side">
    <app-sidenav (sidenavClose)="sidenav.close()"></app-sidenav>
  </mat-sidenav>
  <!-- main content -->
  <mat-sidenav-content>
    <app-header (sidenavToggle)="sidenav.toggle()"></app-header>
    <router-outlet></router-outlet>
    <app-footer></app-footer>
  </mat-sidenav-content>
</mat-sidenav-container>

全体を<mat-sidenav-container>でくくり、中に<mat-sidenav><mat-sidenav-content>タグを配置します。
<mat-sidenav>中には、今回新しく作るsidenav用のコンポーネントを配置します。 <mat-sidenav-content>中には、これまでのヘッダー、フッター、ボディを丸ごと配置します。

補足になりますが、<mat-sidenav>タグ中の(sidenavClose)="sidenav.close()"sidenav用のコンポーネントとの連係の為に書いています。

sidenavコンポーネントの追加

sidenav用のコンポーネントをこれから作成します。
同様にサービスも追加します。

ng generate component ./component/common/sidenav
ng generate service ./service/common/sidenavService/sidenav

sidenav.component.htmlの内容

<mat-nav-list>
  <mat-list-item routerLink="/evaluation-result" (click)="onSidenavClose()">
    <mat-icon>home</mat-icon>{{ 'menu.home' | translate }}
  </mat-list-item>

  <ng-container *ngFor="let item of availableMenuListDtoLists">
    <mat-list-item [matMenuTriggerFor]="appMenu">
      <mat-icon>arrow_drop_down</mat-icon>
      <a matline>{{ item.propertyId | translate }}</a>
    </mat-list-item>
    <mat-menu #appMenu="matMenu">
      <ng-container *ngFor="let subitem of item.availableMenuDto">
        <button mat-menu-item (click)="onSidenavClose()"
          routerLink="/{{subitem.apiName}}">{{ subitem.propertyId | translate }}
        </button>
      </ng-container>
    </mat-menu>
  </ng-container>

  <mat-list-item routerLink="/account-setting" (click)="onSidenavClose()">
    <mat-icon>account_circle</mat-icon> {{ 'menu.accountSetting' | translate }}
  </mat-list-item>

  <mat-list-item routerLink="/sign-out" (click)="onSidenavClose()">
    <mat-icon>exit_to_app</mat-icon> {{ 'menu.signOut' | translate }}
  </mat-list-item>

</mat-nav-list>

ソース全体は<mat-nav-list>でくくります。
次の3行<mat-list-item>...</mat-list-item>はホームメニューです。
次の12行は動的にメニューを作る部分で、<ng-container>...</ng-container>で繰り返し処理を行います。
メニューとサブメニューがありますので繰り返しはネストします。
さらに下 2 つの<mat-list-item>...</mat-list-item>はアカウント設定とサインアウトになります。
どのメニューもクリックしたときにonSidenavClose()メソッドを呼び出すようになっています。

sidenav.component.tsの内容

import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { SidenavService } from 'src/app/service/common/sidenav/sidenav.service';
import { AvailableMenuListDto } from 'src/app/entity/dto/available-menu-list-dto';

@Component({
  selector: 'app-sidenav',
  templateUrl: './sidenav.component.html',
  styleUrls: ['./sidenav.component.css']
})
export class SidenavComponent implements OnInit {
  @Output() sidenavClose = new EventEmitter();
  // メニュー
  public availableMenuListDtoLists: AvailableMenuListDto[];

  constructor(
    private sidenavService: SidenavService,
  ) { }
  ngOnInit() {
    // メニューを取得する。
    this.getAvailableMenu();
  }
  /**
   * メニューを取得する。
   */
  private getAvailableMenu(): void {
    this.sidenavService.getAvailableMenu()
      .subscribe(availableMenuListDtoLists => this.availableMenuListDtoLists = availableMenuListDtoLists);
  }
  /**
   * メニュー選択後に親コンポーネントに対してクローズイベントを発生する。
   */
  public onSidenavClose() {
    this.sidenavClose.emit();
  }
}

このクラスは初期表示時にSidenavServiceからメニューの内容を取得します。
他にはonSidenavClose()メソッドにてsidenavCloseイベントを発生させ、親画面にイベントを通知します(@Output() sidenavClose = new EventEmitter();

sidenav.service.tsの内容

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AppConst } from '../../../app-const';
import { environment } from '../../../../environments/environment';
import { ErrorMessageService } from '../message/error-message.service';
import { AvailableMenuListDto } from 'src/app/entity/dto/available-menu-list-dto';
@Injectable({
  providedIn: 'root'
})
export class SidenavService {
  private server = environment.production ? AppConst.URL_PROD_SERVER : AppConst.URL_DEV_SERVER;
  private webApiUrl = 'availableMenu';
  constructor(
    private http: HttpClient,
    private errorMessageService: ErrorMessageService,
    private readonly translateService: TranslateService
  ) { }
  getAvailableMenu(): Observable {
    console.log(this.server + this.webApiUrl);
    return this.http.get(this.server + this.webApiUrl, AppConst.httpOptions)
      .pipe(
        catchError(err => {
          this.errorMessageService.add(this.translateService.instant('errMessage.http'));
          console.log(err);
          return of([]);
        })
      );
  }
}

このサービスはサーバからメニュー内容(AvailableMenuListDto[])を取得します。

その他 dto などの内容

available-menu-list-dto.ts

import { AvailableMenuDto } from './available-menu-dto';
export class AvailableMenuListDto {
  public propertyId: string;
  public availableMenuDto: AvailableMenuDto[];
}

available-menu-dto.ts

export class AvailableMenuDto {
  public itemNo: number;
  public propertyId: string;
  public apiName: string;
}

app-const.ts

import { HttpHeaders } from '@angular/common/http';
export class AppConst {
  // サーバーURL
  static readonly URL_PROD_SERVER = 'http://localhost/api/';
  static readonly URL_DEV_SERVER = 'http://localhost:8080/api/';
  // ユーザ権限
  static readonly ROLE_USER = 'ROLE_USER';
  static readonly ROLE_ADMIN = 'ROLE_ADMIN';
  // httpオプション(プレフライトリクエスト)
  static readonly httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    }),
    withCredentials: false
  };
}

sidenav 用 api モック作成

こちらも補足になりますが、サーバからメニューの内容を取得する api モックも作りました。
Spring Bootで若干雑に作りましたが、こちらも掲載しておきます。

AccountRestController.javaの内容

package com.weekenditlaboratory.controller;

import java.util.List;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.weekenditlaboratory.entity.dto.account.AvailableMenuListDto;
import com.weekenditlaboratory.mockService.MockAccountService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
@RequestMapping("api/availableMenu")
@CrossOrigin
public class AccountRestController {

    private final MockAccountService mockAccountService;

    @GetMapping
    public List getAvailableMenuList() {
        return mockAccountService.getAvailableMenuList(Long.valueOf(1));
    }

}

MockAccountService.javaの内容

package com.weekenditlaboratory.mockService;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import com.weekenditlaboratory.entity.dto.account.AvailableMenuDto;
import com.weekenditlaboratory.entity.dto.account.AvailableMenuListDto;

@Service
public class MockAccountService {

    public List getAvailableMenuList(Long accountId) {
        // 評価メニュー
        AvailableMenuDto availableMenuDto01 = new AvailableMenuDto(1, "subMenu.employeeLists","employee-lists");
        // 評価結果メニュー
        AvailableMenuDto availableMenuDto11 = new AvailableMenuDto(11, "subMenu.employeeLists", "employee-lists");
        AvailableMenuDto availableMenuDto12 = new AvailableMenuDto(12, "subMenu.evaluatedResultLists", "evaluated-result-lists");
        // 管理者メニュー
        AvailableMenuDto availableMenuDto21 = new AvailableMenuDto(21, "subMenu.companyRegistration", "company-registration");
        AvailableMenuDto availableMenuDto22 = new AvailableMenuDto(22, "subMenu.employeeRegistration", "employee-registration");
        AvailableMenuDto availableMenuDto23 = new AvailableMenuDto(23, "subMenu.employeeBatchRegistration", "employee-batch-registration");
        AvailableMenuDto availableMenuDto24 = new AvailableMenuDto(24, "subMenu.employeeLists", "employee-lists");
        AvailableMenuDto availableMenuDto25 = new AvailableMenuDto(25, "subMenu.questionRegistration", "question-registration");
        AvailableMenuDto availableMenuDto26 = new AvailableMenuDto(26, "subMenu.evaluatorRegistration", "evaluator-registration");
        AvailableMenuDto availableMenuDto27 = new AvailableMenuDto(27, "subMenu.evaluatorBatchRegistration", "evaluator-batch-registration");
        AvailableMenuDto availableMenuDto28 = new AvailableMenuDto(28, "subMenu.evaluationRuleRegistration", "evaluation-rule-registration");
        AvailableMenuDto availableMenuDto29 = new AvailableMenuDto(29, "subMenu.evaluationStatusCheck", "evaluation-status-check");
        // 全件管理者メニュー
        AvailableMenuDto availableMenuDto31 = new AvailableMenuDto(31, "subMenu.companyLists", "company-lists");
        // リストをセット
        List evaluateList = new ArrayList();
        evaluateList.add(availableMenuDto01);
        AvailableMenuListDto evaluateMenuListDto = new AvailableMenuListDto("menu.evaluate", evaluateList);

        List evaluatedResultList = new ArrayList();
        evaluatedResultList.add(availableMenuDto11);
        evaluatedResultList.add(availableMenuDto12);
        AvailableMenuListDto evaluatedResultMenuListDto = new AvailableMenuListDto("menu.evaluatedResult", evaluatedResultList);

        List administratorList = new ArrayList();
        administratorList.add(availableMenuDto21);
        administratorList.add(availableMenuDto22);
        administratorList.add(availableMenuDto23);
        administratorList.add(availableMenuDto24);
        administratorList.add(availableMenuDto25);
        administratorList.add(availableMenuDto26);
        administratorList.add(availableMenuDto27);
        administratorList.add(availableMenuDto28);
        administratorList.add(availableMenuDto29);
        AvailableMenuListDto administratorMenuListDto = new AvailableMenuListDto("menu.administrator", administratorList);

        List allAdministratorList = new ArrayList();
        allAdministratorList.add(availableMenuDto31);
        AvailableMenuListDto allAdministratorMenuListDto = new AvailableMenuListDto("menu.allAdministrator", allAdministratorList);

        List res = new ArrayList();

        res.add(evaluateMenuListDto);
        res.add(evaluatedResultMenuListDto);
        res.add(administratorMenuListDto);
        res.add(allAdministratorMenuListDto);

        return res;

    }

}

その他 dto などの内容 AvailableMenuListDto.java

package com.weekenditlaboratory.entity.dto.account;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AvailableMenuListDto {

    private String propertyId;

    private List availableMenuDto;

}

こちらはメインメニューの dto です。
他言語化して表示したいのでpropertyIdにはAngularで持っている他言語用のjsonのプロパティを設定するようにしました。
availableMenuDtoは実際の詳細なメニューにあたる内容でList<AvailableMenuDto>にしています。

AvailableMenuListDto.java

package com.weekenditlaboratory.entity.dto.account;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AvailableMenuDto {

    private Integer itemNo;

    private String propertyId;

    private String apiName;
}

こちらが詳細メニューの dto です。

動作確認

では動作を確認してみます。
画面を開いたときはこのような感じです。
f:id:tetsufuru:20190507205125p:plain:w500

メニューボタンを押してsidenavを開くと下記のようにサイドにメニューが表示されます。 f:id:tetsufuru:20190507205521p:plain:w500

メニューをクリックするとサブメニューが表示されます。
f:id:tetsufuru:20190507205734p:plain:w500

最後に、いずれかのメニューをクリックすると、画面が切替わりsidenavが閉じた元の状態になります。

今日の感想

今日はAngular Materialsidenavについてでした。
navigation関連のリファレンスがわたしには分かりづらく、そのためGW中盤に始めたのですが、結局GW明けになりました。

ツールバーにもメニューを置きたいですが、そこはまた別途書きたいと思います。

では、今日もお疲れ様でした。

Angular7 で Web アプリを作ろう - Angular Material

今日すること

こんにちは、ふるてつです。
GWも中盤です。
今回のテーマもAngular Materialにしました。
Angular Materialを使い始めたので、リファレンスを交えて、これまでに使用したコンポーネントについて少し書いてみようかと思います。
Angular Materialのリファレンスはこちらです。
https://material.angular.io/

Layout関連

Layout関連では<mat-grid-list><mat-card>を使いました。

<mat-grid-list>

f:id:tetsufuru:20190430110649p:plain:w500

<mat-grid-list>ですがcolsrowHeightを設定します。
2項目しか設定できないところを見るとBootstrapほど自由にグリッドを調整できるわけではなさそうです。
今回わたしは要素を整列するためcol="1"にして、行間を指定するためrowHeight="10rem"などにして使ってます。
中の要素はそれぞれ<mat-grid-tile>の中に記述します。

<mat-grid-list>の使用例

<div id="welcome">
  <mat-grid-list cols="1" rowHeight="10rem">
    <mat-grid-tile>
      <h1 translate>welcomeScreen.title</h1>
    </mat-grid-tile>
    <mat-grid-tile>
      <h1 translate>welcomeScreen.systemName</h1>
    </mat-grid-tile>
    <mat-grid-tile>
      <h2 translate>welcomeScreen.comment</h2>
    </mat-grid-tile>
  </mat-grid-list>
</div>

<mat-card>

<mat-card>はタグを記述するだけで使用できます。
デフォルトで背景に影がつくようで、このあたりがやはり便利です。
f:id:tetsufuru:20190430183446p:plain:w500

<mat-card-header><mat-card-title><mat-card-subtitle>などmat-cardの中で、さらに使える要素があります。
今回は<mat-card-content>のみを使用しています。

Form Controls関連

Form Controls関連では<mat-form-field>matInputを使用しました。

<mat-form-field>

f:id:tetsufuru:20190430215337p:plain:w500

<mat-form-field>は、<input><textarea>などの前後に設定します。
<input>のアンダーラインや、フローティングラベル、ヒントメッセージなどのスタイルを適用するためのものですので、これを書かないとMaterialらしい動きがなにもでません。

matInput

f:id:tetsufuru:20190430215435p:plain:w500

matInput<input>タグの中に記述します。
リファレンスでは<input matInput placeholder="Favorite food" value="Sushi">のようにplaceholdervalueの例があります。
わたしが使用した限りではもう少し設定が必要でした。

matInputの使用例

<mat-form-field class="form-field">
  <input id="signInEMailAddress" matInput type="email" formControlName="signInEMailAddress"
    placeholder="{{ 'signInScreen.eMailAddress' | translate }}">
  <mat-error *ngIf="signInEMailAddress.hasError('required')">
    {{ 'validateErrorMessage.required' | translate }}
  </mat-error>
  <mat-error *ngIf="signInEMailAddress.hasError('email')">
    {{ 'validateErrorMessage.format' | translate }}
  </mat-error>
</mat-form-field>

Data table関連

Data table関連ではmat-tableを使用しました。

mat-table

f:id:tetsufuru:20190430204403p:plain:w500

mat-table<table mat-table [dataSource]="dataSource">のように記述します。
"dataSource"にはtsファイルに記述した「テーブル表示用の配列」を指定します。
上記以外で新たにtsファイルに、「テーブル表示用の配列」の列名だけを格納した変数が必要でした。
こちらをmat-header-rowに設定します(Bootstrapの時にはなかった設定です)
あとはリファレンスの通りに各列を<ng-container></ng-container>で記述します。
余談になりますが、リファレンスに書かれているmat-tableのクラス設定"mat-elevation-z8"は、テーブルの背景が陰になる指定です。

mat-tableの使用例

sign-in.component.html

<div id="informations">
  <table mat-table [dataSource]="informations" class="mat-elevation-z8">
    <ng-container matColumnDef="date">
      <th mat-header-cell *matHeaderCellDef style="width: 20%;">
        {{ "signInScreen.information.date" | translate }}
      </th>
      <td mat-cell *matCellDef="let element"> {{element.date}} </td>
    </ng-container>
    <ng-container matColumnDef="information">
      <th mat-header-cell *matHeaderCellDef style="width: 20%;">{{ "signInScreen.information.detail" | translate }}
      </th>
      <td mat-cell *matCellDef="let element"> {{element.information}} </td>
    </ng-container>
    <ng-container matColumnDef="detail">
      <th mat-header-cell *matHeaderCellDef>
        {{ "signInScreen.information.information" | translate }}</th>
      <td mat-cell *matCellDef="let element"> {{element.detail}} </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayInformationColumns; sticky: true"></tr>
    <tr mat-row *matRowDef="let row; columns: displayInformationColumns;"></tr>
  </table>
</div>

sign-in.component.ts

export class SignInComponent implements OnInit {

  // お知らせ欄の変数
  informations: Information[];
  displayInformationColumns: string[] = ['date', 'information', 'detail'];

  constructor(
    …
  ) { }

  ngOnInit() {
    …
  }
}

information.ts

export class Information {
  date: string;
  information: string;
  detail: string;
}

今日の感想

今日はAngular Materialコンポーネントについてでした。
始めて約1日で使用したものを書いた感じです。
これから先まだまだ出てきそうですが、その時はまた記事にしていきたいと思います。

では、今日もお疲れ様でした。