Skip to content

Commit

Permalink
fix: prefer native Web Animations API instead of @angular/animations …
Browse files Browse the repository at this point in the history
…package
  • Loading branch information
Tehzombie-secret committed Oct 24, 2022
1 parent 4bb1103 commit 03a0035
Show file tree
Hide file tree
Showing 15 changed files with 78 additions and 132 deletions.
17 changes: 15 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,17 @@
"maximumWarning": "6kb"
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
}
},
"defaultConfiguration": "development"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
Expand All @@ -71,8 +80,12 @@
"configurations": {
"production": {
"browserTarget": "ng-carousel-demo:build:production"
},
"development": {
"browserTarget": "ng-carousel-demo:build:development"
}
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
Expand Down
1 change: 0 additions & 1 deletion projects/ng-carousel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"peerDependencies": {
"@angular/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"@angular/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"@angular/animations": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"@angular/cdk": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"rxjs": "^6.5.0 || ^7.0.0"
},
Expand Down
2 changes: 0 additions & 2 deletions projects/ng-carousel/src/lib/carousel.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { A11yModule } from '@angular/cdk/a11y';
import { CommonModule } from '@angular/common';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { CarouselComponent } from '../public-api';
import { CarouselSlideDirective } from './carousel-slide.directive';
Expand All @@ -16,7 +15,6 @@ describe('VirtualCarouselComponent smoke test suite', () => {
TestBed.configureTestingModule({
imports: [
CommonModule,
BrowserAnimationsModule,
A11yModule,
],
declarations: [
Expand Down
3 changes: 2 additions & 1 deletion projects/ng-carousel/src/lib/carousel.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, ContentChild, Input, Output, ViewEncapsulation } from '@angular/core';
import { map } from 'rxjs/operators';
import { distinctUntilChanged, map } from 'rxjs/operators';

import { CarouselConfig } from './carousel-config.type';
import { CarouselSlideDirective } from './carousel-slide.directive';
Expand Down Expand Up @@ -54,6 +54,7 @@ export class CarouselComponent<T = any> {
@Output() itemIndexChange = this.carousel.carouselStateChanges()
.pipe(
map((state: CarouselState<T>) => state.activeItemIndex),
distinctUntilChanged(),
);

get slideIndex(): number {
Expand Down
6 changes: 4 additions & 2 deletions projects/ng-carousel/src/lib/carousel.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { A11yModule } from '@angular/cdk/a11y';
import { CommonModule } from '@angular/common';
import { AsyncPipe, NgForOf, NgTemplateOutlet } from '@angular/common';
import { NgModule } from '@angular/core';

import { CarouselSlideDirective } from './carousel-slide.directive';
Expand All @@ -10,7 +10,9 @@ import { CarouselEngineComponent } from './private/views/carousel-engine.compone

@NgModule({
imports: [
CommonModule,
NgForOf,
AsyncPipe,
NgTemplateOutlet,
A11yModule,
CarouselPreventGhostClickModule,
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { AnimationPlayer } from '@angular/animations';
import { Subscription } from 'rxjs';

/**
* Animation state that is currently in process
*/
Expand All @@ -9,8 +6,7 @@ export class CarouselAnimation {
constructor(
public from: number,
public to: number,
public player?: AnimationPlayer | null,
public onDoneSubscription$?: Subscription,
public player?: Animation | null,
public startTime = new Date().getTime(),
) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { AnimationBuilder } from '@angular/animations';

import { IdGenerator } from '../id-generator';

/**
Expand All @@ -11,7 +9,6 @@ export interface ProcedureEnvironment {
autoplayAction: () => void;
afterAnimationAction: () => void;
isBrowser: boolean;
animationBuilder: AnimationBuilder | null;
animationBezierArgs: number[];
swipeThreshold: number;
maxOverscroll: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AnimationBuilder } from '@angular/animations';
import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, OnDestroy, PLATFORM_ID, TemplateRef } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
Expand Down Expand Up @@ -48,14 +47,12 @@ export class CarouselService<T> implements OnDestroy {
isBrowser: isPlatformBrowser(this.platformId),
autoplayAction: this.next.bind(this, true),
afterAnimationAction: this.cleanup.bind(this),
animationBuilder: this.animationBuilder,
animationBezierArgs: ANIMATION_BEZIER_ARGS,
swipeThreshold: MAX_SWIPE_THRESHOLD,
maxOverscroll: MAX_OVERSCROLL,
};

constructor(
private animationBuilder: AnimationBuilder,
@Inject(SLIDE_ID_GENERATOR) private slideIdGenerator: IdGenerator,
// tslint:disable-next-line: ban-types
@Inject(PLATFORM_ID) private platformId: Object,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,52 @@
import { NoopAnimationPlayer } from '@angular/animations';
import { NEVER, Subscription } from 'rxjs';

import { CarouselAnimation } from '../../../models/carousel-animation';
import { destroyAnimation } from './destroy-animation';

class MockAnimation implements Animation {
currentTime: number | null = 0;
effect: AnimationEffect | null = null;
cancel(): void {}
commitStyles(): void {}
finish(): void {}
addEventListener<K extends keyof AnimationEventMap>(type: K, listener: (this: Animation, ev: AnimationEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void;
addEventListener(type: unknown, listener: unknown, options?: unknown): void {}
dispatchEvent(event: Event): boolean { return true; }
finished: Promise<Animation> = Promise.resolve(this);
id: string = '1';
oncancel: ((this: Animation, ev: AnimationPlaybackEvent) => any) | null = null;
onfinish: ((this: Animation, ev: AnimationPlaybackEvent) => any) | null = null;
onremove: ((this: Animation, ev: Event) => any) | null = null;
pending: boolean = false;
playState: AnimationPlayState = 'running';
playbackRate: number = 1;
ready: Promise<Animation> = Promise.resolve(this);
replaceState: AnimationReplaceState = 'active';
startTime: number | null = null;
timeline: AnimationTimeline | null = null;
pause(): void {}
persist(): void {}
play(): void {}
reverse(): void {}
updatePlaybackRate(playbackRate: number): void {}
removeEventListener<K extends keyof AnimationEventMap>(type: K, listener: (this: Animation, ev: AnimationEventMap[K]) => any, options?: boolean | EventListenerOptions | undefined): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void;
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {}
}

describe('destroyAnimation test suite', () => {

it('should destroy player', () => {
const animationPlayer = new NoopAnimationPlayer();
spyOn(animationPlayer, 'finish');
spyOn(animationPlayer, 'destroy');
const animation = new CarouselAnimation(
0,
0,
animationPlayer,
new Subscription(),
);
destroyAnimation(animation);
expect(animationPlayer.destroy).toHaveBeenCalledTimes(1);
expect(animationPlayer.finish).toHaveBeenCalledTimes(1);
});

it('should unsubscribe', () => {
const subscription$ = NEVER.subscribe();
const mockAnimation = new MockAnimation();
spyOn(mockAnimation, 'finish');
spyOn(mockAnimation, 'cancel');
const animation = new CarouselAnimation(
0,
0,
null,
subscription$,
mockAnimation,
);
destroyAnimation(animation);
expect(subscription$.closed).toBeTruthy('subscription not closed');
expect(mockAnimation.cancel).toHaveBeenCalledTimes(1);
expect(mockAnimation.finish).toHaveBeenCalledTimes(0);
});

it('should not crash upon null animation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,5 @@ import { CarouselAnimation } from '../../../models/carousel-animation';
export function destroyAnimation(
animation?: CarouselAnimation | null,
): void {
try {
animation?.player?.finish();
animation?.player?.destroy();
// Ignore exception since player might be already destroyed
// at this moment
} catch (e) {}
animation?.onDoneSubscription$?.unsubscribe();
animation?.player?.cancel();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function startAnimationProcedure(): Procedure {
environment?.animationBezierArgs ?? [],
environment?.isBrowser ?? false,
environment?.afterAnimationAction ?? (() => {}),
environment?.animationBuilder ?? null,
);
const modifiedState: CarouselState = {
...state,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,11 @@
import { AnimationBuilder, AnimationFactory, AnimationPlayer, NoopAnimationPlayer } from '@angular/animations';
import { fakeAsync, tick } from '@angular/core/testing';
import { Subscription } from 'rxjs';

import { CarouselWidthMode } from '../../../../carousel-width-mode';
import { CarouselAnimation } from '../../../models/carousel-animation';
import { startAnimation } from './start-animation';

describe('startAnimation test suite', () => {

class MockAnimationFactory extends AnimationFactory {
create(): AnimationPlayer {
return new NoopAnimationPlayer();
}
}

class MockAnimationBuilder extends AnimationBuilder {
build(): AnimationFactory {
return new MockAnimationFactory();
}
}

let animationBuilder: AnimationBuilder;

beforeEach(() => {
animationBuilder = new MockAnimationBuilder();
});

it('should not run in browserless environment', () => {
const container = null;
const from = 0;
Expand All @@ -44,13 +24,12 @@ describe('startAnimation test suite', () => {
bezierArgs,
isBrowser,
afterAnimationAction,
animationBuilder,
);
expect(result).toBeNull('result is created');
});

it('should create animation instance', () => {
const container = {} as HTMLElement;
const container = document ? document.createElement('div') : {} as HTMLElement;
const from = -20;
const to = 50;
const widthMode = CarouselWidthMode.PX;
Expand All @@ -67,43 +46,10 @@ describe('startAnimation test suite', () => {
bezierArgs,
isBrowser,
afterAnimationAction,
animationBuilder,
);
expect(result instanceof CarouselAnimation).toBeTruthy('result is not created');
expect(result?.onDoneSubscription$ instanceof Subscription).toBeTruthy('subscription is not Subscription');
expect(result?.player).toBeTruthy('player is not created');
expect(result?.from).toBe(-20, 'incorrect from value');
expect(result?.to).toBe(50, 'incorrect to value');

// Cleanup
result?.onDoneSubscription$?.unsubscribe();
});

it('should emit onDone', fakeAsync(() => {
const container = {} as HTMLElement;
const from = 0;
const to = 0;
const widthMode = CarouselWidthMode.PX;
const duration = 0;
const bezierArgs = [0, 0, 0, 0];
const isBrowser = true;
const afterAnimationActionObject = {action: () => {}};
spyOn(afterAnimationActionObject, 'action');
const result = startAnimation(
container,
from,
to,
widthMode,
duration,
bezierArgs,
isBrowser,
afterAnimationActionObject.action,
animationBuilder,
);
tick();
expect(afterAnimationActionObject.action).toHaveBeenCalledTimes(1);

// Cleanup
result?.onDoneSubscription$?.unsubscribe();
}));
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { animate, AnimationBuilder, style } from '@angular/animations';
import { bindCallback } from 'rxjs';

import { CarouselWidthMode } from '../../../../carousel-width-mode';
import { CarouselAnimation } from '../../../models/carousel-animation';

Expand All @@ -13,38 +10,31 @@ export function startAnimation(
bezierArgs: number[],
isBrowser: boolean,
afterAnimationAction: () => void,
animationBuilder: AnimationBuilder | null,
): CarouselAnimation | null {
if (!isBrowser || !container || from === null || !animationBuilder) {
if (!isBrowser || !container || from === null) {

return null;
}

const cubicBezier = `cubic-bezier(${bezierArgs[0]},${bezierArgs[1]},${bezierArgs[2]},${bezierArgs[3]})`;
const animationFactory = animationBuilder.build([
style({
transform: `translateX(${from}${widthMode})`,
}),
animate(`${transitionDuration}ms ${cubicBezier}`, style({
transform: `translateX(${to}${widthMode})`,
})),
]);
const animationPlayer = animationFactory.create(container);
// Wrap onDone into observable
const boundFunction = bindCallback(animationPlayer.onDone); // Wrap function into function that returns observable
const onDone$ = boundFunction.call(animationPlayer); // Receive observable with context of animation player
const subscription$ = onDone$
.subscribe(() => {
animationPlayer.destroy();
const animationNative = container?.animate?.([
{ transform: `translateX(${from}${widthMode})` },
{ transform: `translateX(${to}${widthMode})` },
], {
duration: transitionDuration,
easing: cubicBezier,
});
if (animationNative) {
animationNative.onfinish = () => {
afterAnimationAction();
});
}
}
const animation = new CarouselAnimation(
from,
to,
animationPlayer,
subscription$,
animationNative,
);
animationPlayer.play();
animationNative?.play?.();

return animation;
}
Loading

0 comments on commit 03a0035

Please sign in to comment.