I'm creating a webpage having full page width/height div's.
While scrolling down I've two types of methods.
Scroll on Click
//HTML
<a (click)="goToDiv('about')"></a>
//JS
goToDiv(id) {
let element = document.querySelector("#"+id);
element.scrollIntoView(element);
}
Scroll on HostListener
#HostListener("window:scroll", ['$event'])
onWindowScroll($event: any): void {
this.topOffSet = window.pageYOffset;
//window.scrollTo(0, this.topOffSet+662);
}
1. How to add a scrolling animation effects?
Just like :
$('.scroll').on('click', function(e) {
$('html, body').animate({
scrollTop: $(window).height()
}, 1200);
});
2. And how to use HostListener to scroll to next div?
You can also use the CSS property
scroll-behavior: smooth
in combination with
var yPosition = 1000;
window.scrollTo(0,yPosition)
Ref: developer.mozilla.org/docs/Web/CSS/scroll-behavior
This one is fun. The solution, as with most things angular 2, is observables.
getTargetElementRef(currentYPos: int): ElementRef {
// you need to figure out how this works
// I can't comment much on it without knowing more about the page
// but you inject the host ElementRef in the component / directive constructor and use normal vanillaJS functions to find other elements
}
//capture the scroll event and pass to a function that triggers your own event for clarity and so you can manually trigger
scrollToSource: Subject<int> = new Subject<int>();
#HostListener("window:scroll", ['$event'])
onWindowScroll($event: any): void {
var target = getTargetElementRef(window.pageYOffset);
this.scrollTo(target);
}
scrollTo(target: ElementRef): void {
// this assumes you're passing in an ElementRef, it may or may not be appropriate, you can pass them to functions in templates with template variable syntax such as: <div #targetDiv>Scroll Target</div> <button (click)="scrollTo(targetDiv)">Click To Scroll</button>
this.scrollToSource.next(target.nativeElement.offsetTop);
}
//switch map takes the last value emitted by an observable sequence, in this case, the user's latest scroll position, and transforms it into a new observable stream
this.scrollToSource.switchMap(targetYPos => {
return Observable.interval(100) //interval just creates an observable stream corresponding to time, this emits every 1/10th of a second. This can be fixed or make it dynamic depending on the distance to scroll
.scan((acc, curr) => acc + 5, window.pageYOffset) // scan takes all values from an emitted observable stream and accumulates them, here you're taking the current position, adding a scroll step (fixed at 5, though this could also be dynamic), and then so on, its like a for loop with +=, but you emit every value to the next operator which scrolls, the second argument is the start position
.do(position => window.scrollTo(0, position)) /// here is where you scroll with the results from scan
.takeWhile(val => val < targetYPos); // stop when you get to the target
}).subscribe(); //don't forget!
With a click this is easy to use. You just bind scrollTo to a click
This only works for scrolling in one direction, However this should get you started. You can make scan smarter so it subtracts if you need to go up, and instead use a function inside takeWhile that figures out the correct termination condition based on if going up or down.
edit: rxjs 5+ compatible version
this.scrollToSource.pipe(switchMap(targetYPos =>
interval(100).pipe( //interval just creates an observable stream corresponding to time, this emits every 1/10th of a second. This can be fixed or make it dynamic depending on the distance to scroll
scan((acc, curr) => acc + 5, window.pageYOffset), // scan takes all values from an emitted observable stream and accumulates them, here you're taking the current position, adding a scroll step (fixed at 5, though this could also be dynamic), and then so on, its like a for loop with +=, but you emit every value to the next operator which scrolls, the second argument is the start position
takeWhile(val => val < targetYPos)) // stop when you get to the target
)).subscribe(position => window.scrollTo(0, position)); // here is where you scroll with the results from scan
I spent days trying to figure this out. Being a newbie I tried many things and none of them work. Finally, I have a solution so I will post it here.
There are 2 steps:
Animate when things appear.
Make things appear when scrolling.
Part 1: I found out these two great tutorials for newbies:
The most basic one
The one that actually animates when stuff appears
Part 2: I simply find the solution in this answer
Part 1 Step by Step:
Add the line import { BrowserAnimationsModule } from '#angular/platform-browser/animations'; to /src/app/app.module.ts and then also:
#NgModule({
// Other arrays removed
imports: [
// Other imports
BrowserAnimationsModule
],
})
In the component.ts you want to animate, add: import { trigger,state,style,transition,animate } from '#angular/animations'; And then:
#Component({
// Here goes the selector and templates and etc.
animations: [
trigger('fadeInOut', [
state('void', style({
opacity: 0
})),
transition('void <=> *', animate(1000)),
]),
]
})
Finally, in the HTML item you want to animate, add [#fadeInOut].
If everything was done correctly, you should now have an animation (but it happens as soon as the webpage loads and not when you scroll.
Part 2 Step by Step:
Create a file .ts like for example appear.ts and copy-paste this code:
import {
ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
} from '#angular/core';
import { Observable, Subscription, fromEvent } from 'rxjs';
import { startWith } from 'rxjs/operators';
//import 'rxjs/add/observable/fromEvent';
//import 'rxjs/add/operator/startWith';
#Directive({
selector: '[appear]'
})
export class AppearDirective implements AfterViewInit, OnDestroy {
#Output()
appear: EventEmitter<void>;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef){
this.appear = new EventEmitter<void>();
}
saveDimensions() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.offsetHeight;
this.windowHeight = window.innerHeight;
}
saveScrollPos() {
this.scrollPos = window.scrollY;
}
getOffsetTop(element: any){
let offsetTop = element.offsetTop || 0;
if(element.offsetParent){
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility(){
if(this.isVisible()){
// double check dimensions (due to async loaded contents, e.g. images)
this.saveDimensions();
if(this.isVisible()){
this.unsubscribe();
this.appear.emit();
}
}
}
isVisible(){
return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
}
subscribe(){
this.subscriptionScroll = fromEvent(window, 'scroll').pipe(startWith(null))
.subscribe(() => {
this.saveScrollPos();
this.checkVisibility();
});
this.subscriptionResize = fromEvent(window, 'resize').pipe(startWith(null))
.subscribe(() => {
this.saveDimensions();
this.checkVisibility();
});
}
unsubscribe(){
if(this.subscriptionScroll){
this.subscriptionScroll.unsubscribe();
}
if(this.subscriptionResize){
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit(){
this.subscribe();
}
ngOnDestroy(){
this.unsubscribe();
}
}
Import it using import {AppearDirective} from './timeline/appear';and add it to the imports as:
#NgModule({
declarations: [
// Other declarations
AppearDirective
],
// Imports and stuff
Somewhere in the class do:
hasAppeared : boolean = false;
onAppear(){
this.hasAppeared = true;
console.log("I have appeared!"); // This is a good idea for debugging
}
Finally, in the HTML add the two following:
(appear)="onAppear()" *ngIf="hasAppeared"
You can check this is working by checking the console for the message "I have appeared!".
The #bryan60 answer works, but I was not comfortable with it, and I preferred to use TimerObservable which seems less confusing for other teammates and also easier to customize for future uses.
I suggest you have a shared service for times you're touching DOM, or working with scroll and other HTML element related issues; Then you can have this method on that service (otherwise having it on a component does not make any problem)
// Choose the target element (see the HTML code bellow):
#ViewChild('myElement') myElement: ElementRef;
this.scrollAnimateAvailable:boolean;
animateScrollTo(target: ElementRef) {
if (this.helperService.isBrowser()) {
this.scrollAnimateAvailable = true;
TimerObservable
.create(0, 20).pipe(
takeWhile(() => this.scrollAnimateAvailable)).subscribe((e) => {
if (window.pageYOffset >= target.nativeElement.offsetTop) {
window.scrollTo(0, window.pageYOffset - e);
} else if (window.pageYOffset <= target.nativeElement.offsetTop) {
window.scrollTo(0, window.pageYOffset + e);
}
if (window.pageYOffset + 30 > target.nativeElement.offsetTop && window.pageYOffset - 30 < target.nativeElement.offsetTop) {
this.scrollAnimateAvailable = false;
}
});
}
}
scrollToMyElement(){
this.animateScrollTo(this.myElement)
}
You need to pass the element to this method, here is how you can do it:
<a (click)="scrollToMyElement()"></a>
<!-- Lots of things here... -->
<div #myElement></div>
Related
Here is my code:
app.component.ts
notifier$ = new BehaviorSubject<any>({});
notify() {
this.notifier$.next({});
}
app.component.html
<div (scroll)="notify()"></div>
<child-component [inp]="notifier$ | async" />
The problem is that when a user is scrolling, the notify() function is called repeatedly and I only want to call notify() once each time the user starts to scroll.
I can accomplish what I want this way:
scrolling = false;
scrollingTimer: NodeJS.Timer;
notify() {
clearTimeout(this.scrollingTimer);
if (!this.scrolling) {
this.notifier$.next({});
}
this.scrolling = true;
this.scrollingTimer = setTimeout(() => (this.scrolling = false), 1000);
}
but I would like to do this with rxjs. However debounceTime is the opposite of what I want, and neither throttleTime nor auditTime are what I want either. Is there a way to do this?
you could build an observable like so:
const scroll$ = fromEvent(document, 'scroll');
const scrollEnd$ = scroll$.pipe(
switchMapTo(
timer(1000) // on every scroll, restart a timer
)
);
const scrollStart$ = scrollEnd$.pipe( // the scroll end event triggers switch to scroll$
startWith(0), // but start it off
switchMapTo(
scroll$.pipe( // then just take the first scroll$ event
first()
)
)
);
scrollStart$.subscribe(v => console.log('scroll start'));
you could generalize it to an operator:
function firstTimeout(timeout: number) { // welcoming notes on a better name
return input$ => {
const inputTimeout$ = input$.pipe(
switchMapTo(timer(timeout))
);
return inputTimeout$.pipe(
startWith(0),
switchMapTo(input$.pipe(first()))
);
};
}
and use it like:
notifier$.pipe(firstTimeout(1000)).subscribe(v => console.log('took one'));
a good idea for this case might be to wrap it in a directive for easy reuse:
#Directive({
selector: '[scrollStart]'
})
export class ScrollStartDirective {
private scrollSource = new Subject();
#HostListener('scroll', ['$event'])
private onScroll(event) {
this.scrollSource.next(event);
}
#Output()
scrollStart = new EventEmitter();
constructor() {
this.scrollSource.pipe(firstTimeout(1000)).subscribe(this.scrollStart);
}
}
then you can use it like this:
<div (scrollStart)="notify()"></div>
When the user scrolls you want notify$ to emit for each scroll event. This provides a constant stream of emitted values. So you want notifier$ to emit once when the stream starts, and again when it's idle for 1 second.
notify$ = new Subject();
notifier$ = merge(
notify$.pipe(first()),
notify$.pipe(switchMap(value => of(value).pipe(delay(1000))))
).pipe(
take(2),
repeat()
);
<div (scroll)="notify$.next()"></div>
You merge two observables. The first emits immediately, and the second emits after a 1 second delay. You use a switchMap so that the delayed observable is always restarted.
We take the next 2 values which triggers the stream to complete, and we use repeat to start over.
you can use take(1)
this.inp.pipe(
take(1),
).subscribe(res => console.log(res));
take(1) just takes the first value and completes. No further logic is involved.
Of course you are supposed to use the above in your child component :)
Also since your observable is finishing...you can create a new subject and pass it to child component every time he scrolls
notify() {
this.notifier$ = new BehaviorSubject<any>({});
this.notifier$.next({});
}
I'm new to vuejs but I was trying to get the window size whenever I
resize it so that i can compare it to some value for a function that I
need to apply depending on the screen size. I also tried using the
watch property but not sure how to handle it so that's probably why it didn't work
methods: {
elem() {
this.size = window.innerWidth;
return this.size;
},
mounted() {
if (this.elem < 767){ //some code }
}
Put this code inside your Vue component:
created() {
window.addEventListener("resize", this.myEventHandler);
},
destroyed() {
window.removeEventListener("resize", this.myEventHandler);
},
methods: {
myEventHandler(e) {
// your code for handling resize...
}
}
This will register your Vue method on component creation, trigger myEventHandler when the browser window is resized, and free up memory once your component is destroyed.
For Vue3, you may use the code below:
mounted() {
window.addEventListener("resize", this.myEventHandler);
},
unmounted() {
window.removeEventListener("resize", this.myEventHandler);
},
methods: {
myEventHandler(e) {
// your code for handling resize...
}
}
destroyed and beforeDestroyed is deprecated in Vue3, hence you might want to use the beforeUnmount and unmounted
Simplest approach
https://www.npmjs.com/package/vue-window-size
Preview
import Vue from 'vue';
import VueWindowSize from 'vue-window-size';
Vue.use(VueWindowSize);
You would then access it normally from your components like this:
<template>
<div>
<p>window width: {{ windowWidth }}</p>
<p>window height: {{ windowHeight }}</p>
</div>
</template>
I looked at the code of that library vue-window-size, and besides the additional logic, it's just adding an event listener on window resize, and it looks like it can be instructed to debounce. Source
The critical problem for me is that my Vue SPA app does not emit a window resize event when a vue-router route changes that makes the <html> element go from 1000px to 4000px, so it's causing me all kinds of problems watching a canvas element controlled by p5.js to redraw a wallpaper using p5.resizeCanvas().
I have a different solution now that involves actively polling the page's offset height.
The first thing to be aware of is JavaScript memory management, so to avoid memory leaks, I put setInterval in the created lifecycle method and clearInterval in the beforeDestroy lifecycle method:
created() {
this.refreshScrollableArea = setInterval(() => {
const { offsetWidth, offsetHeight } = document.getElementById('app');
this.offsetWidth = offsetWidth;
this.offsetHeight = offsetHeight;
}, 100);
},
beforeDestroy() {
return clearInterval(this.refreshScrollableArea);
},
As hinted in the above code, I also placed some initial state:
data() {
const { offsetWidth, offsetHeight } = document.querySelector('#app');
return {
offsetWidth,
offsetHeight,
refreshScrollableArea: undefined,
};
},
Note: if you are using getElementById with something like this.id (ie: an element that is a child in this component), document.getElementById(this.id) will be undefined because DOM elements load outer-to-inner, so if you see an error stemming from the data instantiation, set the width/height to 0 initially.
Then, I put a watcher on offsetHeight to listen for height changes and perform business logic:
watch: {
offsetHeight() {
console.log('offsetHeight changed', this.offsetHeight);
this.state = IS_RESET;
this.setState(this.sketch);
return this.draw(this.sketch);
},
},
Conclusion: I tested with performance.now() and:
document.querySelector('#app').offsetHeight
document.getElementById('app').offsetHeight
document.querySelector('#app').getClientBoundingRect().height
all execute in about the exact same amount of time: 0.2ms, so the above code is costing about 0.2ms every 100ms. I currently find that reasonable in my app including after I adjust for slow clients that operate an order of magnitude slower than my localmachine.
Here is the test logic for your own R&D:
const t0 = performance.now();
const { offsetWidth, offsetHeight } = document.getElementById('app');
const t1 = performance.now();
console.log('execution time:', (t1 - t0), 'ms');
Bonus: if you get any performance issue due to long-running execution time on your setInterval function, try wrapping it in a double-requestAnimationFrame:
created() {
this.refreshScrollableArea = setInterval(() => {
return requestAnimationFrame(() => requestAnimationFrame(() => {
const { offsetWidth, offsetHeight } = document.getElementById(this.id);
this.offsetWidth = offsetWidth;
this.offsetHeight = offsetHeight;
}));
}, 100);
},
requestAnimationFrame itself a person should research. I will leave it out of the scope of this answer.
In closing, another idea I researched later, but am not using is to use a recursive setTimeout function with a dynamic timeout on it (ie: a timeout that decays after the page loads); however, if you consider the recursive setTimeout technique, be conscious of callstack/function-queue length and tail call optimization. Stack size could run away on you.
You can use this anywhere anytime
methods: {
//define below method first.
winWidth: function () {
setInterval(() => {
var w = window.innerWidth;
if (w < 768) {
this.clientsTestimonialsPages = 1
} else if (w < 960) {
this.clientsTestimonialsPages = 2
} else if (w < 1200) {
this.clientsTestimonialsPages = 3
} else {
this.clientsTestimonialsPages = 4
}
}, 100);
}
},
mounted() {
//callback once mounted
this.winWidth()
}
I'm looking to perform some physics animations and have the animation carried out on a set of DOM elements. Not canvas. Very important: Not canvas.
I have it working, but the performance is slower than I anticipated even considering how expensive DOM manipulations are. It borders on unusable if you have more than a few components on the page at a time even if you adjust the interval to be less frequent.
I'm wondering if there is a simpler or more performant way while keeping things within Angular. Maybe a way to skip the Angular rendering of the zone altogether? Doing it vanilla without utilizing Angular bindings and such is way more performant so I'm wondering if I'm just doing the Angular portion wrong, or if I should break these sections free of Angular. I thought Zones were supposed to outperform global manipulation though...?
Example code to wiggle something across the screen (the real animations are more complicated but follow this exact technique):
#Component({
selector: 'thingy,[thingy]',
template: `<div #container [ngStyle]="getStyle()"><ng-content></ng-content></div>`
})
export class Thingy implements OnInit, OnDestroy {
private _x:number = 0;
private _y:number = 0;
private _interval:any;
private _style:CSSStyleDeclaration = {
left: 0,
top: 0,
position: absolute
};
constructor(){}
ngOnInit() {
this._interval = setInterval(() => {
this._x++;
this._y = Math.sin(this._x);
this._style.left = this._x + "px";
this._style.top = this._y + "px";
});
}
ngOnDestroy() {
clearInterval(this._interval); // because it continues ticking after destroy
}
getStyle():CSSStyleDeclaration {
return this._style; // in angular1 it was bad joojoo to return a new object each time so i avoid doing so here too
}
}
I've optimized this approach as much as I know how. I think the built-in animation metadata solution could handle most of the scenarios but I haven't tried because a) I can't imagine how adding more abstraction increases performance and b) these animations are not state transitions so it doesn't seem appropriate.
I've also tried using a template more like this but it doesn't seem to be much different:
<div [style.top.px]="_y" [style.left.px]="_x"><ng-content></ng-content></div>
Also, I've tried directly messing with the ElementRef but that certainly didn't help:
#ViewChild("container") container:ElementRef;
this._container.nativeElement.styles.top = this._y + "px";
If it is best to do this outside of Angular's control, is there any standard for that? I can draw the DOM element with a Component and then dispatch a Window event to kickstart non-angular code...
Also of note: I cannot start at point A and jump immediately to point B in order to let CSS transitions paint the animation. The animations are not predictable enough to transition/ease. Unless theres a very clever solution, I don't see how it can be animated except for ticking through each step.
I solved the performance issue by using a global ticker instead of having each component responsible for its own. The ticker is a simple Observable which each component subscribes to and unsubscribes from where it would otherwise have started and stopped its own interval.
import {Injectable} from '#angular/core';
import {Observer, Observable} from 'rxjs';
#Injectable()
export class TickerService {
private _observer: Observer<number>;
private _timer:any;
public data$ = new Observable(observer => this._observer = observer).share();
constructor() {
this.data$ = new Observable(observer => {
this._observer = observer;
}).share();
}
public start():void {
if(this._timer) { // not required... just didn't want to have two going at once
this.stop();
}
this._timer = setInterval(() => {
this.tick();
}, 30);
}
public stop():void {
clearInterval(this._timer);
this._timer = 0;
}
private tick():void {
this._observer.next(new Date().getTime()); // the date part is irrelevant. i just wanted to use it to track the performance lag between each tick
}
}
Items can them react to it as such:
#Component({
selector: 'thingy,[thingy]',
template: `<div #container [ngStyle]="getStyle()"><ng-content></ng-content></div>`
})
export class Thingy implements OnInit, OnDestroy {
private _x:number = 0;
private _y:number = 0;
private _subscription:Subscription;
private _style:CSSStyleDeclaration = {
left: 0,
top: 0,
position: absolute
};
constructor(private _ticker:TickerService){}
ngOnInit() {
this._subscription = this._ticker.data$.subscribe(() => {
this._x++;
this._y = Math.sin(this._x);
this._style.left = this._x + "px";
this._style.top = this._y + "px";
});
this._ticker.start(); // even though every instance will call this, there's a guard in the TickerService to only allow one ticker to run
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
getStyle():CSSStyleDeclaration {
return this._style;
}
}
This small change took the performance for 5 Thingys from about 5fps to about 60fps. I believe this to be because each Thingy spawned its own ticker, causing its own (blocking) digest/paint to run out of sequence with the others. This caused Angular to digest each change individually, and a giant stack built up. Instead of one tick with 5 updates to perform, it was performing 5 ticks with 1 update each.
So now, all changes are made and then executed within one digest/zone update.
I'm currently trying to give classes to an wrapper that contains all of my app, i usually find this handy for giving certain states like when the header is fixed, or the menu's are opened etc.
So upon reading through some the docs of angular i should probably use an 'Directive'. Now i got this all set up and it looks like this:
constructor(private router:Router, #Inject(DOCUMENT) private document:Document, el:ElementRef, renderer:Renderer) {
this.setClasses(el, renderer);
}
setClasses(el:ElementRef, renderer:Renderer) {
renderer.setElementClass(el.nativeElement, 'header-fixed', this.headerFixed);
}
#HostListener("window:scroll", [])onWindowScroll() {
let number = this.document.body.scrollTop;
if (number > 100) {
this.headerFixed = true;
} else if (this.headerFixed && number < 10) {
this.headerFixed = false;
}
}
Now this is working perfectly but as you can see i'm toggling the headerFixed variable depending on scroll position. However i could of course run the function setClasses() again and it will work but is there anyway to subscribe/watch the variable and update automatically when changed?
Or is there even a better way of achieving wat i'm trying to do?
You can use #HostBinding like:
#HostBinding('class.header-fixed') get myClass() {
return someCondition;
}
Plunker Example
I'm trying to make buttons the same width but I'm having problems actually waiting for Angular 2 to finish all the rendering and stuff before accessing the width of the buttons.
I tried using DoCheck which "works", by "works" I mean that it does pick up the correct width value, but only the second time it runs.
Since NgDoCheck runs twice for some reason, as soon as I add the button.style.width = widest + 'px'; statement, the buttons will all get a width of 115px, so the second time ngDoCheck runs, it will go over all the buttons again, but this time all the buttons will be set to 115px, so widest can never become the correct value of 159px since we set the value the first time ngDoCheck ran.
What would be the correct way to go about this issue? Using DoCheck just seems wrong since it runs twice, and I'm not really checking input values which is DoCheck's use case when reading the docs.
export class ButtonGroupComponent implements DoCheck {
constructor(private _elementRef: ElementRef) {}
ngDoCheck() {
if (this.equalWidth) {
let buttons = this._elementRef.nativeElement.getElementsByTagName('button');
let widest = 0;
for (var button of buttons) {
if (button.getBoundingClientRect().width > widest) {
widest = button.getBoundingClientRect().width;
}
}
for (var button of buttons) {
button.style.width = widest + 'px';
}
}
}
}
Using AfterViewInit seems to do the trick, it only execute once resulting in the correct behavior and correct values. It's as simple as swapping DoCheck and ngDoCheck for AfterViewInit and ngAfterViewInit:
export class ButtonGroupComponent implements AfterViewInit {
constructor(private _elementRef: ElementRef) {}
ngAfterViewInit() {
if (this.equalWidth) {
let buttons = this._elementRef.nativeElement.getElementsByTagName('button');
let widest = 0;
for (var button of buttons) {
if (button.getBoundingClientRect().width > widest) {
widest = button.getBoundingClientRect().width;
}
}
for (var button of buttons) {
button.style.width = widest + 'px';
}
}
}
}
Hope this helps someone else doing DOM manipulation.