Angular Material清除按钮封装为组件后渲染异常问题
问题复现
最初使用内联方式在mat-form-field中编写清除按钮时,按钮与输入框处于同一行,布局符合预期,内联实现代码如下:
<mat-form-field appearance="fill"> <mat-label>Email alebo ID používateľa</mat-label> <input #input matInput type="text" placeholder="123 | mail@mail.com" formControlName="inputValue" /> <button matSuffix type="button" *ngIf="this.userForm.get('inputValue')!.value" mat-icon-button (click)="this.userForm.get('inputValue')?.setValue('')" > <mat-icon inline="true">close</mat-icon> </button> </mat-form-field>
将清除按钮抽离为独立组件app-clear-input-button后,组件模板代码:
<button matSuffix type="button" *ngIf="control.value" mat-icon-button (click)="control?.setValue('')" > <mat-icon inline="true">close</mat-icon> </button>
组件逻辑代码:
import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-clear-input-button', templateUrl: './clear-input-button.component.html', styleUrls: ['./clear-input-button.component.scss'], }) export class ClearInputButtonComponent implements OnInit { @Input() public control: any; constructor() {} ngOnInit(): void {} }
在表单中引入组件替换原有内联按钮:
<mat-form-field appearance="fill"> <mat-label>Email alebo ID používateľa</mat-label> <input #input matInput type="text" placeholder="123 | mail@mail.com" formControlName="inputValue" /> <app-clear-input-button [control]="$any(this.userForm).get('inputValue')" > </app-clear-input-button> </mat-form-field>
此时清除按钮被渲染到单独一行,组件宿主元素产生额外包裹导致布局错位,和内联写法表现不一致。
问题原因
Angular组件默认会生成一个与组件选择器同名的宿主DOM元素(此处为<app-clear-input-button>)。mat-form-field的后缀识别逻辑只会把直接子元素上带matSuffix指令的节点放到输入框同一行的后缀位置。
抽离组件后,matSuffix指令写在了组件内部的button元素上,外层的宿主元素没有被识别为后缀节点,默认作为块级元素占据一整行,最终把内部的按钮挤到了下一行,造成布局错位。
解决方案
方案1:给宿主元素绑定matSuffix指令,消除宿主布局影响
修改组件的装饰器配置,把matSuffix指令绑定到组件宿主元素上,同时设置宿主元素的display属性为contents,让宿主不产生独立的布局盒,不影响内部元素的排布:
修改后的组件逻辑代码:
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-clear-input-button', templateUrl: './clear-input-button.component.html', styleUrls: ['./clear-input-button.component.scss'], host: { // 让mat-form-field识别当前组件为后缀节点 'matSuffix': '', // 消除宿主元素的块级布局影响 'style': 'display: contents;' } }) export class ClearInputButtonComponent { @Input() public control: any; }
同时把组件模板中button上的matSuffix属性删除,避免重复绑定:
<button type="button" *ngIf="control.value" mat-icon-button (click)="control?.setValue('')" > <mat-icon inline="true">close</mat-icon> </button>
这种方案改动最小,不需要调整原有组件的使用方式。
方案2:使用属性选择器定义组件,完全消除额外宿主元素
把组件的选择器从元素选择器改为属性选择器,直接将组件附着在原生button元素上,不会产生额外的自定义DOM节点,DOM结构和内联写法完全一致,从根源避免布局问题。
修改后的组件逻辑代码:
import { Component, Input } from '@angular/core'; @Component({ selector: 'button[app-clear-input-button]', template: `<mat-icon inline="true">close</mat-icon>`, host: { 'matSuffix': '', 'type': 'button', 'mat-icon-button': '', '*ngIf': 'control.value', '(click)': 'control?.setValue("")' } }) export class ClearInputButtonComponent { @Input() public control: any; }
使用组件时直接在button标签上添加组件属性即可:
<mat-form-field appearance="fill"> <mat-label>Email alebo ID používateľa</mat-label> <input #input matInput type="text" placeholder="123 | mail@mail.com" formControlName="inputValue" /> <button app-clear-input-button [control]="userForm.get('inputValue')"></button> </mat-form-field>
这种方案的DOM结构最干净,不会产生多余层级,是更推荐的实现方式。
内容的提问来源于stack exchange,提问作者FatalPaperCut

