前言

本篇记入一些 Angular Material 的小东西。

Override Material Icon Button Size

参考:Stack Overflow – Change size of mat-icon-button

通常当我们想修改 size 的时候,第一个想法是改它的 density

但是 density 只能缩小,不能放大,所以,要放大还得用上一些黑魔法。

常见的黑魔法 (override Material styles) 有好几招:

第一招是 override CSS variables,这是最安全有效的方式,但是 Angular Material 不一定有提供。

第二招就是 ::ng-deep 或者全局 styles.scss。

我们来看一个具体例子

<button class="close-btn" mat-icon-button aria-label="Close">
<mat-icon class="icon" fontIcon="close" />
</button>

上面这是一个 icon button。

默认 size 是 48px。

我们拆开来一个一个看,

首先 button 是 48px 然后带 padding 12px

所以里面扣到来 icon 是 24px

除了 button size 是 CSS variable 其它都是属性值,那我们一次换到完呗。

.close-btn {
--size: 256px;
--mdc-icon-button-state-layer-size: var(--size);
--mdc-icon-button-icon-size: calc(var(--size) / 2); padding: calc(var(--size) / 4); .icon {
font-size: calc(var(--size) / 2);
width: calc(var(--size) / 2);
height: calc(var(--size) / 2);
}
}

效果

虽然看上去是 ok 了,但其实还有一条落网之鱼

这个 element 的 class 没有 _ngcontent-ng-xxxx 所以我们要用 ::ng-deep 才能修改它。

.close-btn {
--size: 256px; ::ng-deep .mat-mdc-button-touch-target {
width: var(--size);
height: var(--size);
}
}

搞定,最后封装起来。

@mixin set-mat-icon-button-size($size) {
--size: #{$size};
--mdc-icon-button-state-layer-size: var(--size);
--mdc-icon-button-icon-size: calc(var(--size) / 2); padding: calc(var(--size) / 4); mat-icon {
font-size: calc(var(--size) / 2);
width: calc(var(--size) / 2);
height: calc(var(--size) / 2);
} ::ng-deep .mat-mdc-button-touch-target {
width: var(--size);
height: var(--size);
}
} .close-btn {
@include set-mat-icon-button-size(256px);
}

scale vs density

scale 指的是整个组件的每一个部分都同比例 scale。

比如 icon button 的话,那就是 padding 和 icon size 一起 scale。

通常整体 scale up 体验是正确的,但反过来整体 scale down 则是错误的。

因为主体不能太小丫,所以遇到这种情况应该使用 density 而不是 scale down。

density 和 scale down 是不同的,density 只会缩小空间 (e.g. padding) 尺寸,不改变主体 (e.g. icon) 尺寸。

总结:想让 icon button 变大可以用上面的 scale up 方法,想让 icon button 变小,请使用 built-in 的 density。

Override Material Button Color

默认情况下 Material Button 的颜色是由 primary theme 决定的。

$color-theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$blue-palette,
tertiary: mat.$green-palette,
),
)
);

效果

假如这些 button 被放到一个 background-color: primary 的 toolbar 里,它的 contrast 就毁了。

background-color: mat.get-theme-color($color-theme, 'primary');

效果

这种情况下,我们需要把的 button color 从原本的 primary 变成 on-primary,这样 contrast 才会正确。

首先,我们可以通过

:root {
@include mat.button-base($base-theme);
@include mat.button-color($color-theme);
}

加上

ng build

获取到所有和 button 有关的 color variables,这样我们就知道要修改什么了。

:root {
--mdc-text-button-container-shape: 9999px;
--mdc-protected-button-container-shape: 9999px;
--mdc-filled-button-container-shape: 9999px;
--mdc-outlined-button-outline-width: 1px;
--mdc-outlined-button-container-shape: 9999px;
--mat-text-button-horizontal-padding: 12px;
--mat-text-button-with-icon-horizontal-padding: 16px;
--mat-text-button-icon-spacing: 8px;
--mat-text-button-icon-offset: -4px;
--mat-protected-button-horizontal-padding: 24px;
--mat-protected-button-icon-spacing: 8px;
--mat-protected-button-icon-offset: -8px;
--mat-filled-button-horizontal-padding: 24px;
--mat-filled-button-icon-spacing: 8px;
--mat-filled-button-icon-offset: -8px;
--mat-outlined-button-horizontal-padding: 24px;
--mat-outlined-button-icon-spacing: 8px;
--mat-outlined-button-icon-offset: -8px;
--mdc-text-button-label-text-color: #343dff;
--mdc-text-button-disabled-label-text-color: rgba(27, 27, 31, 0.38);
--mdc-protected-button-container-elevation-shadow:
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12);
--mdc-protected-button-disabled-container-elevation-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
0px 0px 0px 0px rgba(0, 0, 0, 0.12);
--mdc-protected-button-focus-container-elevation-shadow:
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12);
--mdc-protected-button-hover-container-elevation-shadow:
0px 3px 3px -2px rgba(0, 0, 0, 0.2),
0px 3px 4px 0px rgba(0, 0, 0, 0.14),
0px 1px 8px 0px rgba(0, 0, 0, 0.12);
--mdc-protected-button-pressed-container-elevation-shadow:
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12);
--mdc-protected-button-container-color: #fffbff;
--mdc-protected-button-label-text-color: #343dff;
--mdc-protected-button-disabled-container-color: rgba(27, 27, 31, 0.12);
--mdc-protected-button-disabled-label-text-color: rgba(27, 27, 31, 0.38);
--mdc-filled-button-container-color: #343dff;
--mdc-filled-button-label-text-color: #ffffff;
--mdc-filled-button-disabled-container-color: rgba(27, 27, 31, 0.12);
--mdc-filled-button-disabled-label-text-color: rgba(27, 27, 31, 0.38);
--mdc-outlined-button-disabled-outline-color: rgba(27, 27, 31, 0.12);
--mdc-outlined-button-disabled-label-text-color: rgba(27, 27, 31, 0.38);
--mdc-outlined-button-label-text-color: #343dff;
--mdc-outlined-button-outline-color: #777680;
--mat-text-button-state-layer-color: #343dff;
--mat-text-button-disabled-state-layer-color: #46464f;
--mat-text-button-ripple-color: rgba(52, 61, 255, 0.12);
--mat-text-button-hover-state-layer-opacity: 0.08;
--mat-text-button-focus-state-layer-opacity: 0.12;
--mat-text-button-pressed-state-layer-opacity: 0.12;
--mat-protected-button-state-layer-color: #343dff;
--mat-protected-button-disabled-state-layer-color: #46464f;
--mat-protected-button-ripple-color: rgba(52, 61, 255, 0.12);
--mat-protected-button-hover-state-layer-opacity: 0.08;
--mat-protected-button-focus-state-layer-opacity: 0.12;
--mat-protected-button-pressed-state-layer-opacity: 0.12;
--mat-filled-button-state-layer-color: #ffffff;
--mat-filled-button-disabled-state-layer-color: #46464f;
--mat-filled-button-ripple-color: rgba(255, 255, 255, 0.12);
--mat-filled-button-hover-state-layer-opacity: 0.08;
--mat-filled-button-focus-state-layer-opacity: 0.12;
--mat-filled-button-pressed-state-layer-opacity: 0.12;
--mat-outlined-button-state-layer-color: #343dff;
--mat-outlined-button-disabled-state-layer-color: #46464f;
--mat-outlined-button-ripple-color: rgba(52, 61, 255, 0.12);
--mat-outlined-button-hover-state-layer-opacity: 0.08;
--mat-outlined-button-focus-state-layer-opacity: 0.12;
--mat-outlined-button-pressed-state-layer-opacity: 0.12;
}

我们拿 basic button 作为例子,和 color 有关的 variables 有

--mdc-text-button-label-text-color
--mat-text-button-state-layer-color
--mat-text-button-ripple-color --mdc-text-button-disabled-label-text-color;
--mat-text-button-disabled-state-layer-color;

最后两个是和 disabled 相关的,我们简化一些,就不管 disabled 的 color 了,关注头三个就好了。

/* 1. button color 改用 on-primary,在 bg primary color 下,contrast 就出的来  */
$btn-color: mat.get-theme-color($color-theme, 'on-primary'); /* 2. 把颜色拆分,因为下面要搭配 alpha */
$btn-color-value: #{color.hue($btn-color)} #{color.saturation($btn-color)} #{color.lightness($btn-color)}; /* 3. 为了方便打理,把 SCSS variables 放入 CSS variables */
--btn-color: #{$btn-color};
--btn-color-value: #{$btn-color-value}; /* 4. 接着就可以换颜色了 */
--mdc-text-button-label-text-color: var(--btn-color);
--mat-text-button-state-layer-color: var(--btn-color); /* 5. Ripple color 需要搭配 alpha */
--mat-text-button-ripple-color: hsl(var(--btn-color-value) / var(--mat-text-button-pressed-state-layer-opacity));

效果

basic button contrast ok 了。

接着是 stroked 和 flat button,也是一样的步骤。

/* stroked button 就是 outlined-button,做法和 basic button 大同小异 */
--mdc-outlined-button-label-text-color: var(--btn-color);
--mdc-outlined-button-outline-color: var(--btn-color);
--mat-outlined-button-state-layer-color: var(--btn-color);
--mat-outlined-button-ripple-color: hsl(
var(--btn-color-value) / var(--mat-outlined-button-pressed-state-layer-opacity)
);

flat button 需要多一个 on-btn-color,它就是 on-primary 的 constract 也就是 primary 咯。

$on-btn-color: mat.get-theme-color($color-theme, 'primary');
$on-btn-color-value: #{color.hue($on-btn-color)} #{color.saturation($on-btn-color)} #{color.lightness($on-btn-color)}; --on-btn-color: #{$on-btn-color};
--on-btn-color-value: #{$on-btn-color-value};

接着

/* flat button 就是 filled-button,做法和 basic button 大同小异 */
--mdc-filled-button-container-color: var(--btn-color);
--mdc-filled-button-label-text-color: var(--on-btn-color);
--mat-filled-button-state-layer-color: var(--on-btn-color);
--mat-filled-button-ripple-color: rgb(var(--on-btn-color) / var(--mat-filled-button-pressed-state-layer-opacity));

最终效果

最后讲一下关于 disabled 的情况

disabled 要改成什么颜色不好说,要看 background-color 具体是什么颜色,像 primary,我也不知道什么颜色合适

我们了解一下它原本的就好了呗。

--mdc-outlined-button-disabled-outline-color: rgba(27, 27, 31, 0.12);
--mdc-outlined-button-disabled-label-text-color: rgba(27, 27, 31, 0.38);
--mat-outlined-button-disabled-state-layer-color: #46464f;

#46464f 是 on-surface-variant

27, 27, 31 是 on-surface

alpha 0.12 和 0.38 是 hardcode 来的,我们无法拿到。

Override Material Checkbox Design

下图是 Google Ads 里的 checkbox

它有好几个地方和 Angular Material 原生的 checkbox 不一样。

Alignment

Angular Material 的 checkbox 是 align-items: center

像这样

而 Google Ads 是这样

我们可以这样去修改

.mat-mdc-checkbox .mdc-form-field {
align-items: unset; .mdc-label {
padding-top: calc((var(--mdc-checkbox-state-layer-size) - var(--mat-checkbox-label-text-line-height)) / 2);
}
}

去掉原本的 align-items,添加 padding-top 就可以了

至于计算方式可以参考它旁边的 checkbox icon

这个 18px 是 icon size 来的,相关源码在 _checkbox-theme.scss (是在 material-components-web MDC 项目,不是 Angular Material 哦)

我们要 align 的是 text 不是 icon 所以不使用 18px,而是使用 text 的 line-height --mat-checkbox-label-text-line-height (默认是 20px)。

所以最终的 formula 是

padding-top: calc((var(--mdc-checkbox-state-layer-size) - var(--mat-checkbox-label-text-line-height)) / 2);

Full checkbox clickable

红框是 Angular Material <mat-checkbox> 的范围,只有左边的 checkbox 和文字可以点击,右边空白区域点击无效。

Google Ads 的 checkbox 则是全部可以点击

我们可以这样去修改

Styles

mat-checkbox {
cursor: pointer;
}

HTML

<mat-checkbox #checkbox (click)="handleCheckboxClick($event, checkbox)">
check me
</mat-checkbox>

监听点击事件

Scripts

export class SimpleTestComponent {
handleCheckboxClick(event: Event, checkbox: MatCheckbox) {
if (!(event.target instanceof HTMLElement)) return; if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLLabelElement ||
event.target.classList.contains('mat-mdc-checkbox-touch-target')
) {
return;
} checkbox._onTouchTargetClick();
}
}

如果点击的是 input, label 或者 .mat-mdc-checkbox-touch-target 那就不需要处理 (因为原本就会处理了),其余的地方就使用 MatCheckbox._onTouchTargetClick 方法更新。

注:_onTouchTargetClick 是私有方法,不安全,但我也没有找到更好的方法了,直接改 checked 或者用 toggle 改 checked 都不会触发 @Output change,所以都不理想。

效果

Highlight on hover

下图是 Google Ads 的 checkbox

它和 Angular Material 有两个区别,第一是 hover 的范围大,第二是 checked 时 hover 会变色。

下图是 Angular Material 的 checkbox

我们可以这样去修改

mat-checkbox {
width: 256px;
border: 1px solid red; cursor: pointer; &:hover {
::ng-deep .mdc-checkbox__native-control {
/* 没有 checked 时 hover 出现圆背景 */
&:not(:focus):not(:checked):not(:indeterminate) ~ .mdc-checkbox__ripple {
opacity: var(--mdc-checkbox-unselected-hover-state-layer-opacity);
background-color: var(--mdc-checkbox-unselected-focus-state-layer-color);
} /* 有 checked 时 hover 出现圆背景,圆背景色不同 */
&:not(:focus):checked ~ .mdc-checkbox__ripple,
&:not(:focus):indeterminate ~ .mdc-checkbox__ripple {
opacity: var(--mdc-checkbox-selected-hover-state-layer-opacity);
background-color: var(--mdc-checkbox-selected-focus-state-layer-color);
} /* 没有 checked 时 hover checkbox 框的颜色加深 */
&:not(:focus):not(:checked):not(:indeterminate) ~ .mdc-checkbox__background {
border-color: var(--mdc-checkbox-unselected-hover-icon-color);
} $color-theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$blue-palette,
tertiary: mat.$green-palette,
),
)
); /* 原本 checkbox 的颜色四 40,这里拿一个深一点的颜色 30 */
$color: mat.get-theme-color($color-theme, primary, 30); /* 有 checked 时 hover checkbox 框和框内变深色 */
&:checked ~ .mdc-checkbox__background,
&:indeterminate ~ .mdc-checkbox__background {
border-color: $color !important;
background-color: $color !important;
}
}
}
}

好,罗里吧嗦一堆,大致上是这样。

Smaller

Material checkbox 的最大 dimension 是 48x48px

红框就是 touch area。

touch area 比较大,<mat-checkbox> 是里面的蓝框,它只有 40x40px。

而最里面的 input 框只有 18x18px

假如我们想缩写 material checkbox,可以使用 density

@function define-density-theme($scale) {
@return mat.define-theme(
(
density: (
scale: $scale,
),
)
);
} mat-checkbox {
@include mat.checkbox-density(define-density-theme(-2));
}

效果.

红框 touch-area 会被 display none,没了。

蓝框变成 32x32px。

最里面的 input 框则依然是 18x18px,因为 density 只会改变 spacing (空间),不会改变其它具体的东西。

如果我们偏偏就是想要让 input 框变小呢?

像 Google Ads 和 Gmail 的 input 框就只有 16px 而已

 

我们看看它的结构和 CSS Styles,有 2 个 element 决定了 18px

还有

都是写死的,无法透过 CSS Variables 修改,只能用杀手锏了 ::ng-deep 了

mat-checkbox {
@include mat.checkbox-density(define-density-theme(-2)); ::ng-deep {
.mdc-checkbox {
flex: 0 0 16px;
width: 16px;
height: 16px;
padding: calc((var(--mdc-checkbox-state-layer-size) - 16px) / 2);
} .mdc-checkbox__background {
width: 16px;
height: 16px;
top: calc((var(--mdc-checkbox-state-layer-size) - 16px) / 2);
left: calc((var(--mdc-checkbox-state-layer-size) - 16px) / 2);
}
}
}

效果

Override Material Form Field Styles

看这篇 Override MatFormField Styling

Override Material Snackbar Styles

下图 Material Snackbar

下图是 Google Tag Manager 里的 Material Snackbar

显然,Google Tag Manager 的短短,肥肥更可爱。

要 override snackbar 并不容易,因为它是用 Overlay 做的。

我们在 Overlay 文章讲解过,Overlay 会被 append 到 body,所以只有全局 styles 才对它有效。

同理,要 override snackbar 也需要使用全局 styles。

App 组件

export class AppComponent {
private snackBar = inject(MatSnackBar);
private snackBarRef: MatSnackBarRef<TextOnlySnackBar> | null = null; show() {
this.snackBarRef = this.snackBar.open('Loading...', undefined, {
horizontalPosition: 'start',
verticalPosition: 'bottom',
// 1. 添加 panel class,作为 CSS selector
panelClass: 'my-loading-snack-bar',
});
} hide() {
this.snackBarRef!.dismiss();
}
}

在打开 snackbar 的时候要给予它一个 panel class 作为 CSS selector。

然后在全局 styles.scss

.my-loading-snack-bar {
margin-left: 24px !important;
margin-bottom: 24px !important; .mdc-snackbar__label {
padding-left: 24px !important;
padding-right: 16px !important;
} .mdc-snackbar__surface {
min-width: unset !important;
}
}

选择指定的 snackbar overlay override styles,这样就可以了。

另外,不只是 snackbar,只要是有使用 Overlay 的 Material 组件 (Menu, Select, Dialog 等等) 一样都是透过设置 panelClass + 全局 styles 来实现 override。

SelectionModel

SelectionModel 是一个 class,它是 Angular Material 提供的一个小小功能。虽然小,但还挺好用的。

在 Angular Material Table 其中一个 examples -- Table with selection 里就使用到了 SelectionModel。

Simple use cases

顾名思义 SelectionModel 就是拿来做 select 和维护 model 用的。

new SelectionModel

type SelectedType = string;
const multiple = false;
const defaultSelected = ['a'];
const emitChanges = true;
const compareWithFn: SelectionModel<SelectedType>['compareWith'] = (v1, v2) => v1 === v2; const selectionModel = new SelectionModel<SelectedType>(multiple, defaultSelected, emitChanges, compareWithFn);

首先是实例化一个 SelectionModel 对象。

实例化时可以传入一些参数作为配置:

  1. multiple

    select 有分 single 和 multiple,就像 <select multiple> 那样,默认是 single

  2. default selected

    selected 就是指 SelectionModel 的 values。

    注:它的类型一定是 array,不管是 single 还是 multiple,Angular Material 都统一用 array。

  3. emitChanges

    如果没有想要监听 selected 变更,那可以关掉,默认是 true

  4. compareWithFn

    上面的例子 selected 的类型是 string,一般对比的方法是 ===,如果是对象就不一样了,可能是 obj.id === obj.id

    默认是 v1 === v2

好,我们往下看它的功就能体会上面这些配置的不同了。

selected

const multiple = false;
const defaultSelected = ['Derrick'];
const selectionModel = new SelectionModel<string>(multiple, defaultSelected);
console.log(selectionModel.selected); // ['Derrick']

selected 就是调出当前已选择的全部值。

无论是 multiple 与否,它都返回 array。

multiple 的例子

const multiple = true;
const defaultSelected = ['Derrick', 'David'];
const selectionModel = new SelectionModel<string>(multiple, defaultSelected);
console.log(selectionModel.selected); // ['Derrick', 'David']

clear

clear 就是清空 selected

const multiple = false;
const defaultSelected = ['Derrick'];
const selectionModel = new SelectionModel<string>(multiple, defaultSelected);
console.log(selectionModel.selected); // ['Derrick'] selectionModel.clear();
console.log(selectionModel.selected); // []

select

single select 的例子

const selectionModel = new SelectionModel<string>();
selectionModel.select('Derrick');
console.log(selectionModel.selected); // ['Derrick']

选了再选

const selectionModel = new SelectionModel<string>();
selectionModel.select('Derrick'); // 先 select 'Derrick'
selectionModel.select('David'); // 再 select 'David'
console.log(selectionModel.selected); // ['David'] 最后是 'David'

multiple select 的例子

const selectionModel = new SelectionModel<string>(true);
selectionModel.select('Derrick'); // 先 select 'Derrick'
selectionModel.select('David'); // 再 select 'David'
console.log(selectionModel.selected); // ['David', 'David'] 最后两个都有

single 最终只会有一个 selected,multiple 就会有多个。

注:selected 是会依据顺序的哦,先 select 的就在前面,后 select 就在后面。

duplicated select?

const selectionModel = new SelectionModel<string>(true);
selectionModel.select('Derrick'); // 先 select 'Derrick'
selectionModel.select('Derrick'); // 再 select 多一次 'Derrick'
console.log(selectionModel.selected); // ['Derrick'] 最后只有一个 'Derrick'

SelectionModel 内部是用 Set 来保存 selected 的,所以它不会出现 duplicated 的情况

deselect

deselect 就是指定去掉某个 selected

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('David', 'Jay');
console.log(selectionModel.selected); // ['David', 'Jay'] selectionModel.deselect('David');
console.log(selectionModel.selected); // ['Jav'] 剩下 'Jay', 'David' 被去掉了

如果指定的不存在,那操作会被无视。

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('David', 'Jay');
console.log(selectionModel.selected); // ['David', 'Jay'] selectionModel.deselect('Stefanie'); // 本来就没有 Stefanie,所以 selected 不会有任何变化,也不会报错,它只是被无视而已。
console.log(selectionModel.selected); // ['David', 'Jay']

toggle

toggle 就是,当前有就 deselect,当前没有就 select。

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('David', 'Jay');
console.log(selectionModel.selected); // ['David', 'Jay'] selectionModel.toggle('Stefanie'); // 当前没有 'Stefanie',所以它会被 select 进去
selectionModel.toggle('Jay'); // 当前有 'Jay',所以它会被 deselect 出去
console.log(selectionModel.selected); // ['David', 'Stefanie']

isSelected

查看当前是否有指定的 selected

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('David', 'Jay');

console.log(selectionModel.isSelected('Jay'));      // true
console.log(selectionModel.isSelected('Stefanie')); // false

hasValue 和 isEmpty

查看当前是否有任何 selected。hasValue 和 isEmpty 用途是一样的,只是表诉不同 (相反) 而已。

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('David', 'Jay');

console.log(selectionModel.hasValue()); // true
console.log(selectionModel.isEmpty()); // false

hasValue = true 就是有 selected,isEmpty 是 true 就是没有 selected (空的意思)。

setSelection

setSelection 就是先 clear 再 select

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('David', 'Jay');

selectionModel.setSelection('Stefanie', 'Richard');

console.log(selectionModel.selected); // ['Stefanie', 'Richard']

相等于

selectionModel.clear();
selectionModel.select('Stefanie', 'Richard');

isMultipleSelection

查看是 single 还是 multiple

const selectionModel = new SelectionModel<string>(true);

console.log(selectionModel.isMultipleSelection()); // true

sort

上面我们有提到,selected 的顺序是依据 select 先来后到排序的,sort 方法可以对 selected 重新排序

const selectionModel = new SelectionModel<string>(true);

selectionModel.select('z');
selectionModel.select('y');
selectionModel.select('x'); console.log(selectionModel.selected); // [ 'z', 'y', 'x' ] 按 select 先来后到 selectionModel.selected.sort((v1 , v2) => v1.localeCompare(v2)); // 按 a-z 排序 console.log(selectionModel.selected); // [ 'x', 'y', 'z' ] 顺序换了

change

change 是一个 RxJS Subject,用来监听 selected 变更

const selectionModel = new SelectionModel<string>(true);

selectionModel.changed.subscribe(({ added, removed, source }) => {
console.log(source === selectionModel); // true, source 就是 selectionModel 对象
console.log(added); // 新添加的 selected
console.log(removed); // 被去掉的 selected
}); selectionModel.select('David', 'Jay'); // added = ['David', 'Jay']
selectionModel.deselect('David'); // removed = ['David']
selectionModel.setSelection('Stefanie'); // added = ['Stefanie'], removed = ['Jay']

每次变更可以获得新添加的 selected,和被去掉的 deselected。

compareWith

用来定义对比 selected 的函数

const selectionModel = new SelectionModel<Date>(true);
selectionModel.select(new Date(1970, 1, 1));
selectionModel.select(new Date(1970, 1, 1));
console.log(selectionModel.selected.length); // 2

默认是 ===,由于 Date 对象是引用类型,所以上面 2 个对象是不相等的 (虽然是同一天)。

const selectionModel = new SelectionModel<Date>(true);
selectionModel.compareWith = (date1, date2) => date1.getTime() === date2.getTime(); // 定义对比的方式 -- 不比对象引用,改比 epoch time selectionModel.select(new Date(1970, 1, 1));
selectionModel.select(new Date(1970, 1, 1));
console.log(selectionModel.selected.length); // 1

加了 compareWith 就相等了。

随便提一嘴。在没有定义 compareWith 的情况下,SelectionModel 内部是用 Set 来维护 selected,用的是 Set 的 compareWith 逻辑。

const values = new Set();
values.add(0);
values.add(-0);
values.add(NaN);
values.add(NaN);
console.log(values.size); // 2

0 和 -1 是相等的,NaN 和 NaN 是相等的。

这些和 ===, Object.is 有所不同哦

console.log('x', 0 === -0);            // true
console.log('x', NaN === NaN); // false console.log('y', Object.is(0, -1)); // false
console.log('y', Object.is(NaN, NaN)); // true

但显然 Set 的符合直觉,所以安心用就可以了。

假如有自定义 compareWith,那 SelectionModel  会那 Set 来 for loop 一个一个用自定义的 compareWith 做对比,找出 selected。

像这样

先 loop 一轮 compare,然后再用 Set 的 has 跑多一轮。

其实这种情况直接用 Array 不要用 Set 性能会更好。看样子它是为了统一管理,或者 compareWith 是后来加上去的逻辑,所以才选择这样实现。

Interactive disabled buttons

当一个 button 被 disabled 以后,用户就完全不能交互了 (click, hover, focus 等都无效)。

注:这里指的是 Angular Material 的 button,游览器原生 button disabled 以后 hover 是可以的,只是 click 和 focus 不行。

很多时候用户是懵的,他知道 disabled 代表不能操作,但是为什么不能操作呢?

这时如果能有一个 tooltips 提示他,体验就完整了。

这就是 Interactive disabled buttons 的用意。

<button mat-fab extended disabled (click)="delete()">
<mat-icon>delete</mat-icon>
Delete
</button> <button mat-fab extended disabled disabledInteractive (click)="delete()">
<mat-icon>delete</mat-icon>
Delete
</button>

有两个 button,第一个是单纯的 disabled,第二个是 interactive disabled。

效果

第一个完全不能交互,第二个看上去不能交互,但其实可以交互 (hoverable, clickable)。

完整版通常长这样

<button mat-fab extended
[disabled]="disabled()" [disabledInteractive]="disabled()"
matTooltip="Can only be deleted when its status is 'Processing'"
[matTooltipDisabled]="!disabled()"
(click)="!disabled() && delete()"
>
<mat-icon>delete</mat-icon>
Delete
</button>

提醒:它可以交互,所以我们需要自己判断是否处理 click 事件哦。

效果

另外,比起单纯的 disabled,button 除了有 class mat-mdc-button-disabled 还多了一个 mat-mdc-button-disabled-interactive。

目录

想查看目录,请移步 Angular 18+ 高级教程 – 目录

Angular Material 18+ 高级教程 – 大杂烩的更多相关文章

  1. Angular Material 教程之布局篇

    Angular Material 教程之布局篇 (一) : 布局简介https://segmentfault.com/a/1190000007215707 Angular Material 教程之布局 ...

  2. Angular Material TreeTable Component 使用教程

    一. 安装 npm i ng-material-treetable --save npm i @angular/material @angular/cdk @angular/animations -- ...

  3. Angular Material design设计

    官网: https://material.io/design/ https://meterial.io/components 优秀的Meterial design站点: http://material ...

  4. Siki_Unity_2-9_C#高级教程(未完)

    Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...

  5. Angular Material Starter App

      介绍 Material Design反映了Google基于Android 5.0 Lollipop操作系统的原生应用UI开发理念,而AngularJS还发起了一个Angular Material ...

  6. 基于 Angular Material 的 Data Grid 设计实现

    自 Extensions 组件库发布以来,Data Grid 成为了使用及咨询最多的组件.最开始 Data Grid 的设计非常简陋,经过一番重构,组件质量有了质的提升. Extensions 组件库 ...

  7. Pandas之:Pandas高级教程以铁达尼号真实数据为例

    Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...

  8. ios cocopods 安装使用及高级教程

    CocoaPods简介 每种语言发展到一个阶段,就会出现相应的依赖管理工具,例如Java语言的Maven,nodejs的npm.随着iOS开发者的增多,业界也出现了为iOS程序提供依赖管理的工具,它的 ...

  9. 【读书笔记】.Net并行编程高级教程(二)-- 任务并行

    前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...

  10. 【读书笔记】.Net并行编程高级教程--Parallel

    一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机 ...

随机推荐

  1. Solo 开发者周刊 (第9期):Dawwin首位人工智能编程师或将改变未来?

    这里会整合 Solo 社区每周推广内容.产品模块或活动投稿,每周五发布.在这期周刊中,我们将深入探讨开源软件产品的开发旅程,分享来自一线独立开发者的经验和见解.本杂志开源,欢迎投稿. 好文推荐 Daw ...

  2. redis基本数据结构-集合set

    redis基本数据结构-集合set 特性 一个集合键最多存储 2^32 - 1 个字符串值 元素在集合内无序(哈希表-链地址法解决冲突) 元素在集合内唯一 向集合添加元素 sadd key value ...

  3. ProgressBar 进度控件

    在 VB.NET 中,你可以使用 ProgressBar 控件或者自定义的进度提示方法来实现这个功能.以下是一个示例代码,展示如何使用 ProgressBar 控件来显示导入情况: ' 创建一个 Pr ...

  4. 图的存储、创建、遍历、求最小生成树、最短路径(Java)

    带权无向图 存储结构 存储结构选用邻接表. 当一个图为稀疏图时,使用邻接矩阵法显然要浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费. 当然,即使我们所处理的 ...

  5. Top monitor[2.1.11] 开源

    说明 本项目将转为开源项目. \(\text{GitHub}\) 仓库下载链接 \(|\) 项目初始化下载 \(|\) 项目编辑文件下载. 项目说明 声明:此项目由 \(\text{So_noSlac ...

  6. JavaScript一天一个算法题~持续更新中。。。。。

    1,数组去重 i.暴力去重 思路:建一个空数组,通过判断原数组的元素是否在空数组内,如果在,不放入,不在,放入空数组. function clearCommnetArray(array){ let a ...

  7. appium模拟键盘事件

    原方法: 1 def press_keycode(self, keycode, metastate=None): 2 """Sends a keycode to the ...

  8. 商业级java开发单体项目环境搭建

    写在开发前,端口经常被战占,windows10用以下命令先查先杀,netstat -aon | findstr "8080" taskkill /F /PID <PID> ...

  9. midjourney 生成相似类型图片

    生成类似形象 midjourney 核心就是一次运行3次图片,多尝试 上传此图片到discord里的mj -> 复制图片的link -> 使用describe拆词 也可以自己手动拆词,人工 ...

  10. 【Docker】09 部署挂载本地目录的Redis

    1.拉取Redis镜像: docker pull redis:6.0.6 2.执行挂载命令: docker run -d \ --name=redis \ --restart=always \ --p ...