References
Scenario
When I create landing page with angular 2+, animation is run immediately even component is not show. After contact with google =)), well, I found that is normal case we meet went using css animation. This is why I create this post. Enjoy!
Requirement: Read about intersection observer api and angular animation above.
Thank @Epenance because he creates the lib NGX Animate In. I took a reference from his code and created this post with some of my own opinions and my scenario.
Implement Code
// animations/directives/animate-after-appear.directive.ts
import { Directive, Input, ElementRef, OnInit } from '@angular/core';
import {
animate,
AnimationBuilder,
AnimationFactory,
AnimationMetadata,
AnimationPlayer,
style,
} from '@angular/animations';
import { IntersectionObserverService } from '../services/intersection-observer.service';
import * as buildInAnmiations from '../animations';
@Directive({
selector: '[animateAfterAppear]',
})
export class AnimateAfterAppearDirective implements OnInit {
@Input() animateAfterAppear: 'fadeIn' | 'fadeInDown';
@Input() animationOptions: any; // custom your own animations
player?: AnimationPlayer;
constructor(
private _observer: IntersectionObserverService,
private el: ElementRef,
private animationBuilder: AnimationBuilder
) {}
ngOnInit() {
let animation: AnimationFactory;
if (
!!this.animationOptions !== null &&
this.animationOptions !== undefined
) {
animation = this.animationBuilder.build(this.animationOptions);
}
if (
!!this.animateAfterAppear &&
!!buildInAnmiations[this.animateAfterAppear]
) {
console.log('build in', this.animateAfterAppear);
animation = this.animationBuilder.build(
buildInAnmiations[this.animateAfterAppear]
);
} else {
animation = this.animationBuilder.build([
style({ opacity: 0, transform: 'translateX(-100px)' }),
animate(
'1200ms cubic-bezier(0.35, 0, 0.25, 1)',
style({ opacity: 1, transform: 'translateX(0)' })
),
]);
}
if (this._observer.isSupported()) {
this.player = animation.create(this.el.nativeElement);
this.player.init();
const callback = this.startAnimating.bind(this);
this._observer.addTarget(this.el.nativeElement, callback);
}
}
/**
* Builds and triggers the animation
* when it enters the viewport
* @param {boolean} inViewport
*/
startAnimating(inViewport?: boolean, element?: Element): void {
console.log('start animating');
if (inViewport) {
this.player?.play();
}
}
}
// animations/services/intersection-observer.service.ts
import { Injectable, Optional } from '@angular/core';
export class IntersectionObserverServiceConfig {
root?: Element | null;
rootMargin?: string;
threshold?: number | number[];
}
export type CallbackType = (inViewport?: boolean, element?: Element) => void;
export interface WatchedItem {
element: Element;
callback: CallbackType;
}
@Injectable()
export class IntersectionObserverService {
options: IntersectionObserverServiceConfig = {
rootMargin: '0px',
threshold: 0.1,
};
// where Intersection is support
supported = false;
watching: Array<WatchedItem> = [];
observer: IntersectionObserver | null;
/**
* Assigns the user config if they wish to
* override the defaults by using forRoot
* @param {IntersectionObserverServiceConfig} config
*/
constructor(@Optional() config: IntersectionObserverServiceConfig) {
this.supported =
'IntersectionObserver' in window && 'IntersectionObserverEntry' in window;
if (config) {
this.options = { ...this.options, ...config };
}
this.observer = this.supported
? new IntersectionObserver(this.handleEvent.bind(this), this.options)
: null;
}
/**
* Handles events made by the observer
* @param {IntersectionObserverEntry[]} entries
*/
handleEvent(entries: IntersectionObserverEntry[]): void {
entries.forEach((entry: IntersectionObserverEntry) => {
const target = this.watching.find((element) => {
return element.element === entry.target;
});
if (entry.isIntersecting) {
// un observe after intersecting
this.observer?.unobserve(entry.target);
// callback
target?.callback(true, entry.target);
// remove item in watching list
this.watching = this.watching.filter(
(element) => element.element !== entry.target
);
}
});
}
/**
* Adds the target to our array so we can call its
* call back when it enters the viewport
* @param {Element} element
* @param {CallbackType} callback
*/
addTarget(element: Element, callback: CallbackType): void {
this.observer?.observe(element);
this.watching.push({
element: element,
callback: callback,
});
}
isSupported() {
return this.supported;
}
}
// animations/animations/fade.ts
import { animate, AnimationMetadata, state, style } from '@angular/animations';
export const fadeIn: AnimationMetadata[] = [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
];
export const fadeInDown: AnimationMetadata[] = [
style({ opacity: 0, transform: 'translate3d(0, -20%, 0)' }),
animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];
export const fadeInUp: AnimationMetadata[] = [
style({ opacity: 0, transform: 'translate3d(0, 20%, 0)' }),
animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];
export const fadeInLeft: AnimationMetadata[] = [
style({ opacity: 0, transform: 'translate3d(-10%, 0, 0)' }),
animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];
export const fadeInRight: AnimationMetadata[] = [
style({ opacity: 0, transform: 'translate3d(10%, 0, 0)' }),
animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];
// animations/animations/index.ts
export * from './fade';
Top comments (0)