ng-zorro UI源码技巧(二)
nz-button的点击效果
我们可以添加给自己的自定义nz-wave
指令
我觉得源码不错, 扣过来
nz-wave-renderer.ts
/**
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
*/
import { Platform } from '@angular/cdk/platform';
import { NgZone } from '@angular/core';
export class NzWaveRenderer {
private waveTransitionDuration = 400;
private styleForPseudo: HTMLStyleElement | null = null;
private extraNode: HTMLDivElement | null = null;
private lastTime = 0;
private platform!: Platform;
clickHandler: (event: MouseEvent) => void;
get waveAttributeName(): string {
return this.insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node';
}
constructor(
private triggerElement: HTMLElement,
private ngZone: NgZone,
private insertExtraNode: boolean,
private platformId: any
) {
this.platform = new Platform(this.platformId);
this.clickHandler = this.onClick.bind(this);
this.bindTriggerEvent();
}
onClick = (event: MouseEvent): void => {
if (
!this.triggerElement ||
!this.triggerElement.getAttribute ||
this.triggerElement.getAttribute('disabled') ||
(event.target as HTMLElement).tagName === 'INPUT' ||
this.triggerElement.className.indexOf('disabled') >= 0
) {
return;
}
this.fadeOutWave();
};
bindTriggerEvent(): void {
if (this.platform.isBrowser) {
this.ngZone.runOutsideAngular(() => {
this.removeTriggerEvent();
if (this.triggerElement) {
this.triggerElement.addEventListener('click', this.clickHandler, true);
}
});
}
}
removeTriggerEvent(): void {
if (this.triggerElement) {
this.triggerElement.removeEventListener('click', this.clickHandler, true);
}
}
removeStyleAndExtraNode(): void {
if (this.styleForPseudo && document.body.contains(this.styleForPseudo)) {
document.body.removeChild(this.styleForPseudo);
this.styleForPseudo = null;
}
if (this.insertExtraNode && this.triggerElement.contains(this.extraNode)) {
this.triggerElement.removeChild(this.extraNode as Node);
}
}
destroy(): void {
this.removeTriggerEvent();
this.removeStyleAndExtraNode();
}
private fadeOutWave(): void {
const node = this.triggerElement;
const waveColor = this.getWaveColor(node);
node.setAttribute(this.waveAttributeName, 'true');
if (Date.now() < this.lastTime + this.waveTransitionDuration) {
return;
}
if (this.isValidColor(waveColor)) {
if (!this.styleForPseudo) {
this.styleForPseudo = document.createElement('style');
}
this.styleForPseudo.innerHTML = `
[ant-click-animating-without-extra-node='true']::after, .ant-click-animating-node {
--antd-wave-shadow-color: ${waveColor};
}`;
document.body.appendChild(this.styleForPseudo);
}
if (this.insertExtraNode) {
if (!this.extraNode) {
this.extraNode = document.createElement('div');
}
this.extraNode.className = 'ant-click-animating-node';
node.appendChild(this.extraNode);
}
this.lastTime = Date.now();
this.runTimeoutOutsideZone(() => {
node.removeAttribute(this.waveAttributeName);
this.removeStyleAndExtraNode();
}, this.waveTransitionDuration);
}
private isValidColor(color: string): boolean {
return (
!!color &&
color !== '#ffffff' &&
color !== 'rgb(255, 255, 255)' &&
this.isNotGrey(color) &&
!/rgba\(\d*, \d*, \d*, 0\)/.test(color) &&
color !== 'transparent'
);
}
private isNotGrey(color: string): boolean {
const match = color.match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/);
if (match && match[1] && match[2] && match[3]) {
return !(match[1] === match[2] && match[2] === match[3]);
}
return true;
}
private getWaveColor(node: HTMLElement): string {
const nodeStyle = getComputedStyle(node);
return (
nodeStyle.getPropertyValue('border-top-color') || // Firefox Compatible
nodeStyle.getPropertyValue('border-color') ||
nodeStyle.getPropertyValue('background-color')
);
}
private runTimeoutOutsideZone(fn: () => void, delay: number): void {
this.ngZone.runOutsideAngular(() => setTimeout(fn, delay));
}
}
nz-wave.directive.ts
/**
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
*/
import {
Directive,
ElementRef,
Inject,
InjectionToken,
Input,
NgZone,
OnDestroy,
OnInit,
Optional,
PLATFORM_ID
} from '@angular/core';
import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';
import { NzWaveRenderer } from './nz-wave-renderer';
export interface NzWaveConfig {
disabled?: boolean;
}
export const NZ_WAVE_GLOBAL_DEFAULT_CONFIG: NzWaveConfig = {
disabled: false
};
export const NZ_WAVE_GLOBAL_CONFIG = new InjectionToken<NzWaveConfig>('nz-wave-global-options', {
providedIn: 'root',
factory: NZ_WAVE_GLOBAL_CONFIG_FACTORY
});
export function NZ_WAVE_GLOBAL_CONFIG_FACTORY(): NzWaveConfig {
return NZ_WAVE_GLOBAL_DEFAULT_CONFIG;
}
@Directive({
selector: '[yl-wave]',
exportAs: 'ylWave'
})
export class NzWaveDirective implements OnInit, OnDestroy {
@Input() nzWaveExtraNode = false;
private waveRenderer?: NzWaveRenderer;
private waveDisabled: boolean = false;
get disabled(): boolean {
return this.waveDisabled;
}
get rendererRef(): NzWaveRenderer | undefined {
return this.waveRenderer;
}
constructor(
private ngZone: NgZone,
private elementRef: ElementRef,
@Optional() @Inject(NZ_WAVE_GLOBAL_CONFIG) private config: NzWaveConfig,
@Optional() @Inject(ANIMATION_MODULE_TYPE) private animationType: string,
@Inject(PLATFORM_ID) private platformId: any
) {
this.waveDisabled = this.isConfigDisabled();
}
isConfigDisabled(): boolean {
let disabled = false;
if (this.config && typeof this.config.disabled === 'boolean') {
disabled = this.config.disabled;
}
if (this.animationType === 'NoopAnimations') {
disabled = true;
}
return disabled;
}
ngOnDestroy(): void {
if (this.waveRenderer) {
this.waveRenderer.destroy();
}
}
ngOnInit(): void {
this.renderWaveIfEnabled();
}
renderWaveIfEnabled(): void {
if (!this.waveDisabled && this.elementRef.nativeElement) {
this.waveRenderer = new NzWaveRenderer(
this.elementRef.nativeElement,
this.ngZone,
this.nzWaveExtraNode,
this.platformId
);
}
}
disable(): void {
this.waveDisabled = true;
if (this.waveRenderer) {
this.waveRenderer.removeTriggerEvent();
this.waveRenderer.removeStyleAndExtraNode();
}
}
enable(): void {
// config priority
this.waveDisabled = this.isConfigDisabled() || false;
if (this.waveRenderer) {
this.waveRenderer.bindTriggerEvent();
}
}
}
<div yl-wave style=" color: #fff;
border-color: #ff4d4f;border-radius: 2px;
background: #ff4d4f; margin-right: 8px;
margin-bottom: 12px; margin-left:400px;padding:20px;
text-shadow: 0 -1px 0 rgba(0 0 0 .12);display: inline-block;
box-shadow: 0 2px #0000000b;">Primary</div>
:not()
匹配不符合一组选择器的元素
/* 既不是 <div> 也不是 <span> 的元素 */
body :not(div):not(span) {
font-weight: bold;
}
/* 既不是[type='text'] 也不是 [type='number'] */
button:[type='text']:[type='number']
国际化修改后,其他页面想检测到变化
import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class Report2Service {
_locale = 'aaa';
private _change = new BehaviorSubject<any>(this._locale);
constructor() {
}
get localeChange(): Observable<any> {
return this._change.asObservable();
}
// 修改
setLocal(local: string): void {
if (this._locale && this._locale === local) {
return;
}
this._locale = local;
this._change.next(local);
}
}
其他页面检测变化
ngOnInit(): void {
this.report2Service.localeChange.pipe(
takeUntil(this.destroy$)
).subscribe(res => {
console.log(res);
this.cdr.markForCheck();
});
}
ngTemplateOutlet 源码探索
@Directive({
selector: '[ylTemplateOutlet]'
})
export class YlTemplateOutletDirective implements OnChanges {
public _viewRef: EmbeddedViewRef<any> | null = null;
//context对象中使用key ' $implicit '将其值设为默认值。
@Input() public ylTemplateOutletContext: object | null = null;
// 一个定义模板引用和可选的模板上下文对象的字符串。
@Input() public ylTemplateOutlet: TemplateRef<any> | null = null;
constructor(private _viewContainerRef: ViewContainerRef) {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['ylTemplateOutlet']) {
const viewContainer = this._viewContainerRef;
// 判断之前引入的对象有没有值
if (this._viewRef) {
viewContainer.remove(viewContainer.indexOf(this._viewRef))
}
this._viewRef = this.ylTemplateOutlet ?
viewContainer.createEmbeddedView(this.ylTemplateOutlet, this.ylTemplateOutletContext) :
null;
} else if (
this._viewRef && changes['ylTemplateOutlet'] && this.ylTemplateOutletContext
) {
this._viewRef.context = this.ylTemplateOutletContext;
}
}
}
例子
<ng-container *ylTemplateOutlet="bbb;context:{$implicit:'测试'}"></ng-container>
<ng-template #bbb let-names>
<div>
<h1>2222--{{names}}</h1>
</div>
</ng-template>
刚开始比较疑惑, 为啥用$implicit
在上下文对象中使用键$implicit
会将其值设置为默认值。
vcRef.createEmbeddedView(template, { $implicit: 'value' })
模板
<ng-template let-foo>
{{ foo }}
</ng-template>
比如我们修改, 上下文
{ bar: 'value' }
我们必须像这样声明变量:
let-foo="bar"
输入框输入
<textarea #textarea></textarea>
private textarea$ = new BehaviorSubject<ElementRef<HTMLTextAreaElement> | null>(null);
@ViewChild('textarea')
set textarea(textDOM: ElementRef<HTMLTextAreaElement> | undefined) {
if (textDOM) {
this.textarea$.next(textDOM)
}
}
ngOnInit(): void {
const textarea = this.textarea$.pipe(
filter((textDOM):textDOM is ElementRef<HTMLTextAreaElement> => textDOM !== null)
);
textarea.pipe(
switchMap(
textDOM => new Observable(subscriber => {
// 疑惑这个的使用场景,发现源码很多地方这样用
this.ngZone.runOutsideAngular(() =>
fromEvent(textDOM?.nativeElement, 'keydown').subscribe(subscriber)
)
})
)
).subscribe(console.log)
}
ngZone
ngZone.runOutsideAngular 不触发脏检测执行
this.ngZone.runOutsideAngular(() => {
fromEvent<MouseEvent>(this.elementRef.nativeElement!, 'mouseenter')
.pipe(mapTo(this.resizeRef.origin.nativeElement!), takeUntil(this.destroyed))
.subscribe(cell => this.eventDispatcher.headerCellHovered.next(cell));
fromEvent<MouseEvent>(this.elementRef.nativeElement!, 'mouseleave')
fromEvent<MouseEvent>(this.elementRef.nativeElement!, 'mousedown')
});
我们发现对于dom的一些操作,可以用runOutsideAngular
以减少消耗
ngZone.run 触发脏检测执行
this.ngZone.run(() => {
const sizeMessage = {columnId: this.columnDef.name, size};
if (completedSuccessfully) {
this.resizeNotifier.resizeCompleted.next(sizeMessage);
} else {
this.resizeNotifier.resizeCanceled.next(sizeMessage);
}
});
立即执行的条件或者函数
父传class到子
export class TwoComponent implements OnInit {
// 父元素设置class 传入子元素
@Input('class')
set classList(value: string | string[]) {
if (value && value.length) {
this._classList = coerceStringArray(value).reduce((classList, className) => {
classList[className] = true;
return classList
}, {} as { [key: string]: boolean })
} else {
this._classList = {};
}
}
_classList: { [key: string]: boolean } = {};
}
<div [ngClass]="_classList"></div>
决定自己的高度的是你的态度,而不是你的才能
记得我们是终身初学者和学习者
总有一天我也能成为大佬