Angular 2 getBoundingClientRect from component - javascript

I have component like below which is basically a popover:
import {Component, Input, ViewChild} from 'angular2/core'
declare var $: any;
#Component({
selector: 'popover',
template: `
<div id="temp" [ngStyle]="{'position':'absolute', 'z-index':'10000', 'top': y + 'px', left: x + 'px'}"
[hidden]="hidden" #temp>
<ng-content></ng-content>
</div>
`
})
export class Popover {
#ViewChild("temp") temp;
private hidden: boolean = true;
private y: number = 0;
private x: number = 0;
show(target, shiftx = 0, shifty = 0){
let position = $(target).offset();
this.x = position.left + shiftx;
this.y = position.top + shifty;
this.hidden = false;
console.log("#temp", this.temp.nativeElement.getBoundingClientRect()); //all 0s
console.log("temp id", document.getElementById('temp').getBoundingClientRect()); //all 0s
}
hide(){
this.hidden = true;
}
}
Inside the show() method I am trying to get the result of getBoundingClientRect() but its returning 0 for all properties but when I type in document.getElementById("temp").getBoundingClientRect() from Chrome's console I get proper result with actual values in the properties. Why the difference and what can I do to get the actual value from my component?

Instead of using setTimeout or lifecycle hooks that can be triggered more than once, I solved it with setting the Renderer to watch the window load event.
import { OnInit, Renderer2} from '#angular/core';
...
export class MyComponent implements OnInit {
constructor(private el: ElementRef, private render: Renderer2) {}
ngOnInit() {
this.render.listen('window', 'load', () => {
const rect = this.el.nativeElement.getBoundingClientRect().top;
})
}
}
Maybe this helps someone's usecase.

I would use ngAfterContentChecked(). It works perfectly fine for me.
import { AfterContentChecked } from '#angular/core';
export class ModelComponent implements AfterContentChecked {
ngAfterContentChecked() {
//your code
}
}
I hope it helps. :)

For some reason, the DOM was not updated right after it was shown so, a setTimeout e.g. 10 did the trick.

In this context, ngAfterContentChecked works. As answered above.
ngAfterContentChecked hook method runs last in the lifecycle before the ngOnDestroy. You can read more about Lifecycle Hooks here.
Also be careful what you do in here, as it may result into performance issue.
Timing
Called after the ngAfterViewInit() and every subsequent
ngAfterContentChecked().

Related

How to share data between sibling components in Angular

I am still learning and I got stuck so I need to ask a question. My understanding of Input Output decorators is that I need to add selector to html of parent to be able to use them, but for my case I don't think it's the way to go, but someone can prove me wrong.
CASE: For readability purposes I have split components. I have one component, data-fetch-transform that gets the data form local JSON file and does some adjustments to it, and another one, that wants to take that data for further use.
PROBLEM: I am unsure how to read the data from one component in the other. On the example below, how can I get countryNumber and centerNumber result in my other component. I intend to have data-fetch-transform.component.ts just manipulate the data and used in other components
Target component
project/src/app/data-use/data-use.component.ts
Data Source component
project/src/app/data-fetch-transform/data-fetch-transform.component.ts
import { Component, OnInit } from '#angular/core';
import * as data from '../../../../../data/Data.json';
#Component({
selector: 'app-datafetch-transform',
templateUrl: './datafetch-transform.component.html',
styleUrls: ['./datafetch-transform.component.css'],
})
export class DatafetchComponent implements OnInit {
public dataList: any = (data as any).default;
dataPointCount = this.data.length!!;
uniqueValues = (dt: [], sv: string) => {
var valueList: [] = [];
for (let p = 0; p < this.dataPointCount; p++) {
valueList.push(dt[p][sv]);
}
var uniqueValues = new Set(valueList);
return uniqueValues.size;
};
countryNumber=this.uniqueValues(this.dataList, 'Country')
centerNumber=this.uniqueValues(this.dataList, 'Center Name')
constructor() {}
ngOnInit(): void {}
}
You don't need another component for data manipulation (data-fetch-transform), you need a service (data-fetch-transform-service) where you should do the logic.
HERE IS WHAT YOU SHOULD HAVE IN THE SERVICE
private _dataList = new behaviorSubject([]);
public dataList$ = _dataList.asObservable();
for (let p = 0; p < this.dataPointCount; p++) {
// ... do your thing
_valueList.next(result);
}
and in the component you just subscribe to the service:
declarations:
private _subscription = new Subscription()
in constructor:
private dataService:DataFetchTransformService
and in ngOnInit:
this_subscription.add(this.dataService.dataList$.subscribe((response:any)=>{
this.data = response;
}))
in ngOnDestroy():
ngOnDestroy(){
this._subscription.unsubscribe();
}
I strongly suggest to stop using any since it can bring a lot of bugs up.
Also, as a good pattern, I always suggest use behaviorSubject only in the service as a private variable and user a public observable for data.
WHY IS BETTER TO USE A SERVICE
You can subscribe from 100 components and writing only 4 lines of code you bring the data anywhere.
DON'T FORGET TO UNSUBSRIBE IN ngOnDestroy
If you don't unsubscribe, you'll get unexpected behavior.

How to invoke parent function from child component

I am trying invoke parent component from child component that nested within <router-outlet> tag. In my case, I tried to invoke timer.start() function that start the timer which lies within parent component.
I have succefully invoked the parent's function by importing to the child, but the timer is not working. I have tried to log the flag that indicated if the timer is running or not, and it's already in true condition.
Here is the code:
import { NavbarComponent } from './../navbar/navbar.component'; /* This is the parent component */
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-css-inoculation-scoring',
templateUrl: './css-inoculation-scoring.component.html',
styleUrls: ['./css-inoculation-scoring.component.scss'],
providers: [ NavbarComponent ]
})
export class CSSInoculationScoringComponent implements OnInit {
constructor(private _service: DataModelService, private _navbar: NavbarComponent) {}
ngOnInit() {
this.vessel.getVessel();
this._navbar.timer.start();
}
}
And this is the timer.start function:
start: () => {
this.timer.isTicking = true;
this.timer.startTime();
}
The timer.start function also called another function, here is the timer.startTimer() function:
startTime: () => {
if (this.timer.isTicking) {
let hour = parseInt(this.timer.hour, 10);
let minute = parseInt(this.timer.minute, 10);
let second = parseInt(this.timer.second, 10);
second += 1;
if (second > 60) {
minute += 1;
second = 0;
}
if (minute > 60) {
hour += 1;
minute = 0;
}
this.timer.second = second < 10 ? `0${second}` : `${second}`;
this.timer.minute = minute < 10 ? `0${minute}` : `${minute}`;
this.timer.hour = hour < 10 ? `0${hour}` : `${hour}`;
setTimeout(this.timer.startTime, 1000);
}
}
I have idea to change the value of isTicking through the service, and return the observable. I have another case similar, and it's work. But in timer.startTime() function also modified the properties of timer. Should i also use service for that? Or is there any other approach?
I assume that you want to call parent's method with parent's context.
I would recommend to avoid passing components as services, because if there's a functionality that needs to be shared - it should be a service. But if a child needs to trigger parent's method within parent's context, then you can pass it to a child and call if from there.
// child component
import { Component, EventEmitter, OnInit } from '#angular/core';
#Component({
selector: 'app-css-inoculation-scoring',
templateUrl: './css-inoculation-scoring.component.html',
styleUrls: ['./css-inoculation-scoring.component.scss'],
})
export class CSSInoculationScoringComponent implements OnInit {
#Output() startTimer: EventEmitter<any> = new EventEmitter();
constructor(private _service: DataModelService)
ngOnInit() {
this.vessel.getVessel();
this.startTimer.emit();
}
}
// PARENT COMPONENT TEMPLATE
<targeting
(startTimer)="timer.start()">
</targeting>
Theres no way a child component can invoke a function on its parent component directly. Using an EventEmitter as per the above example is the closest thing to it. But if your child component is declared as a child route in a routing module, you wont be able to do this (there now way to bind the event emitter).
I would suggest moving your timer logic into a shared service that can be injected into both components. This way you can have either component call the start functions as and when they need to.
If you provide this service as a singleton (by providing it only once via your module), you will be able to keep track of whether the timer is running via your isTicking flag.

Angular 2 routing inside a component not working (this.router.navigate not working)

Im trying to develop an angular 2 app that routes to a different component once a specific condition is met, for this i used the this.router.navigate method but it doesnt seem to execute as it keeps showing the "Cannot read property 'navigate' of undefined" error. will appreciate any help on this :)
The specific component
import { Component, OnInit } from '#angular/core';
import { RouterModule, Routes, Router } from '#angular/router';
#Component({
selector: 'app-searching',
templateUrl: './searching.component.html',
styleUrls: ['./searching.component.css']
})
export class SearchingComponent implements OnInit {
constructor(private router:Router) { }
ngOnInit() {
var elem = document.getElementById("rightSideMovingProgressBar");
var elem2 = document.getElementById("progressText");
var height = 100;
var height2 = 0;
var id = setInterval(frame, 20);
function frame() {
if (height <= 0) {
clearInterval(id);
this.router.navigate(['/details']);
} else {
height--;
height2++;
elem.style.height = height + 'vh';
elem2.innerHTML = height2 + '%';
}
}
}
}
The error
It's because this in the frame is no longer points to the component. Use the following:
var id = setInterval(()=>{ frame() }, 20);
Read this and this answers for more information and other possible approaches using bind and bound class properties.
You can store this into a variable:
var that = this;
inside the function you can use as
that.router.navigate(['/details']);
Hope this helps :)

Angular 4 - Scroll Animation

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>

Angular2 Physics animation

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.

Categories

Resources