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>
posted @ 2022-03-07 17:52  猫神甜辣酱  阅读(470)  评论(0编辑  收藏  举报