Skip to content

Commit

Permalink
feat(alert): add animations
Browse files Browse the repository at this point in the history
  • Loading branch information
fbasso authored and maxokorokov committed May 12, 2020
1 parent 82881ab commit ba7362e
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
<p>
Static self-closing alert that disappears after 20 seconds (refresh the page if it has already disappeared)
</p>
<ngb-alert *ngIf="!staticAlertClosed" (close)="staticAlertClosed = true">Check out our awesome new features!</ngb-alert>
<ngb-alert #staticAlert *ngIf="!staticAlertClosed" (close)="staticAlertClosed = true">Check out our awesome new
features!</ngb-alert>

<hr/>
<hr />

<p>
Show a self-closing success message that disappears after 5 seconds.
</p>
<ngb-alert *ngIf="successMessage" type="success" (close)="successMessage = ''">{{ successMessage }}</ngb-alert>
<ngb-alert #selfClosingAlert *ngIf="successMessage" type="success" (close)="successMessage = ''">{{ successMessage }}
</ngb-alert>
<p>
<button class="btn btn-primary" (click)="changeSuccessMessage()">Change message</button>
</p>
</p>
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, ViewChild} from '@angular/core';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
import {NgbAlert} from '@ng-bootstrap/ng-bootstrap';

@Component({
selector: 'ngbd-alert-selfclosing',
templateUrl: './alert-selfclosing.html'
})
@Component({selector: 'ngbd-alert-selfclosing', templateUrl: './alert-selfclosing.html'})
export class NgbdAlertSelfclosing implements OnInit {
private _success = new Subject<string>();

staticAlertClosed = false;
successMessage = '';

@ViewChild('staticAlert', {static: false}) staticAlert: NgbAlert;
@ViewChild('selfClosingAlert', {static: false}) selfClosingAlert: NgbAlert;

ngOnInit(): void {
setTimeout(() => this.staticAlertClosed = true, 20000);
setTimeout(() => this.staticAlert.close(), 20000);

this._success.subscribe(message => this.successMessage = message);
this._success.pipe(
debounceTime(5000)
).subscribe(() => this.successMessage = '');
this._success.pipe(debounceTime(5000)).subscribe(() => {
if (this.selfClosingAlert) {
this.selfClosingAlert.close();
}
});
}

public changeSuccessMessage() {
this._success.next(`${new Date()} - Message successfully changed.`);
}
public changeSuccessMessage() { this._success.next(`${new Date()} - Message successfully changed.`); }
}
3 changes: 2 additions & 1 deletion src/alert/alert-config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {NgbAlertConfig} from './alert-config';
import {NgbConfig} from '../ngb-config';

describe('ngb-alert-config', () => {
it('should have sensible default values', () => {
const config = new NgbAlertConfig();
const config = new NgbAlertConfig(new NgbConfig());

expect(config.dismissible).toBe(true);
expect(config.type).toBe('warning');
Expand Down
4 changes: 4 additions & 0 deletions src/alert/alert-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Injectable} from '@angular/core';
import {NgbConfig} from '../ngb-config';

/**
* A configuration service for the [NgbAlert](#/components/alert/api#NgbAlert) component.
Expand All @@ -8,6 +9,9 @@ import {Injectable} from '@angular/core';
*/
@Injectable({providedIn: 'root'})
export class NgbAlertConfig {
animation: boolean;
dismissible = true;
type = 'warning';

constructor(ngbConfig: NgbConfig) { this.animation = ngbConfig.animation; }
}
6 changes: 4 additions & 2 deletions src/alert/alert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {NgbAlertModule} from './alert.module';
import {NgbAlert} from './alert';
import {NgbAlertConfig} from './alert-config';

import {NgbConfig} from '../ngb-config';

const createTestComponent = (html: string) =>
createGenericTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;

Expand All @@ -27,7 +29,7 @@ describe('ngb-alert', () => {
beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbAlertModule]}); });

it('should initialize inputs with default values', () => {
const defaultConfig = new NgbAlertConfig();
const defaultConfig = new NgbAlertConfig(new NgbConfig());
const alertCmp = TestBed.createComponent(NgbAlert).componentInstance;
expect(alertCmp.dismissible).toBe(defaultConfig.dismissible);
expect(alertCmp.type).toBe(defaultConfig.type);
Expand Down Expand Up @@ -142,7 +144,7 @@ describe('ngb-alert', () => {
});

describe('Custom config as provider', () => {
let config = new NgbAlertConfig();
let config = new NgbAlertConfig(new NgbConfig());
config.dismissible = false;
config.type = 'success';

Expand Down
27 changes: 23 additions & 4 deletions src/alert/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from '@angular/core';

import {NgbAlertConfig} from './alert-config';
import {ngbRunTransition} from '../util/transition/ngbTransition';
import {ngbAlertFadingTransition} from '../util/transition/ngbFadingTransition';

/**
* Alert is a component to provide contextual feedback messages for user.
Expand All @@ -23,43 +25,60 @@ import {NgbAlertConfig} from './alert-config';
selector: 'ngb-alert',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {'role': 'alert', 'class': 'alert', '[class.alert-dismissible]': 'dismissible'},
host: {'role': 'alert', 'class': 'alert show fade', '[class.alert-dismissible]': 'dismissible'},
template: `
<ng-content></ng-content>
<button *ngIf="dismissible" type="button" class="close" aria-label="Close" i18n-aria-label="@@ngb.alert.close"
(click)="closeHandler()">
(click)="close()">
<span aria-hidden="true">&times;</span>
</button>
`,
styleUrls: ['./alert.scss']
})
export class NgbAlert implements OnInit,
OnChanges {
/**
* If `true`, alert closing will be animated.
*
* Animation is triggered only when clicked on the close button (×)
* or via the `.close()` function
*/
@Input() animation: boolean;

/**
* If `true`, alert can be dismissed by the user.
*
* The close button (×) will be displayed and you can be notified
* of the event with the `(close)` output.
*/
@Input() dismissible: boolean;

/**
* Type of the alert.
*
* Bootstrap provides styles for the following types: `'success'`, `'info'`, `'warning'`, `'danger'`, `'primary'`,
* `'secondary'`, `'light'` and `'dark'`.
*/
@Input() type: string;

/**
* An event emitted when the close button is clicked. It has no payload and only relevant for dismissible alerts.
*/
@Output() close = new EventEmitter<void>();
@Output('close') closeEvent = new EventEmitter<void>();


constructor(config: NgbAlertConfig, private _renderer: Renderer2, private _element: ElementRef) {
this.dismissible = config.dismissible;
this.type = config.type;
this.animation = config.animation;
}

closeHandler() { this.close.emit(); }
close() {
ngbRunTransition(this._element.nativeElement, ngbAlertFadingTransition, {
animation: this.animation,
runningTransition: 'continue'
}).subscribe(() => this.closeEvent.emit());
}

ngOnChanges(changes: SimpleChanges) {
const typeChange = changes['type'];
Expand Down
1 change: 1 addition & 0 deletions src/environment.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const environment = {
animation: false,
transitionTimerDelayMs: 500,
};
1 change: 1 addition & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const environment = {
animation: true,
transitionTimerDelayMs: 5,
};
3 changes: 3 additions & 0 deletions src/util/transition/ngbFadingTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ngbAlertFadingTransition = ({classList}: HTMLElement): void => {
classList.remove('show');
};
65 changes: 65 additions & 0 deletions src/util/transition/ngbTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {EMPTY, fromEvent, Observable, of, race, Subject, timer} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {getTransitionDurationMs} from './util';
import {environment} from '../../environment';

export type NgbTransitionStartFn = (element: HTMLElement) => void;

export interface NgbTransitionOptions {
animation: boolean;
runningTransition: 'continue' | 'stop';
}

const {transitionTimerDelayMs} = environment;
const runningTransitions = new Map<HTMLElement, Subject<any>>();

export const ngbRunTransition =
(element: HTMLElement, startFn: NgbTransitionStartFn, options: NgbTransitionOptions): Observable<undefined> => {

// If animations are disabled, we have to emit a value and complete the observable
if (!options.animation) {
return of(undefined);
}

// Checking if there are already running transitions on the given element.
const runningTransition$ = runningTransitions.get(element);
if (runningTransition$) {
// If there is one running and we want for it to continue to run, we have to cancel the current one.
// We're not emitting any values, but simply completing the observable (EMPTY).
if (options.runningTransition === 'continue') {
return EMPTY;
}
}

// If 'prefer-reduced-motion' is enabled, the 'transition' will be set to 'none'.
// In this case we have to call the start function, but can finish immediately by emitting a value
// and completing the observable.
const {transitionProperty} = window.getComputedStyle(element);
if (transitionProperty === 'none') {
startFn(element);
return of(undefined);
}

// Starting a new transition
const transition$ = new Subject<any>();
runningTransitions.set(element, transition$);

const transitionDurationMs = getTransitionDurationMs(element);

startFn(element);

// We have to both listen for the 'transitionend' event and have a 'just-in-case' timer,
// because 'transitionend' event might not be fired in some browsers, if the transitioning
// element becomes invisible (ex. when scrolling, making browser tab inactive, etc.). The timer
// guarantees, that we'll release the DOM element and complete 'ngbRunTransition'.
const transitionEnd$ = fromEvent(element, 'transitionend').pipe(takeUntil(transition$));
const timer$ = timer(transitionDurationMs + transitionTimerDelayMs).pipe(takeUntil(transition$));

race(timer$, transitionEnd$).subscribe(() => {
runningTransitions.delete(element);
transition$.next();
transition$.complete();
});

return transition$.asObservable();
};
7 changes: 7 additions & 0 deletions src/util/transition/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getTransitionDurationMs(element: HTMLElement) {
const {transitionDelay, transitionDuration} = window.getComputedStyle(element);
const transitionDelaySec = parseFloat(transitionDelay);
const transitionDurationSec = parseFloat(transitionDuration);

return (transitionDelaySec + transitionDurationSec) * 1000;
}

0 comments on commit ba7362e

Please sign in to comment.