noxi雑記

.NET、Angularまわりの小ネタブログ

Angular Materialでreadonlyっぽい入力を作る

Angular Mateiralにはinputやselect、checkbox、radioなどの入力系コントロールがありますが、inputだけreadonlyがサポートされ、他コントロールの入力可否状態はdisabled or notです。実際にアプリを開発する際にdisabledではなくreadonlyが欲しい場合があるので、どうすれば良いかを考えてみました。

試した環境

  • Angular 6.0.0-rc.5
  • Angular Material 6.0.0-rc.5

試した内容

Input

inputは公式にreadonlyがサポートされているため特に工夫する必要はありません。 参考

Select

selectはいわゆるドロップダウンです。見た目としては通常のinputの右側にドロップダウン可能である▼が表示されます。このコンポーネントはクリックするとdisabled状態以外ではドロップダウンの中身が必ず表示されてしまいます。

selectは通常状態、disabled状態の時は普通にselectを表示し、readonly状態の時はselectでは無くinputを表示することでそれっぽくなります。

<!--通常状態-->
<mat-form-field>
  <mat-label>NORMAL</mat-label>
  <mat-select>
    <mat-option value="0">Hoge</mat-option>
    <mat-option value="1">Fuga</mat-option>
    <mat-option value="2">Piyo</mat-option>
  </mat-select>
</mat-form-field>

<!--disabled-->
<mat-form-field>
  <mat-label>DISABLED</mat-label>
  <mat-select disabled>
    <mat-option value="0">Hoge</mat-option>
    <mat-option value="1">Fuga</mat-option>
    <mat-option value="2">Piyo</mat-option>
  </mat-select>
</mat-form-field>

<!--readonly-->
<mat-form-field>
  <mat-label>READONLY</mat-label>
  <mat-select *ngIf="false">
    <mat-option value="0">Hoge</mat-option>
    <mat-option value="1">Fuga</mat-option>
    <mat-option value="2">Piyo</mat-option>
  </mat-select>

  <input *ngIf="true" type="text" matInput readonly value="Hoge">
  <div matSuffix class="mat-select-arrow"></div>
</mat-form-field>

f:id:noxi515:20180415183903p:plain

DatePicker

DatePickerは日付が入力内容となるInputです。DatePicker本体、およびDatePickerを表示するためのDatePickerToggleを利用します。

inputをreadonlyとしつつトグルをdisabledにするとそれっぽくなります。

<!--通常状態-->
<mat-form-field>
  <mat-label>NORMAL</mat-label>
  <input matInput [matDatepicker]="picker1">
  <mat-datepicker-toggle matSuffix [for]="picker1"></mat-datepicker-toggle>
  <mat-datepicker #picker1></mat-datepicker>
</mat-form-field>

<!--disabled-->
<mat-form-field>
  <mat-label>DISABLED</mat-label>
  <input matInput [matDatepicker]="picker2" disabled>
  <mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
  <mat-datepicker #picker2></mat-datepicker>
</mat-form-field>

<!--readonly-->
<mat-form-field>
  <mat-label>READONLY</mat-label>
  <input matInput [matDatepicker]="picker3" readonly>
  <mat-datepicker-toggle matSuffix [for]="picker3" disabled></mat-datepicker-toggle>
  <mat-datepicker #picker3></mat-datepicker>
</mat-form-field>

f:id:noxi515:20180415185624p:plain

Checkbox

checkboxは状態として checked or not、indeterminate or not を持つコントロールです。

checkboxは明らかにInputと見た目が異なるためInputのreadonlyを使用することができません。なので状態が変更されたら自動で元の状態に戻すDirectiveを作成してそれっぽく見せてみます。

import { AfterViewInit, Directive, Input, OnDestroy} from '@angular/core';
import { MatCheckbox } from '@angular/material';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';

@Directive({
  selector: 'mat-checkbox[mat-readonly]'
})
export class MatCheckboxReadonly implements OnDestroy, AfterViewInit {

  private readonly _onDestroy$ = new Subject<void>();
  private _readonly = false;
  private _checked = false;
  private _indeterminate = false;

  get readonly() {
    return this._readonly;
  }

  @Input('mat-readonly')
  set readonly(value: boolean) {
    this._readonly = value;
    this._apply();
  }

  constructor(private readonly _checkbox: MatCheckbox) {
  }

  ngOnDestroy() {
    this._onDestroy$.next();
  }

  ngAfterViewInit() {
    // 値が変更されたら元に戻す
    this._checkbox.change
      .pipe(
        takeUntil(this._onDestroy$),
        filter(() => this.readonly),
      )
      .subscribe(() => this._checkbox.checked = this._checked);
    this._checkbox.indeterminateChange
      .pipe(
        takeUntil(this._onDestroy$),
        filter(() => this.readonly),
      )
      .subscribe(() => this._checkbox.indeterminate = this._indeterminate);

    this._apply();
  }

  private _apply() {
    // Rippleを消すことでクリックできた感を無くす
    this._checkbox.disableRipple = this.readonly;

    this._checked = this._checkbox.checked;
    this._indeterminate = this._checkbox.indeterminate;
  }

}
<!--通常状態-->
<mat-checkbox>NORMAL</mat-checkbox>

<!--disabled-->
<mat-checkbox disabled="">DISABLED</mat-checkbox>

<!--readonly-->
<mat-checkbox [mat-readonly]="true">READONLY</mat-checkbox>

f:id:noxi515:20180415191230p:plain

欠点としては、Indeterminate状態の時、最初のクリックに対して一瞬変な見た目になります。

Radio

Radioはグループ内のどれか一つだけを選択するRadioButtonです。これもCheckbox同様Inputと全く見た目が異なるため、値が変更されたら強制的に元に戻すDirectiveを作成します。

import { AfterViewInit, Directive, Input, OnDestroy } from '@angular/core';
import { CanDisableRipple, MatRadioGroup } from '@angular/material';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';

@Directive({
  selector: 'mat-radio-group[mat-readonly]'
})
export class MatRadioGroupReadonly implements OnDestroy, AfterViewInit {

  private readonly _onDestroy$ = new Subject<void>();
  private _readonly = false;
  private _value: any = '';

  get readonly() {
    return this._readonly;
  }

  @Input('mat-readonly')
  set readonly(value: boolean) {
    this._readonly = value;
    this._apply();
  }

  constructor(private readonly _radio: MatRadioGroup) {
  }

  ngOnDestroy() {
    this._onDestroy$.next();
    this._onDestroy$.complete();
    this._onDestroy$.unsubscribe();
  }

  ngAfterViewInit() {
    // 値が変更されたら元に戻す
    this._radio.change
      .pipe(
        takeUntil(this._onDestroy$),
        filter(() => this.readonly),
      )
      .subscribe(() => this._radio.value = this._value);
    this._radio._radios.changes
      .pipe(
        takeUntil(this._onDestroy$),
        filter(() => this.readonly),
      )
      .subscribe(() => this._apply());

    this._apply();
  }

  private _apply() {
    // Rippleを消すことでクリックできた感を無くす
    if (this._radio._radios) {
      this._radio._radios.forEach(i => {
        (i as CanDisableRipple).disableRipple = this.readonly;
        // これをしないと最初の1クリックだけ必ずRippleが表示されてしまう
        i._ripple.disabled = this.readonly;
      });
    }

    this._value = this._radio.value;
  }

}
<!--通常状態-->
<mat-radio-group value="0">
  <mat-radio-button value="0">Hoge</mat-radio-button>
  <mat-radio-button value="1">Fuga</mat-radio-button>
  <mat-radio-button value="2">Piyo</mat-radio-button>
</mat-radio-group>

<!--disabled-->
<mat-radio-group disabled value="0">
  <mat-radio-button value="0">Hoge</mat-radio-button>
  <mat-radio-button value="1">Fuga</mat-radio-button>
  <mat-radio-button value="2">Piyo</mat-radio-button>
</mat-radio-group>

<!--readonly-->
<mat-radio-group [mat-readonly]="true" value="0">
  <mat-radio-button value="0">Hoge</mat-radio-button>
  <mat-radio-button value="1">Fuga</mat-radio-button>
  <mat-radio-button value="2">Piyo</mat-radio-button>
</mat-radio-group>

f:id:noxi515:20180415193654p:plain

終わりに

とりあえずそれっぽくなるように調整してみただけなので、あまりよろしい方法では無いかも知れないけど、それっぽくはなります。 disabled状態に対してCSSでゴニョゴニョするのもありかなって思ってはいるのですが、こっちの方が楽だったので。。。