Skip to content

Commit 0147475

Browse files
committed
feat: add angular animations and dialog close transition
1 parent 3e412ea commit 0147475

File tree

4 files changed

+142
-53
lines changed

4 files changed

+142
-53
lines changed

projects/ngneat/dialog/src/lib/dialog-ref.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export abstract class DialogRef<
2424
abstract resetDrag(offset?: DragOffset): void;
2525
}
2626

27+
type DialogAnimationState = 'void' | 'enter' | 'exit';
28+
2729
export class InternalDialogRef extends DialogRef {
30+
_state: DialogAnimationState = 'void';
2831
public backdropClick$: Subject<MouseEvent>;
2932

3033
beforeCloseGuards: GuardFN<unknown>[] = [];
@@ -40,7 +43,11 @@ export class InternalDialogRef extends DialogRef {
4043
close(result?: unknown): void {
4144
this.canClose(result)
4245
.pipe(filter<boolean>(Boolean))
43-
.subscribe({ next: () => this.onClose(result) });
46+
.subscribe({
47+
next: () => {
48+
this.onClose(result);
49+
},
50+
});
4451
}
4552

4653
beforeClose(guard: GuardFN<unknown>) {
@@ -69,4 +76,8 @@ export class InternalDialogRef extends DialogRef {
6976
asDialogRef(): DialogRef {
7077
return this;
7178
}
79+
80+
_getAnimationState(): DialogAnimationState {
81+
return this._state;
82+
}
7283
}

projects/ngneat/dialog/src/lib/dialog.component.scss

-22
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@
33
flex-direction: column;
44
overflow: hidden;
55
position: relative;
6-
@keyframes dialog-open {
7-
0% {
8-
transform: translateX(50px);
9-
}
10-
100% {
11-
transform: none;
12-
}
13-
}
14-
15-
animation: dialog-open 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
166

177
border-radius: var(--dialog-content-border-radius, 4px);
188
box-sizing: border-box;
@@ -49,18 +39,6 @@
4939
&.ngneat-dialog-backdrop-visible {
5040
background: var(--dialog-backdrop-bg, rgba(0, 0, 0, 0.32));
5141
}
52-
53-
animation: dialog-open-backdrop 0.3s;
54-
55-
@keyframes dialog-open-backdrop {
56-
0% {
57-
opacity: 0;
58-
}
59-
60-
100% {
61-
opacity: 1;
62-
}
63-
}
6442
}
6543

6644
.ngneat-drag-marker {

projects/ngneat/dialog/src/lib/dialog.component.ts

+89-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,75 @@
11
import { CommonModule, DOCUMENT } from '@angular/common';
2-
import { Component, ElementRef, inject, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
2+
import {
3+
Component,
4+
ElementRef,
5+
EventEmitter,
6+
inject,
7+
OnDestroy,
8+
OnInit,
9+
ViewChild,
10+
ViewEncapsulation,
11+
} from '@angular/core';
312
import { fromEvent, merge, Subject } from 'rxjs';
413
import { filter, takeUntil } from 'rxjs/operators';
514
import { InternalDialogRef } from './dialog-ref';
615
import { DialogService } from './dialog.service';
716
import { coerceCssPixelValue } from './dialog.utils';
817
import { DialogDraggableDirective, DragOffset } from './draggable.directive';
918
import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
19+
import { DialogConfig } from '@ngneat/dialog';
20+
import { animate, animateChild, group, keyframes, query, state, style, transition, trigger } from '@angular/animations';
21+
22+
export const _defaultParams = {
23+
params: { enterAnimationDuration: '150ms', exitAnimationDuration: '75ms' },
24+
};
1025

1126
@Component({
1227
selector: 'ngneat-dialog',
1328
standalone: true,
1429
imports: [DialogDraggableDirective, CommonModule],
30+
animations: [
31+
trigger('dialogContent', [
32+
// Note: The `enter` animation transitions to `transform: none`, because for some reason
33+
// specifying the transform explicitly, causes IE both to blur the dialog content and
34+
// decimate the animation performance. Leaving it as `none` solves both issues.
35+
state('void, exit', style({ opacity: 0, transform: 'scale(0.7)' })),
36+
state('enter', style({ transform: 'none' })),
37+
transition(
38+
'* => enter',
39+
group([
40+
animate(
41+
'0.4s cubic-bezier(0.25, 0.8, 0.25, 1)',
42+
keyframes([style({ opacity: 0, transform: 'translateX(50px)' }), style({ opacity: 1, transform: 'none' })])
43+
),
44+
query('@*', animateChild(), { optional: true }),
45+
]),
46+
_defaultParams
47+
),
48+
transition(
49+
'* => void, * => exit',
50+
group([
51+
animate('0.4s cubic-bezier(0.4, 0.0, 0.2, 1)', style({ opacity: 0 })),
52+
query('@*', animateChild(), { optional: true }),
53+
]),
54+
_defaultParams
55+
),
56+
]),
57+
trigger('dialogContainer', [
58+
transition(
59+
'* => void, * => exit',
60+
group([
61+
animate('0.4s cubic-bezier(0.4, 0.0, 0.2, 1)', style({ opacity: 0 })),
62+
query('@*', animateChild(), { optional: true }),
63+
]),
64+
_defaultParams
65+
),
66+
]),
67+
],
68+
host: {
69+
'[@dialogContainer]': `this.dialogRef._getAnimationState()`,
70+
'(@dialogContainer.start)': '_onAnimationStart($event)',
71+
'(@dialogContainer.done)': '_onAnimationDone($event)',
72+
},
1573
template: `
1674
<div
1775
#backdrop
@@ -21,6 +79,9 @@ import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
2179
>
2280
<div
2381
#dialog
82+
[@dialogContent]="this.dialogRef._getAnimationState()"
83+
(@dialogContent.start)="_onAnimationStart($event)"
84+
(@dialogContent.done)="_onAnimationDone($event)"
2485
class="ngneat-dialog-content"
2586
[class.ngneat-dialog-resizable]="config.resizable"
2687
[ngStyle]="styles"
@@ -49,6 +110,7 @@ import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
49110
encapsulation: ViewEncapsulation.None,
50111
})
51112
export class DialogComponent implements OnInit, OnDestroy {
113+
_animationStateChanged = new EventEmitter<{ state: string; totalTime: number }>();
52114
config = inject(DIALOG_CONFIG);
53115
dialogRef = inject(InternalDialogRef);
54116

@@ -96,6 +158,16 @@ export class DialogComponent implements OnInit, OnDestroy {
96158
}
97159

98160
ngOnInit() {
161+
const dialogElement = this.dialogElement.nativeElement;
162+
this.evaluateConfigBasedFields();
163+
164+
// `dialogElement` is resolved at this point
165+
// And here is where dialog finally will be placed
166+
this.nodes.forEach((node) => dialogElement.appendChild(node));
167+
this.dialogRef._state = 'enter';
168+
}
169+
170+
private evaluateConfigBasedFields(): void {
99171
const backdrop = this.config.backdrop ? this.backdrop.nativeElement : this.document.body;
100172
const dialogElement = this.dialogElement.nativeElement;
101173

@@ -133,6 +205,22 @@ export class DialogComponent implements OnInit, OnDestroy {
133205
}
134206
}
135207

208+
_onAnimationStart(event): any {
209+
if (event.toState === 'enter') {
210+
this._animationStateChanged.next({ state: 'opening', totalTime: event.totalTime });
211+
} else if (event.toState === 'exit' || event.toState === 'void') {
212+
this._animationStateChanged.next({ state: 'closing', totalTime: event.totalTime });
213+
}
214+
}
215+
216+
_onAnimationDone(event) {
217+
if (event.toState === 'enter') {
218+
// this._openAnimationDone(totalTime);
219+
} else if (event.toState === 'exit') {
220+
this._animationStateChanged.next({ state: 'closed', totalTime: event.totalTime });
221+
}
222+
}
223+
136224
reset(offset?: DragOffset): void {
137225
if (this.config.draggable) {
138226
this.draggable.reset(offset);

projects/ngneat/dialog/src/lib/dialog.service.ts

+41-29
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import {
1010
Type,
1111
ViewRef,
1212
} from '@angular/core';
13-
import { BehaviorSubject, startWith, Subject } from 'rxjs';
13+
import { BehaviorSubject, Subject, take } from 'rxjs';
1414
import { DialogRef, InternalDialogRef } from './dialog-ref';
1515
import { DialogComponent } from './dialog.component';
1616
import { DragOffset } from './draggable.directive';
1717
import { DIALOG_CONFIG, DIALOG_DOCUMENT_REF, GLOBAL_DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
1818
import { AttachOptions, DialogConfig, ExtractData, ExtractResult, GlobalDialogConfig, OpenParams } from './types';
19-
import { map } from 'rxjs/operators';
19+
import { provideAnimations } from '@angular/platform-browser/animations';
20+
import { filter } from 'rxjs/operators';
2021

2122
const OVERFLOW_HIDDEN_CLASS = 'ngneat-dialog-hidden';
2223

@@ -141,33 +142,43 @@ export class DialogService {
141142
};
142143

143144
const onClose = (result: unknown) => {
144-
this.globalConfig.onClose?.();
145-
this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id);
146-
this.hasOpenDialogSub.next(this.hasOpenDialogs());
147-
148-
container.removeChild(dialog.location.nativeElement);
149-
this.appRef.detachView(dialog.hostView);
150-
this.appRef.detachView(view);
151-
152-
dialog.destroy();
153-
view.destroy();
154-
155-
dialogRef.backdropClick$.complete();
156-
157-
dialogRef.mutate({
158-
ref: null,
159-
onClose: null,
160-
afterClosed$: null,
161-
backdropClick$: null,
162-
beforeCloseGuards: null,
163-
onReset: null,
164-
});
165-
166-
hooks.after.next(result);
167-
hooks.after.complete();
168-
if (this.dialogs.length === 0) {
169-
this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
170-
}
145+
dialog.instance._animationStateChanged
146+
.pipe(
147+
filter((event) => event.state === 'closed'),
148+
take(1)
149+
)
150+
.subscribe((event) => {
151+
this.globalConfig.onClose?.();
152+
153+
this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id);
154+
this.hasOpenDialogSub.next(this.hasOpenDialogs());
155+
156+
container.removeChild(dialog.location.nativeElement);
157+
this.appRef.detachView(dialog.hostView);
158+
this.appRef.detachView(view);
159+
160+
dialog.destroy();
161+
view.destroy();
162+
163+
dialogRef.backdropClick$.complete();
164+
165+
dialogRef.mutate({
166+
ref: null,
167+
onClose: null,
168+
afterClosed$: null,
169+
backdropClick$: null,
170+
beforeCloseGuards: null,
171+
onReset: null,
172+
});
173+
174+
hooks.after.next(result);
175+
hooks.after.complete();
176+
if (this.dialogs.length === 0) {
177+
this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
178+
}
179+
});
180+
181+
dialogRef._state = 'exit';
171182
};
172183

173184
const onReset = (offset?: DragOffset) => {
@@ -209,6 +220,7 @@ export class DialogService {
209220
provide: DIALOG_CONFIG,
210221
useValue: config,
211222
},
223+
provideAnimations(),
212224
],
213225
parent: this.injector,
214226
}),

0 commit comments

Comments
 (0)