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