Skip to content

Commit

Permalink
feat: add grouping feature
Browse files Browse the repository at this point in the history
  • Loading branch information
shhdharmen committed Apr 14, 2024
1 parent e6fcdf7 commit c342de6
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div style="position: fixed; z-index: 9999; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none">
<div style="position: relative; height: 100%">
<div>
@for (toast of toasts; track trackById(i, toast); let i = $index) {
@for (toast of toasts; track trackById(i, toast); let i = $index) { @if (toast.group.parent) {} @else {
<hot-toast
[toast]="toast"
[offset]="calculateOffset(toast.id, toast.position)"
Expand All @@ -14,7 +14,7 @@
(beforeClosed)="beforeClosed(toast)"
(afterClosed)="afterClosed($event)"
></hot-toast>
}
} }
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { filter } from 'rxjs/operators';
import { Content } from '@ngneat/overview';
import { HotToastComponent } from '../hot-toast/hot-toast.component';
import { HOT_TOAST_DEPTH_SCALE, HOT_TOAST_DEPTH_SCALE_ADD, HOT_TOAST_MARGIN } from '../../constants';
import { HotToastService } from '../../hot-toast.service';

@Component({
selector: 'hot-toast-container',
Expand All @@ -36,7 +37,7 @@ export class HotToastContainerComponent {

private onClosed$ = this._onClosed.asObservable();

constructor(private cdr: ChangeDetectorRef) {}
constructor(private cdr: ChangeDetectorRef, private toastService: HotToastService) {}

trackById(index: number, toast: Toast<unknown>) {
return toast.id;
Expand All @@ -45,6 +46,7 @@ export class HotToastContainerComponent {
getVisibleToasts(position: ToastPosition) {
return this.toasts.filter((t) => t.visible && t.position === position);
}

calculateOffset(toastId: string, position: ToastPosition) {
const visibleToasts = this.getVisibleToasts(position);
const index = visibleToasts.findIndex((toast) => toast.id === toastId);
Expand Down Expand Up @@ -87,6 +89,22 @@ export class HotToastContainerComponent {

this.cdr.detectChanges();

const groupRefs: CreateHotToastRef<unknown>[] = [];

if (toast.group) {
if (toast.group.children) {
const items = toast.group.children;
groupRefs.push(
...items.map((item) => {
item.options.group = { parent: ref };
return this.toastService.show(item.options.message, item.options);
})
);
} else if (toast.group.parent) {
// TODO
}
}

return {
dispose: () => {
this.closeToast(toast.id);
Expand All @@ -101,6 +119,7 @@ export class HotToastContainerComponent {
this.cdr.detectChanges();
},
afterClosed: this.getAfterClosed(toast),
groupRefs,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div
class="hot-toast-bar-base-container"
[ngStyle]="containerPositionStyle"
[ngClass]="'hot-toast-theme-' + toast.theme"
[style.--hot-toast-scale]="1"
[style.--hot-toast-translate-y]="translateY"
>
<div class="hot-toast-bar-base-wrapper" (mouseenter)="handleMouseEnter()" (mouseleave)="handleMouseLeave()">
<div
class="hot-toast-bar-base"
#hotToastBarBase
[ngStyle]="toastBarBaseStyles"
[ngClass]="toast.className"
[style.--hot-toast-animation-state]="isManualClose ? 'running' : 'paused'"
[style.--hot-toast-exit-animation-state]="isShowingAllToasts ? 'paused' : 'running'"
[style.--hot-toast-exit-animation-delay]="exitAnimationDelay"
[attr.aria-live]="toast.ariaLive"
[attr.role]="toast.role"
>
<div class="hot-toast-icon" aria-hidden="true">
@if (toast.icon !== undefined) { @if (isIconString) {
<hot-toast-animated-icon [iconTheme]="toast.iconTheme">{{ toast.icon }}</hot-toast-animated-icon>
} @else {
<div>
<ng-container *dynamicView="toast.icon"></ng-container>
</div>
} } @else {
<hot-toast-indicator [theme]="toast.iconTheme" [type]="toast.type"></hot-toast-indicator>
}
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Injector, NgZone, Renderer2 } from '@angular/core';
import { HotToastComponent } from '../hot-toast/hot-toast.component';
import { NgClass, NgStyle } from '@angular/common';
import { AnimatedIconComponent } from '../animated-icon/animated-icon.component';
import { IndicatorComponent } from '../indicator/indicator.component';

@Component({
selector: 'hot-toast-group-item',
templateUrl: 'hot-toast-group-item.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgClass, NgStyle, AnimatedIconComponent, IndicatorComponent],
})
export class HotToastGroupItemComponent extends HotToastComponent {
constructor(injector: Injector, renderer: Renderer2, ngZone: NgZone, cdr: ChangeDetectorRef) {
super(injector, renderer, ngZone, cdr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,24 @@
<ng-container *dynamicView="toast.message; context: context; injector: toastComponentInjector"></ng-container>
</div>
</div>
</div>

@if (toast.dismissible) {
<button
(click)="close()"
type="button"
class="hot-toast-close-btn"
aria-label="Close"
[ngStyle]="toast.closeStyle"
></button>
@if (toast.group?.children) {
<div role="list" class="hot-toast-bar-base-group">
@for (item of childGroupToasts; track $index) {
<hot-toast-group-item
[toast]="item"
[offset]="calculateOffset(item.id, item.position)"
[toastRef]="toastRef.groupRefs[$index]"
[toastsAfter]="(item.autoClose ? childGroupToasts.length : getVisibleToasts(item.position).length) - 1 - $index"
[defaultConfig]="defaultConfig"
[isShowingAllToasts]="isShowingAllToasts"
(height)="updateHeight($event, item)"
(beforeClosed)="beforeClosedGroupItem(item)"
(afterClosed)="afterClosedGroupItem($event)"
></hot-toast-group-item>
}
</div>
}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Expand All @@ -22,25 +23,28 @@ import {
ENTER_ANIMATION_DURATION,
EXIT_ANIMATION_DURATION,
HOT_TOAST_DEPTH_SCALE,
HOT_TOAST_MARGIN,
} from '../../constants';
import { HotToastRef } from '../../hot-toast-ref';
import { CreateHotToastRef, HotToastClose, Toast, ToastConfig } from '../../hot-toast.model';
import { CreateHotToastRef, HotToastClose, Toast, ToastConfig, ToastPosition } from '../../hot-toast.model';
import { animate } from '../../utils';
import { IndicatorComponent } from '../indicator/indicator.component';
import { AnimatedIconComponent } from '../animated-icon/animated-icon.component';
import { HotToastGroupItemComponent } from '../hot-toast-group-item/hot-toast-group-item.component';

@Component({
selector: 'hot-toast',
templateUrl: 'hot-toast.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, DynamicViewDirective, IndicatorComponent, AnimatedIconComponent],
imports: [CommonModule, DynamicViewDirective, IndicatorComponent, AnimatedIconComponent, HotToastGroupItemComponent],
})
export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
@Input() toast: Toast<unknown>;
@Input() offset = 0;
@Input() defaultConfig: ToastConfig;
@Input() toastRef: CreateHotToastRef<unknown>;

private _toastsAfter = 0;
get toastsAfter() {
return this._toastsAfter;
Expand All @@ -62,6 +66,7 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh
}
}
}

@Input() isShowingAllToasts = false;

@Output() height = new EventEmitter<number>();
Expand All @@ -78,7 +83,12 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh
private unlisteners: VoidFunction[] = [];
private softClosed = false;

constructor(private injector: Injector, private renderer: Renderer2, private ngZone: NgZone) {}
constructor(
private injector: Injector,
private renderer: Renderer2,
private ngZone: NgZone,
private cdr: ChangeDetectorRef
) {}

get toastBarBaseHeight() {
return this.toastBarBase.nativeElement.offsetHeight;
Expand Down Expand Up @@ -144,6 +154,20 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh
return typeof this.toast.icon === 'string';
}

get childGroupToasts() {
return this.toast.group.children.map((t) => t.options) ?? [];
}
set childGroupToasts(value) {
this.toast.group.children = value.map((t) => ({ options: t })) ?? [];
}

get childGroupToastRefs() {
return this.toastRef.groupRefs ?? [];
}
set childGroupToastRefs(value) {
this.toastRef.groupRefs = value;
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.toast && !changes.toast.firstChange && changes.toast.currentValue?.message) {
requestAnimationFrame(() => {
Expand Down Expand Up @@ -257,4 +281,40 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh
this.renderer.setAttribute(this.toastBarBase.nativeElement, key, value);
}
}

getVisibleToasts(position: ToastPosition) {
return this.childGroupToasts.filter((t) => t.visible && t.position === position);
}

calculateOffset(toastId: string, position: ToastPosition) {
const visibleToasts = this.getVisibleToasts(position);
const index = visibleToasts.findIndex((toast) => toast.id === toastId);
const offset =
index !== -1
? visibleToasts.slice(...(this.defaultConfig.reverseOrder ? [index + 1] : [0, index])).reduce((acc, t, i) => {
return this.defaultConfig.visibleToasts !== 0 && i < visibleToasts.length - this.defaultConfig.visibleToasts
? 0
: acc + (t.height || 0) + HOT_TOAST_MARGIN;
}, 0)
: 0;
return offset;
}

updateHeight(height: number, toast: Toast<unknown>) {
toast.height = height;
this.cdr.detectChanges();
}

beforeClosedGroupItem(toast: Toast<unknown>) {
toast.visible = false;
}

afterClosedGroupItem(closeToast: HotToastClose) {
const toastIndex = this.childGroupToasts.findIndex((t) => t.id === closeToast.id);
if (toastIndex > -1) {
this.childGroupToasts = this.childGroupToasts.filter((t) => t.id !== closeToast.id);
this.childGroupToastRefs = this.childGroupToastRefs.filter((t) => t.getToast().id !== closeToast.id);
this.cdr.detectChanges();
}
}
}
11 changes: 7 additions & 4 deletions projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Content } from '@ngneat/overview';
import { Observable, race, Subject } from 'rxjs';
import { defer, Observable, race, Subject } from 'rxjs';

// This should be a `type` import since it causes `ng-packagr` compilation to fail because of a cyclic dependency.
import type { HotToastContainerComponent } from './components/hot-toast-container/hot-toast-container.component';
import { HotToastClose, Toast, UpdateToastOptions, HotToastRefProps, DefaultDataType } from './hot-toast.model';
import { HotToastClose, Toast, UpdateToastOptions, HotToastRefProps, DefaultDataType, CreateHotToastRef } from './hot-toast.model';

export class HotToastRef<DataType = DefaultDataType> implements HotToastRefProps<DataType> {
updateMessage: (message: Content) => void;
updateToast: (options: UpdateToastOptions<DataType>) => void;
afterClosed: Observable<HotToastClose>;
groupRefs: CreateHotToastRef<unknown>[] = [];

private _dispose: () => void;

Expand All @@ -33,16 +34,18 @@ export class HotToastRef<DataType = DefaultDataType> implements HotToastRefProps
return this.toast;
}

/**Used for internal purpose
/**
* Used for internal purpose
* Attach ToastRef to container
*/
appendTo(container: HotToastContainerComponent) {
const { dispose, updateMessage, updateToast, afterClosed } = container.addToast(this);
const { dispose, updateMessage, updateToast, afterClosed, groupRefs } = container.addToast(this);

this.dispose = dispose;
this.updateMessage = updateMessage;
this.updateToast = updateToast;
this.afterClosed = race(this._onClosed.asObservable(), afterClosed);
this.groupRefs = groupRefs;
return this;
}

Expand Down
8 changes: 7 additions & 1 deletion projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Injector } from '@angular/core';
import { Content } from '@ngneat/overview';
import { Observable } from 'rxjs';
import { HotToastRef } from './hot-toast-ref';

export type ToastStacking = 'vertical' | 'depth';

Expand Down Expand Up @@ -201,6 +202,8 @@ export interface Toast<DataType> {
* @memberof Toast
*/
data?: DataType;

group?: { children?: { options: Toast<unknown> }[], parent?: CreateHotToastRef<unknown> };
}

export type ToastOptions<DataType> = Partial<
Expand All @@ -223,6 +226,7 @@ export type ToastOptions<DataType> = Partial<
| 'injector'
| 'data'
| 'attributes'
| 'group'
>
>;

Expand Down Expand Up @@ -271,12 +275,14 @@ export interface HotToastRefProps<DataType> {
updateToast: (options: UpdateToastOptions<DataType>) => void;
/** Observable for notifying the user that the toast has been closed. */
afterClosed: Observable<HotToastClose>;
afterGroupItemClosed?: Observable<HotToastClose>;
/**Closes the toast */
close: (closeData?: { dismissedByAction: boolean }) => void;
/**
* @since 2.0.0
*/
data: DataType;
groupRefs: CreateHotToastRef<unknown>[];
}

/** Event that is emitted when a snack bar is dismissed. */
Expand Down Expand Up @@ -318,7 +324,7 @@ export class ToastPersistConfig {

export type AddToastRef<DataType> = Pick<
HotToastRefProps<DataType>,
'afterClosed' | 'dispose' | 'updateMessage' | 'updateToast'
'afterClosed' | 'dispose' | 'updateMessage' | 'updateToast' | 'groupRefs'
>;

export type CreateHotToastRef<DataType> = Omit<Omit<HotToastRefProps<DataType>, 'appendTo'>, 'dispose'>;
Expand Down
Loading

0 comments on commit c342de6

Please sign in to comment.