Skip to content

Commit

Permalink
feat(module:notification): add onClick observable (#4989)
Browse files Browse the repository at this point in the history
close #4986

BREAKING CHANGE: 


- NzMessageDataFilled is replaced by NzMessageRef
- NzNotificationDataFilled is replaced by NzNotificationRef
  • Loading branch information
Wendell authored Apr 14, 2020
1 parent 87b8e55 commit 9224240
Show file tree
Hide file tree
Showing 18 changed files with 481 additions and 582 deletions.
228 changes: 228 additions & 0 deletions components/message/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/**
* @license
* Copyright Alibaba.com All Rights Reserved.
*
* 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 { ComponentType, Overlay } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { ChangeDetectorRef, EventEmitter, Injector, OnDestroy, OnInit } from '@angular/core';
import { MessageConfig, NzConfigService } from 'ng-zorro-antd/core/config';
import { NzSingletonService } from 'ng-zorro-antd/core/services';
import { Subject } from 'rxjs';

import { NzMessageData, NzMessageDataOptions } from './typings';

let globalCounter = 0;

export abstract class NzMNService {
protected abstract componentPrefix: string;
protected container: NzMNContainerComponent;

constructor(protected nzSingletonService: NzSingletonService, protected overlay: Overlay, private injector: Injector) {}

remove(id?: string): void {
if (id) {
this.container.remove(id);
} else {
this.container.removeAll();
}
}

protected getInstanceId(): string {
return `${this.componentPrefix}-${globalCounter++}`;
}

protected withContainer<T extends NzMNContainerComponent>(ctor: ComponentType<T>): T {
let containerInstance = this.nzSingletonService.getSingletonWithKey(this.componentPrefix);
if (containerInstance) {
return containerInstance as T;
}

const overlayRef = this.overlay.create({
hasBackdrop: false,
scrollStrategy: this.overlay.scrollStrategies.noop(),
positionStrategy: this.overlay.position().global()
});
const componentPortal = new ComponentPortal(ctor, null, this.injector);
const componentRef = overlayRef.attach(componentPortal);
const overlayPane = overlayRef.overlayElement;
overlayPane.style.zIndex = '1010';

if (!containerInstance) {
this.container = containerInstance = componentRef.instance;
this.nzSingletonService.registerSingletonWithKey(this.componentPrefix, containerInstance);
}

return containerInstance as T;
}
}

export abstract class NzMNContainerComponent implements OnInit, OnDestroy {
config: Required<MessageConfig>;
instances: Array<Required<NzMessageData>> = [];

protected readonly destroy$ = new Subject<void>();

constructor(protected cdr: ChangeDetectorRef, protected nzConfigService: NzConfigService) {
this.updateConfig();
}

ngOnInit(): void {
this.subscribeConfigChange();
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}

create(data: NzMessageData): Required<NzMessageData> {
const instance = this.onCreate(data);

if (this.instances.length >= this.config.nzMaxStack) {
this.instances = this.instances.slice(1);
}

this.instances = [...this.instances, instance];

this.readyInstances();

return instance;
}

remove(id: string, userAction: boolean = false): void {
this.instances.some((instance, index) => {
if (instance.messageId === id) {
this.instances.splice(index, 1);
this.instances = [...this.instances];
this.onRemove(instance, userAction);
this.readyInstances();
return true;
}
return false;
});
}

removeAll(): void {
this.instances.forEach(i => this.onRemove(i, false));
this.instances = [];

this.readyInstances();
}

protected onCreate(instance: NzMessageData): Required<NzMessageData> {
instance.options = this.mergeOptions(instance.options);
instance.onClose = new Subject<boolean>();
return instance as Required<NzMessageData>;
}

protected onRemove(instance: Required<NzMessageData>, userAction: boolean): void {
instance.onClose.next(userAction);
instance.onClose.complete();
}

protected readyInstances(): void {
this.cdr.detectChanges();
}

protected abstract updateConfig(): void;

protected abstract subscribeConfigChange(): void;

protected mergeOptions(options?: NzMessageDataOptions): NzMessageDataOptions {
const { nzDuration, nzAnimate, nzPauseOnHover } = this.config;
return { nzDuration, nzAnimate, nzPauseOnHover, ...options };
}
}

export abstract class NzMNComponent implements OnInit, OnDestroy {
instance: Required<NzMessageData>;
index: number;

readonly destroyed = new EventEmitter<{ id: string; userAction: boolean }>();

protected options: Required<NzMessageDataOptions>;
protected autoClose: boolean;
protected eraseTimer: number | null = null;
protected eraseTimingStart: number;
protected eraseTTL: number;

constructor(protected cdr: ChangeDetectorRef) {}

ngOnInit(): void {
this.options = this.instance.options as Required<NzMessageDataOptions>;

if (this.options.nzAnimate) {
this.instance.state = 'enter';
}

this.autoClose = this.options.nzDuration > 0;

if (this.autoClose) {
this.initErase();
this.startEraseTimeout();
}
}

ngOnDestroy(): void {
if (this.autoClose) {
this.clearEraseTimeout();
}
}

onEnter(): void {
if (this.autoClose && this.options.nzPauseOnHover) {
this.clearEraseTimeout();
this.updateTTL();
}
}

onLeave(): void {
if (this.autoClose && this.options.nzPauseOnHover) {
this.startEraseTimeout();
}
}

protected destroy(userAction: boolean = false): void {
if (this.options.nzAnimate) {
this.instance.state = 'leave';
this.cdr.detectChanges();
setTimeout(() => {
this.destroyed.next({ id: this.instance.messageId, userAction: userAction });
}, 200);
} else {
this.destroyed.next({ id: this.instance.messageId, userAction: userAction });
}
}

private initErase(): void {
this.eraseTTL = this.options.nzDuration;
this.eraseTimingStart = Date.now();
}

private updateTTL(): void {
if (this.autoClose) {
this.eraseTTL -= Date.now() - this.eraseTimingStart;
}
}

private startEraseTimeout(): void {
if (this.eraseTTL > 0) {
this.clearEraseTimeout();
this.eraseTimer = setTimeout(() => this.destroy(), this.eraseTTL);
this.eraseTimingStart = Date.now();
} else {
this.destroy();
}
}

private clearEraseTimeout(): void {
if (this.eraseTimer !== null) {
clearTimeout(this.eraseTimer);
this.eraseTimer = null;
}
}
}
5 changes: 3 additions & 2 deletions components/message/doc/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ You can use `NzConfigService` to configure this component globally. Please check
| nzAnimate | Whether to turn on animation | `boolean` | `true` |
| nzTop | Distance from top | `number \| string` | `24` |

### NzMessageDataFilled
### NzMessageRef

It's the object that returned when you call `NzMessageService.success` and others.

```ts
export interface NzMessageDataFilled {
export interface NzMessageRef {
messageId: string;
onClose: Subject<false>; // It would emit an event when the message is closed
}
```
5 changes: 3 additions & 2 deletions components/message/doc/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ import { NzMessageModule } from 'ng-zorro-antd/message';
| nzAnimate | 开关动画效果 | `boolean` | `true` |
| nzTop | 消息距离顶部的位置 | `number \| string` | `24` |

### NzMessageDataFilled
### NzMessageRef

当你调用 `NzMessageService.success` 或其他方法时会返回该对象。

```ts
export interface NzMessageDataFilled {
export interface NzMessageRef {
messageId: string;
onClose: Subject<false>; // 当 message 关闭时它会派发一个事件
}
```
99 changes: 15 additions & 84 deletions components/message/message-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
*/

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewEncapsulation } from '@angular/core';
import { MessageConfig, NzConfigService } from 'ng-zorro-antd/core/config';
import { toCssPixel } from 'ng-zorro-antd/core/util';
import { Subject } from 'rxjs';

import { takeUntil } from 'rxjs/operators';
import { NzMessageDataFilled, NzMessageDataOptions } from './typings';
import { NzMNContainerComponent } from './base';
import { NzMessageData } from './typings';

const NZ_CONFIG_COMPONENT_NAME = 'message';

const NZ_MESSAGE_DEFAULT_CONFIG: Required<MessageConfig> = {
nzAnimate: true,
nzDuration: 3000,
Expand All @@ -31,78 +33,17 @@ const NZ_MESSAGE_DEFAULT_CONFIG: Required<MessageConfig> = {
preserveWhitespaces: false,
template: `
<div class="ant-message" [style.top]="top">
<nz-message
*ngFor="let message of messages"
[nzMessage]="message"
(messageDestroy)="removeMessage($event.id, $event.userAction)"
></nz-message>
<nz-message *ngFor="let instance of instances" [instance]="instance" (destroyed)="remove($event.id, $event.userAction)"></nz-message>
</div>
`
})
export class NzMessageContainerComponent implements OnInit, OnDestroy {
destroy$ = new Subject<void>();
messages: NzMessageDataFilled[] = [];
config: Required<MessageConfig>;
export class NzMessageContainerComponent extends NzMNContainerComponent {
readonly destroy$ = new Subject<void>();
instances: Array<Required<NzMessageData>> = [];
top: string | null;

constructor(protected cdr: ChangeDetectorRef, protected nzConfigService: NzConfigService) {
this.updateConfig();
}

ngOnInit(): void {
this.subscribeConfigChange();
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}

/**
* Create a new message.
* @param message Parsed message configuration.
*/
createMessage(message: NzMessageDataFilled): void {
if (this.messages.length >= this.config.nzMaxStack) {
this.messages = this.messages.slice(1);
}
message.options = this.mergeMessageOptions(message.options);
message.onClose = new Subject<boolean>();
this.messages = [...this.messages, message];
this.cdr.detectChanges();
}

/**
* Remove a message by `messageId`.
* @param messageId Id of the message to be removed.
* @param userAction Whether this is closed by user interaction.
*/
removeMessage(messageId: string, userAction: boolean = false): void {
this.messages.some((message, index) => {
if (message.messageId === messageId) {
this.messages.splice(index, 1);
this.messages = [...this.messages];
this.cdr.detectChanges();
message.onClose!.next(userAction);
message.onClose!.complete();
return true;
}
return false;
});
}

/**
* Remove all messages.
*/
removeMessageAll(): void {
this.messages = [];
this.cdr.detectChanges();
}

protected updateConfig(): void {
this.config = this.updateConfigFromConfigService();
this.top = toCssPixel(this.config.nzTop);
this.cdr.markForCheck();
constructor(cdr: ChangeDetectorRef, nzConfigService: NzConfigService) {
super(cdr, nzConfigService);
}

protected subscribeConfigChange(): void {
Expand All @@ -112,24 +53,14 @@ export class NzMessageContainerComponent implements OnInit, OnDestroy {
.subscribe(() => this.updateConfig());
}

protected updateConfigFromConfigService(): Required<MessageConfig> {
return {
protected updateConfig(): void {
this.config = {
...NZ_MESSAGE_DEFAULT_CONFIG,
...this.config,
...this.nzConfigService.getConfigForComponent(NZ_CONFIG_COMPONENT_NAME)
};
}

/**
* Merge default options and custom message options
* @param options
*/
protected mergeMessageOptions(options?: NzMessageDataOptions): NzMessageDataOptions {
const defaultOptions: NzMessageDataOptions = {
nzDuration: this.config.nzDuration,
nzAnimate: this.config.nzAnimate,
nzPauseOnHover: this.config.nzPauseOnHover
};
return { ...defaultOptions, ...options };
this.top = toCssPixel(this.config.nzTop);
this.cdr.markForCheck();
}
}
Loading

0 comments on commit 9224240

Please sign in to comment.