Skip to content

Commit

Permalink
feat: add angular animations and dialog close transition
Browse files Browse the repository at this point in the history
  • Loading branch information
va-stefanek committed Jun 30, 2023
1 parent 3e412ea commit e374ff2
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 66 deletions.
13 changes: 12 additions & 1 deletion projects/ngneat/dialog/src/lib/dialog-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export abstract class DialogRef<
abstract resetDrag(offset?: DragOffset): void;
}

type DialogAnimationState = 'void' | 'enter' | 'exit';

export class InternalDialogRef extends DialogRef {
_state: DialogAnimationState = 'void';
public backdropClick$: Subject<MouseEvent>;

beforeCloseGuards: GuardFN<unknown>[] = [];
Expand All @@ -40,7 +43,11 @@ export class InternalDialogRef extends DialogRef {
close(result?: unknown): void {
this.canClose(result)
.pipe(filter<boolean>(Boolean))
.subscribe({ next: () => this.onClose(result) });
.subscribe({
next: () => {
this.onClose(result);
},
});
}

beforeClose(guard: GuardFN<unknown>) {
Expand Down Expand Up @@ -69,4 +76,8 @@ export class InternalDialogRef extends DialogRef {
asDialogRef(): DialogRef {
return this;
}

_getAnimationState(): DialogAnimationState {
return this._state;
}
}
22 changes: 0 additions & 22 deletions projects/ngneat/dialog/src/lib/dialog.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@
flex-direction: column;
overflow: hidden;
position: relative;
@keyframes dialog-open {
0% {
transform: translateX(50px);
}
100% {
transform: none;
}
}

animation: dialog-open 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);

border-radius: var(--dialog-content-border-radius, 4px);
box-sizing: border-box;
Expand Down Expand Up @@ -49,18 +39,6 @@
&.ngneat-dialog-backdrop-visible {
background: var(--dialog-backdrop-bg, rgba(0, 0, 0, 0.32));
}

animation: dialog-open-backdrop 0.3s;

@keyframes dialog-open-backdrop {
0% {
opacity: 0;
}

100% {
opacity: 1;
}
}
}

.ngneat-drag-marker {
Expand Down
116 changes: 102 additions & 14 deletions projects/ngneat/dialog/src/lib/dialog.component.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,75 @@
import { CommonModule, DOCUMENT } from '@angular/common';
import { Component, ElementRef, inject, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
inject,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { fromEvent, merge, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { InternalDialogRef } from './dialog-ref';
import { DialogService } from './dialog.service';
import { coerceCssPixelValue } from './dialog.utils';
import { DialogDraggableDirective, DragOffset } from './draggable.directive';
import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
import { DialogConfig } from '@ngneat/dialog';
import { animate, animateChild, group, keyframes, query, state, style, transition, trigger } from '@angular/animations';

export const _defaultParams = {
params: { enterAnimationDuration: '150ms', exitAnimationDuration: '75ms' },
};

@Component({
selector: 'ngneat-dialog',
standalone: true,
imports: [DialogDraggableDirective, CommonModule],
animations: [
trigger('dialogContent', [
// Note: The `enter` animation transitions to `transform: none`, because for some reason
// specifying the transform explicitly, causes IE both to blur the dialog content and
// decimate the animation performance. Leaving it as `none` solves both issues.
state('void, exit', style({ opacity: 0, transform: 'scale(0.7)' })),
state('enter', style({ transform: 'none' })),
transition(
'* => enter',
group([
animate(
'0.4s cubic-bezier(0.25, 0.8, 0.25, 1)',
keyframes([style({ opacity: 0, transform: 'translateX(50px)' }), style({ opacity: 1, transform: 'none' })])
),
query('@*', animateChild(), { optional: true }),
]),
_defaultParams
),
transition(
'* => void, * => exit',
group([
animate('0.4s cubic-bezier(0.4, 0.0, 0.2, 1)', style({ opacity: 0 })),
query('@*', animateChild(), { optional: true }),
]),
_defaultParams
),
]),
trigger('dialogContainer', [
transition(
'* => void, * => exit',
group([
animate('0.4s cubic-bezier(0.4, 0.0, 0.2, 1)', style({ opacity: 0 })),
query('@*', animateChild(), { optional: true }),
]),
_defaultParams
),
]),
],
host: {
'[@dialogContainer]': `this.dialogRef._getAnimationState()`,
'(@dialogContainer.start)': '_onAnimationStart($event)',
'(@dialogContainer.done)': '_onAnimationDone($event)',
},
template: `
<div
#backdrop
Expand All @@ -21,6 +79,9 @@ import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
>
<div
#dialog
[@dialogContent]="this.dialogRef._getAnimationState()"
(@dialogContent.start)="_onAnimationStart($event)"
(@dialogContent.done)="_onAnimationDone($event)"
class="ngneat-dialog-content"
[class.ngneat-dialog-resizable]="config.resizable"
[ngStyle]="styles"
Expand Down Expand Up @@ -49,6 +110,7 @@ import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
encapsulation: ViewEncapsulation.None,
})
export class DialogComponent implements OnInit, OnDestroy {
_animationStateChanged = new EventEmitter<{ state: string; totalTime: number }>();
config = inject(DIALOG_CONFIG);
dialogRef = inject(InternalDialogRef);

Expand Down Expand Up @@ -96,6 +158,34 @@ export class DialogComponent implements OnInit, OnDestroy {
}

ngOnInit() {
const dialogElement = this.dialogElement.nativeElement;
this.evaluateConfigBasedFields();

// `dialogElement` is resolved at this point
// And here is where dialog finally will be placed
this.nodes.forEach((node) => dialogElement.appendChild(node));
this.dialogRef._state = 'enter';
}

reset(offset?: DragOffset): void {
if (this.config.draggable) {
this.draggable.reset(offset);
}
}

closeDialog() {
this.dialogRef.close();
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

this.dialogRef = null;
this.nodes = null;
}

private evaluateConfigBasedFields(): void {
const backdrop = this.config.backdrop ? this.backdrop.nativeElement : this.document.body;
const dialogElement = this.dialogElement.nativeElement;

Expand Down Expand Up @@ -133,21 +223,19 @@ export class DialogComponent implements OnInit, OnDestroy {
}
}

reset(offset?: DragOffset): void {
if (this.config.draggable) {
this.draggable.reset(offset);
_onAnimationStart(event): any {
if (event.toState === 'enter') {
this._animationStateChanged.next({ state: 'opening', totalTime: event.totalTime });
} else if (event.toState === 'exit' || event.toState === 'void') {
this._animationStateChanged.next({ state: 'closing', totalTime: event.totalTime });
}
}

closeDialog() {
this.dialogRef.close();
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

this.dialogRef = null;
this.nodes = null;
_onAnimationDone(event) {
if (event.toState === 'enter') {
// this._openAnimationDone(totalTime);
} else if (event.toState === 'exit') {
this._animationStateChanged.next({ state: 'closed', totalTime: event.totalTime });
}
}
}
70 changes: 41 additions & 29 deletions projects/ngneat/dialog/src/lib/dialog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
Type,
ViewRef,
} from '@angular/core';
import { BehaviorSubject, startWith, Subject } from 'rxjs';
import { BehaviorSubject, Subject, take } from 'rxjs';
import { DialogRef, InternalDialogRef } from './dialog-ref';
import { DialogComponent } from './dialog.component';
import { DragOffset } from './draggable.directive';
import { DIALOG_CONFIG, DIALOG_DOCUMENT_REF, GLOBAL_DIALOG_CONFIG, NODES_TO_INSERT } from './providers';
import { AttachOptions, DialogConfig, ExtractData, ExtractResult, GlobalDialogConfig, OpenParams } from './types';
import { map } from 'rxjs/operators';
import { provideAnimations } from '@angular/platform-browser/animations';
import { filter } from 'rxjs/operators';

const OVERFLOW_HIDDEN_CLASS = 'ngneat-dialog-hidden';

Expand Down Expand Up @@ -141,33 +142,43 @@ export class DialogService {
};

const onClose = (result: unknown) => {
this.globalConfig.onClose?.();
this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id);
this.hasOpenDialogSub.next(this.hasOpenDialogs());

container.removeChild(dialog.location.nativeElement);
this.appRef.detachView(dialog.hostView);
this.appRef.detachView(view);

dialog.destroy();
view.destroy();

dialogRef.backdropClick$.complete();

dialogRef.mutate({
ref: null,
onClose: null,
afterClosed$: null,
backdropClick$: null,
beforeCloseGuards: null,
onReset: null,
});

hooks.after.next(result);
hooks.after.complete();
if (this.dialogs.length === 0) {
this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
}
dialog.instance._animationStateChanged
.pipe(
filter((event) => event.state === 'closed'),
take(1)
)
.subscribe((event) => {
this.globalConfig.onClose?.();

this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id);
this.hasOpenDialogSub.next(this.hasOpenDialogs());

container.removeChild(dialog.location.nativeElement);
this.appRef.detachView(dialog.hostView);
this.appRef.detachView(view);

dialog.destroy();
view.destroy();

dialogRef.backdropClick$.complete();

dialogRef.mutate({
ref: null,
onClose: null,
afterClosed$: null,
backdropClick$: null,
beforeCloseGuards: null,
onReset: null,
});

hooks.after.next(result);
hooks.after.complete();
if (this.dialogs.length === 0) {
this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
}
});

dialogRef._state = 'exit';
};

const onReset = (offset?: DragOffset) => {
Expand Down Expand Up @@ -209,6 +220,7 @@ export class DialogService {
provide: DIALOG_CONFIG,
useValue: config,
},
provideAnimations(),
],
parent: this.injector,
}),
Expand Down

0 comments on commit e374ff2

Please sign in to comment.