系统中的组件与配置都在config中
通过config.ts声明了各种类型的config类型
export interface NzConfig {
affix?: AffixConfig;
select?: SelectConfig;
alert?: AlertConfig;
anchor?: AnchorConfig;
avatar?: AvatarConfig;
backTop?: BackTopConfig;
badge?: BadgeConfig;
button?: ButtonConfig;
card?: CardConfig;
carousel?: CarouselConfig;
cascader?: CascaderConfig;
codeEditor?: CodeEditorConfig;
collapse?: CollapseConfig;
collapsePanel?: CollapsePanelConfig;
datePicker?: DatePickerConfig;
descriptions?: DescriptionsConfig;
drawer?: DrawerConfig;
empty?: EmptyConfig;
form?: FormConfig;
icon?: IconConfig;
message?: MessageConfig;
modal?: ModalConfig;
notification?: NotificationConfig;
pageHeader?: PageHeaderConfig;
pagination?: PaginationConfig;
progress?: ProgressConfig;
rate?: RateConfig;
space?: SpaceConfig;
spin?: SpinConfig;
switch?: SwitchConfig;
table?: TableConfig;
tabs?: TabsConfig;
timePicker?: TimePickerConfig;
tree?: TreeConfig;
treeSelect?: TreeSelectConfig;
typography?: TypographyConfig;
image?: ImageConfig;
popconfirm?: PopConfirmConfig;
popover?: PopoverConfig;
}
可以通过NzConfigService获取系统中的组件配置
export class NzConfigService {
private configUpdated$ = new Subject<keyof NzConfig>();
/** Global config holding property. */
private config: NzConfig;
constructor(@Optional() @Inject(NZ_CONFIG) defaultConfig?: NzConfig) {
this.config = defaultConfig || {};
}
getConfigForComponent<T extends NzConfigKey>(componentName: T): NzConfig[T] {
return this.config[componentName];
}
// get observable data
getConfigChangeEventForComponent(componentName: NzConfigKey): Observable<void> {
return this.configUpdated$.pipe(
filter(n => n === componentName),
mapTo(undefined)
);
}
// set property value of config
set<T extends NzConfigKey>(componentName: T, value: NzConfig[T]): void {
this.config[componentName] = { ...this.config[componentName], ...value };
this.configUpdated$.next(componentName);
}
}
对一些系统变量的设置,但是只有测试模式设置
export const environment = {
isTestMode: false
};
管道中,先对字符串做转义,具体方法如下:关于unicode编码的知识可以查看这篇文章,彻底弄懂Unicode编码
// Regular Expressions for parsing tags and attributes
const SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
// ! to ~ is the ASCII range.
const NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
/**
* Escapes all potentially dangerous characters, so that the
* resulting string can be safely inserted into attribute or
* element text.
*/
function encodeEntities(value: string): string {
return value
.replace(/&/g, '&')
.replace(SURROGATE_PAIR_REGEXP, (match: string) => {
const hi = match.charCodeAt(0);
const low = match.charCodeAt(1);
return `&#${(hi - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000};`;
})
.replace(NON_ALPHANUMERIC_REGEXP, (match: string) => `&#${match.charCodeAt(0)};`)
.replace(/</g, '<')
.replace(/>/g, '>');
}
UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF),要么是 4 个字节(U+010000 到 U+10FFFF)。那么问题来了,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?
这里有一个很巧妙的地方,在基本平面内,从 U+D800 到 U+DFFF 是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。
辅助平面的字符位共有 2^20 个,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF(空间大小 2^10),称为高位(H),后 10 位映射在 U+DC00 到 U+DFFF(空间大小 2^10),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
因此,当我们遇到两个字节,发现它的码点在 U+D800 到 U+DBFF 之间,就可以断定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF 之间,这四个字节必须放在一起解读。
Unicode3.0 中给出了辅助平面字符的转换公式:
H = Math.floor((c-0x10000) / 0x400)+0xD800
L = (c - 0x10000) % 0x400 + 0xDC00
Surrogate Pair 是 UTF-16 中用于扩展字符而使用的编码方式,是一种采用四个字节(两个 UTF-16 编码)来表示一个字符,称作代理对。
‘ABC’.codePointAt(1); // 66
‘\uD800\uDC00’.codePointAt(0); // 65536
‘XYZ’.codePointAt(42); // undefined
从注释中可以看出是获取一个组件的元素
/**
* A patch directive to select the element of a component.
*/
@Directive({
selector: '[nzElement], [nz-element]',
exportAs: 'nzElement'
})
export class NzElementPatchDirective {
get nativeElement(): NzSafeAny {
return this.elementRef.nativeElement;
}
constructor(public elementRef: ElementRef) {}
}
主要是记录程序在调试模式下面的一些日志记录
warn()
log()
import { isDevMode } from '@angular/core';
import { environment } from 'ng-zorro-antd/core/environments';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
const record: Record<string, boolean> = {};
export const PREFIX = '[NG-ZORRO]:';
function notRecorded(...args: NzSafeAny[]): boolean {
const asRecord = args.reduce((acc, c) => acc + c.toString(), '');
if (record[asRecord]) {
return false;
} else {
record[asRecord] = true;
return true;
}
}
function consoleCommonBehavior(consoleFunc: (...args: NzSafeAny) => void, ...args: NzSafeAny[]): void {
if (environment.isTestMode || (isDevMode() && notRecorded(...args))) {
consoleFunc(...args);
}
}
// Warning should only be printed in dev mode and only once.
export const warn = (...args: NzSafeAny[]) => consoleCommonBehavior((...arg: NzSafeAny[]) => console.warn(PREFIX, ...arg), ...args);
export const warnDeprecation = (...args: NzSafeAny[]) => {
if (!environment.isTestMode) {
const stack = new Error().stack;
return consoleCommonBehavior((...arg: NzSafeAny[]) => console.warn(PREFIX, 'deprecated:', ...arg, stack), ...args);
} else {
return () => {};
}
};
// Log should only be printed in dev mode.
export const log = (...args: NzSafeAny[]) => {
if (isDevMode()) {
console.log(PREFIX, ...args);
}
};
顾名思义就是消除组件动画,具体实现是判断该组件是否禁止动画,如果禁止动画将添加nz-animate-disabled
const DISABLED_CLASSNAME = 'nz-animate-disabled';
export class NzNoAnimationDirective implements OnChanges, AfterViewInit {
static ngAcceptInputType_nzNoAnimation: BooleanInput;
@Input() @InputBoolean() nzNoAnimation: boolean = false;
constructor(
private element: ElementRef,
private renderer: Renderer2,
@Optional() @Inject(ANIMATION_MODULE_TYPE) private animationType: string
) {}
ngOnChanges(): void {
this.updateClass();
}
ngAfterViewInit(): void {
this.updateClass();
}
private updateClass(): void {
const element = coerceElement(this.element);
if (!element) {
return;
}
if (this.nzNoAnimation || this.animationType === 'NoopAnimations') {
this.renderer.addClass(element, DISABLED_CLASSNAME);
} else {
this.renderer.removeClass(element, DISABLED_CLASSNAME);
}
}
}
render2 对比 ViewChild() ElementRef
Angular 正确使用 @ViewChild、@ViewChildren 访问 DOM、组件、指令 - 简书 (jianshu.com)
string_template_outlet
export class NzStringTemplateOutletDirective<_T = unknown> implements OnChanges {
private embeddedViewRef: EmbeddedViewRef<NzSafeAny> | null = null;
private context = new NzStringTemplateOutletContext();
@Input() nzStringTemplateOutletContext: NzSafeAny | null = null;
@Input() nzStringTemplateOutlet: NzSafeAny | TemplateRef<NzSafeAny> = null;
static ngTemplateContextGuard<T>(_dir: NzStringTemplateOutletDirective<T>, _ctx: NzSafeAny): _ctx is NzStringTemplateOutletContext {
return true;
}
private recreateView(): void {
this.viewContainer.clear();
const isTemplateRef = this.nzStringTemplateOutlet instanceof TemplateRef;
const templateRef = (isTemplateRef ? this.nzStringTemplateOutlet : this.templateRef) as NzSafeAny;
this.embeddedViewRef = this.viewContainer.createEmbeddedView(
templateRef,
isTemplateRef ? this.nzStringTemplateOutletContext : this.context
);
}
private updateContext(): void {
const isTemplateRef = this.nzStringTemplateOutlet instanceof TemplateRef;
const newCtx = isTemplateRef ? this.nzStringTemplateOutletContext : this.context;
const oldCtx = this.embeddedViewRef!.context as NzSafeAny;
if (newCtx) {
for (const propName of Object.keys(newCtx)) {
oldCtx[propName] = newCtx[propName];
}
}
}
constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef<NzSafeAny>) {}
// change update
ngOnChanges(changes: SimpleChanges): void {
const { nzStringTemplateOutletContext, nzStringTemplateOutlet } = changes;
const shouldRecreateView = (): boolean => {
let shouldOutletRecreate = false;
if (nzStringTemplateOutlet) {
if (nzStringTemplateOutlet.firstChange) {
shouldOutletRecreate = true;
} else {
// detect the content change
const isPreviousOutletTemplate = nzStringTemplateOutlet.previousValue instanceof TemplateRef;
const isCurrentOutletTemplate = nzStringTemplateOutlet.currentValue instanceof TemplateRef;
shouldOutletRecreate = isPreviousOutletTemplate || isCurrentOutletTemplate;
}
}
const hasContextShapeChanged = (ctxChange: SimpleChange): boolean => {
const prevCtxKeys = Object.keys(ctxChange.previousValue || {});
const currCtxKeys = Object.keys(ctxChange.currentValue || {});
// compare the length of each context
if (prevCtxKeys.length === currCtxKeys.length) {
// iterate context
for (const propName of currCtxKeys) {
// change detection
if (prevCtxKeys.indexOf(propName) === -1) {
return true;
}
}
return false;
} else {
return true;
}
};
const shouldContextRecreate = nzStringTemplateOutletContext && hasContextShapeChanged(nzStringTemplateOutletContext);
return shouldContextRecreate || shouldOutletRecreate;
};
if (nzStringTemplateOutlet) {
this.context.$implicit = nzStringTemplateOutlet.currentValue;
}
const recreateView = shouldRecreateView();
// need recreate view
if (recreateView) {
/** recreate view when context shape or outlet change **/
this.recreateView();
} else {
/** update context **/
this.updateContext();
}
}
}
export class NzStringTemplateOutletContext {
public $implicit: NzSafeAny;
}
用于呈现悬浮层元素
主要元素overlay-position.ts 计算悬浮元素的位置
import { ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay';
// position map
export const POSITION_MAP: { [key: string]: ConnectionPositionPair } = {
top: new ConnectionPositionPair({ originX: 'center', originY: 'top' }, { overlayX: 'center', overlayY: 'bottom' }),
topCenter: new ConnectionPositionPair({ originX: 'center', originY: 'top' }, { overlayX: 'center', overlayY: 'bottom' }),
topLeft: new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }),
topRight: new ConnectionPositionPair({ originX: 'end', originY: 'top' }, { overlayX: 'end', overlayY: 'bottom' }),
right: new ConnectionPositionPair({ originX: 'end', originY: 'center' }, { overlayX: 'start', overlayY: 'center' }),
rightTop: new ConnectionPositionPair({ originX: 'end', originY: 'top' }, { overlayX: 'start', overlayY: 'top' }),
rightBottom: new ConnectionPositionPair({ originX: 'end', originY: 'bottom' }, { overlayX: 'start', overlayY: 'bottom' }),
bottom: new ConnectionPositionPair({ originX: 'center', originY: 'bottom' }, { overlayX: 'center', overlayY: 'top' }),
bottomCenter: new ConnectionPositionPair({ originX: 'center', originY: 'bottom' }, { overlayX: 'center', overlayY: 'top' }),
bottomLeft: new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }),
bottomRight: new ConnectionPositionPair({ originX: 'end', originY: 'bottom' }, { overlayX: 'end', overlayY: 'top' }),
left: new ConnectionPositionPair({ originX: 'start', originY: 'center' }, { overlayX: 'end', overlayY: 'center' }),
leftTop: new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'end', overlayY: 'top' }),
leftBottom: new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'end', overlayY: 'bottom' })
};
export const DEFAULT_TOOLTIP_POSITIONS = [POSITION_MAP.top, POSITION_MAP.right, POSITION_MAP.bottom, POSITION_MAP.left];
export const DEFAULT_CASCADER_POSITIONS = [POSITION_MAP.bottomLeft, POSITION_MAP.bottomRight, POSITION_MAP.topLeft, POSITION_MAP.topRight];
export const DEFAULT_MENTION_TOP_POSITIONS = [
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'bottom' }),
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'end', overlayY: 'bottom' })
];
export const DEFAULT_MENTION_BOTTOM_POSITIONS = [
POSITION_MAP.bottomLeft,
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'end', overlayY: 'top' })
];
export function getPlacementName(position: ConnectedOverlayPositionChange): string | undefined {
// position don't change
for (const placement in POSITION_MAP) {
if (
position.connectionPair.originX === POSITION_MAP[placement].originX &&
position.connectionPair.originY === POSITION_MAP[placement].originY &&
position.connectionPair.overlayX === POSITION_MAP[placement].overlayX &&
position.connectionPair.overlayY === POSITION_MAP[placement].overlayY
) {
return placement;
}
}
return undefined;
}