Scroll based on URL fragment (Angular + NGRX + ElementRef) - javascript

I have a main page with couple of sections.
Each section can be reached by typing its fragment name on the url (Ex. www.website.com/#introduction - will open the homepage and scroll to the introduction section automatically).
In order to do that, I need to fetch the section’s container element and run .scrollIntoView() function on it.
What I have done:
List item I have created a store (ngrx) which stores the ElementRef(s) of the sections.
In each section component (located in HomeComponent - parent of the sections), I put a #ViewChild() pointing on the element.
#ViewChild(‘container’, { static: true }) private container: ElementRef;
Then on ngAfterViewInit() of this section component, I put
Store.dispatch() which stores my ElementRef on the store.
this.store.dispatch(new HomeActions.SetElementRef({
linkName: COMP_LINK_NAME,
elementRef: this.container
}));
Then, in HomeComponent, I listen to the sectionsRef store this.store.select('sectionRefs').
Filtered it if its undefined (as it is the default of the store = default statement is empty).
this.sectionRefs$ = this.store.select('sectionRefs').pipe(
map((data: { sectionRefs: SectionReference[] }) => data.sectionRefs), // transform to a more convenient object
tap((sectionRefs: SectionReference[]) => this.sectionRefs = sectionRefs), // Updates the sectionRefs array regardless the completion of it (filter below)
filter((sectionRefs: SectionReference[]) => { // No emission until sectionRefs Element Refs are completed
return sectionRefs.find((sectionRef: SectionReference) => {
return (
sectionRef.element === undefined ||
sectionRef.element === null ||
sectionRef.element.nativeElement.offsetHeight === 0 && sectionRef.element.nativeElement.offsetWidth === 0
);
}) === undefined // Means that no incompleted sectionRef was found
}),
distinctUntilChanged(
(prev: SectionReference[], next: SectionReference[]) => JSON.stringify(prev) === JSON.stringify(next)
),
first()
);
HomeCompoent: And eventually I listened to the ActivatedRoute.fragment, to check which ElementRef is relevant for the scrolling.
HomeComponent: Activate .scrollIntoView() on the relevant
ElementRef.
The issue is that I do it when the page is loaded so the ElementRef.nativeElement.scrollIntoView() is scrolling to incorrect place.
I checked to see the offsetHeight retrieved (ElementRef.nativeElement.offsetHeight) and it was also incorrect.
So In each section Component I wrapped the Store.dispatch() action with setTimeout()
setTimeout(() => {
this.store.dispatch(new HomeActions.SetElementRef({
linkName: COMP_LINK_NAME,
elementRef: this.container
}));
}, 150);
And it works!
But it is looking like a very bad practice to do this work around.
Is there any other solution?

Related

Preserve dynamically added <title> in Angular while reloading page

Is there a possibility when refreshing the page to save the tag that has been dynamically added ?
Now, the moment I refresh the page, while loading, the title tag changes to the original one, which I set in the index.html. When the page is loaded, the title tag then comes back to the correct one which is dynamically added. But, I want the title tag to stay the same while the page is refreshing.
This is my app.component.ts:
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.activatedRoute),
map((route) => {
while (route.firstChild) route = route.firstChild;
return route;
}),
filter((route) => route.outlet === 'primary'),
mergeMap((route) => route.data)
)
.subscribe((event) => {
console.log(event)
this.translateService.get(event['title']).subscribe(name => {
this._seoService.updateTitle(name);
});
this._seoService.updateDescription(event['description'])
});
One approach is to make use of Local Storage to store your dynamic title in there. Here's a simple example where I am storing the title in Local storage and refreshing the page, and retaining my title back. Angular provides a service called Title that allows us dynamically update the title anytime.
<button (click)="setItem()">Click to set a title</button>
<p *ngIf="showInfo" >Refresh the page now :)</p>
export class AppComponent implements OnInit {
showInfo = false;
constructor(private titleService: Title) {}
ngOnInit() {
this.getItem();
}
setItem() {
localStorage.setItem('title', 'Hey World!');
this.showInfo = true;
this.getItem();
}
getItem() {
if (localStorage.getItem('title'))
this.titleService.setTitle(localStorage.getItem('title'));
else this.titleService.setTitle('No title');
}
}
Here's a live application.
Code - Stackblitz

Alternative to setTimeout in Angular 2+

I am dispatching the data to ngrx store. After that I want to scroll to a specific div, which is using this data from store.
#ViewChild('datalist') private myScrollContainer: ElementRef;
this.store.dispatch(new SetClientSearchResultsAction(filteredData));
setTimeout(() => {
this.myScrollContainer.nativeElement.scrollIntoView({ behavior:'smooth', block: 'start'});
}, 300);
Below is the HTML div.
<div #datalist id="mydata" *ngIf="clientSearchResults$ | async as searchResults"
class = 'result'>
<p> hellooo</p>
</div>
I am getting the scroll at my div after dispatching the data to store. But I do not want to use setTimeout. It is unnecessarily waiting for 300 milliseconds. Is there any alternative way to do that ? I just want to scroll to my div, when my data is dispatched or ngif condition got fulfilled.
Below is the constructor of my component where I am getting the value from Store.
constructor(private store: Store<AppState>,
private formBuilder: FormBuilder, private _clientService: ClientService) {
this.clientSearchResults$ = this.store.select('searchResults');
}
You can use Lifecycle hook, AfterViewInit Respond after Angular initializes the component's views and child views / the view that a directive is in.
class MyComponent implements AfterViewInit {
ngAfterViewInit() {
// ...
}
}
There are multiple ways with RXJS
Option 1 - with asapScheduler
asapScheduler.schedule(() => { // Your Code });
Link
Option 2 - with interval
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';
interval(0).pipe(take(1),
).subscribe(value => { // Your Code });
Link
Option 3 - With Promise
Promise.resolve().then(() => { // Your Code });
The Optimum way will be with Option 1 as it is made to avoid setTimeout(deferredTask, 0)
From the RxJS docs for asapScheduler:
asap scheduler will do its best to minimize time between end of
currently executing code and start of scheduled task. This makes it
best candidate for performing so called "deferring". Traditionally
this was achieved by calling setTimeout(deferredTask, 0), but that
technique involves some (although minimal) unwanted delay.
Assuming that clientSearchResults$ will emit a new value when dispatching SetClientSearchResultsAction, you can subscribe to clientSearchResults$ in your component and initiate the scrolling from there.
ngOnInit() {
clientSearchResults$
.subscribe(() => {
this.myScrollContainer.nativeElement.scrollIntoView({ behavior:'smooth', block: 'start'});
});
}

What is the value of 'this' Inside subscribe function of RxJS

Trying to show a popup after routing event I'm facing this issues :
the angular routing event is firing many times after one routerLink click, so I open many popup .
To resolve the first problem I created a boolean to check if it is the first event.
But when I try to change the boolean after the first routing events call it always take the same value in events following.
My question is : is the value of this in subscribe Method a copy of my component ?, otherwise what can cause this issue and how can I solve it.
thanks a lot.
Code :
#Component({
template ='<a routerLink ="/something" >'
})
export class MyComponent implements OnInit{
firstcall : boolean = true;
constructor(private _router : Router){
this._router.events.filter(event => event instanceof NavigationStart && something)
.subscribe( (event) => {
if(this.firstcall) {
this.showPopup()
this.firstcall=false
}
}
}
}
Update
even with this._router.events.distinct(event => event['url']).subscribe not working
Add a variable of "this" (of the class) before the subscribe call and use that variable inside subscribe, else it use the "this" instance of the subscribe method.
Try this,
#Component({
template ='<a routerLink ="/something" >'
})
export class MyComponent implements OnInit{
firstcall : boolean = true;
that : any = this;
constructor(private _router : Router){
this._router.events.filter(event => event instanceof NavigationStart && something)
.subscribe( (event) => {
if(that.firstcall) {
that.showPopup()
that.firstcall=false
}
}
}
}
Hope this helps!

Emit http requests + debounce on Kendo field changes

I have a method which is called everytime some input text is change. (Basically it is a search).
I want to delay post/get request so if user quickly types, only one request will be send to server.
I was thinking something like that:
public partnerFilterChange(value)
{
if (this.partnersSubscriptions)
this.partnersSubscriptions.unsubscribe();
this.partnersSubscriptions = this.partnersService.list({ filter: value })
.debounceTime(5000)
.subscribe(partners =>
{
delete this.partnersSubscriptions;
this.partners = partners;
});
}
but it is not working.
Http request is executed immediately, not after 5 seconds. I also try delay instead debounceTime.
Edited:
I am using kendo drop down list component and its change event, so I have no control over the function call, only on subscribing to http request.
As per my comment. You can't use form.get('fieldName').valueChanges directly since you're using Kendo. But you CAN push the values received from Kendo to your own, custom observable, thus replicating the behavior of valueChanges:
class AppComponent {
// This is your own, custom observable that you'll subscribe to.
// It will contain a stream of filter values received from Kendo.
private _filterValues: Subject<string> = new Subject<string>();
constructor() {
// Start from your custom stream, debounce values, and run http query
this._filterValues.asObservable()
.debounceTime(400)
.mergeMap(value => this.partnersService.list({ filter: value }))
.subscribe(partners => this.partners = partners);
}
// This method is called by Kendo every time the field value changes.
handleFilter(value) {
// Push the value in the custom stream.
this._filterValues.next(value);
}
}
NB. This code assumes that this.partnersService.list() returns an observable.
With this code, every time you update the field, the list of partners should be refreshed and the debounce should be applied. (I haven't tested the code, you might need to adapt it to your own use case.)
import { Component, Output, HostListener, ChangeDetectionStrategy } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { FormGroup, FormControl } from '#angular/forms';
#Component({
selector: 'my-user-search',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<form role="search" class="navbar-form-custom form-inline" [formGroup]="searchForm">
<div class="form-group">
<label for="user-search">Search</label>
<input
id="user-search"
class="form-control"
name="input"
type="text"
placeholder="Search Co-worker"
formControlName="search"
/>
</div>
</form>
`
})
export class UserSearchComponent {
searchForm = new FormGroup({
search: new FormControl('')
});
#Output() search: Observable<string> = this.searchForm.valueChanges
.map(form => form.search)
.debounceTime(700)
.distinctUntilChanged();
#HostListener('window:keyup', ['$event'])
cancelSearch(event) {
if (event.code === 'Escape') {
this.searchForm.reset();
// this.searchControl.setValue('', {emitEvent: true});
}
}
}
Usage
Where the $event value is the search term and (search) will be called first when 700ms has passed and the input is not the same
<my-user-search (search)="handleSearch($event)"></my-user-search>
I had a similar issue in my Angular 2 app, I am just going to paste my solution:
subscribeToSearchQueryChanges(){
const sub = Observable.fromEvent(this.panelSuggestionBox.nativeElement, 'keyup')
.debounceTime(300)
.filter((kbE: KeyboardEvent) => {
return !(kbE['code'] === 'Space' || kbE.key === 'ArrowDown' || kbE.key === 'ArrowUp' || kbE.key === 'Enter' || kbE.key === 'Tab' || kbE.key === 'Shift')
})
.map(() => _.trim(this.panelSuggestionBox.nativeElement.value) )
.filter((term: string) => term.length > 2 )
.switchMap((term: string) => this.suggestionService.getSuggestions(this.suggestionTypes, term))
.subscribe((suggestions: Suggestion[]) => {
this.suggestionsData = this.suggestionService.groupSuggestions(suggestions);
this.toggleSuggestionList();
}, err => {
console.error('suggestions failed', err);
this.removeSubscription(sub);
this.subscribeToSearchQueryChanges();
});
this.addSubscription(sub);
}

Angular 2 How to detect back button press using router and location.go()?

I have built an app that uses router 3.0.0-beta.1 to switch between app sections. I also use location.go() to emulate the switch between subsections of the same page. I used <base href="/"> and a few URL rewrite rules in order to redirect all routes to index.html in case of page refresh. This allows the router to receive the requested subsection as a URL param. Basically I have managed to avoid using the HashLocationStrategy.
routes.ts
export const routes: RouterConfig = [
{
path: '',
redirectTo: '/catalog',
pathMatch: 'full'
},
{
path: 'catalog',
component: CatalogComponent
},
{
path: 'catalog/:topCategory',
component: CatalogComponent
},
{
path: 'summary',
component: SummaryComponent
}
];
If I click on a subsection in the navigation bar 2 things happen:
logation.go() updates the URL with the necessary string in order to indicate the current subsection
A custom scrollTo() animation scrolls the page at the top of the requested subsection.
If I refresh the page I am using the previously defined route and extract the necessary parameter to restore scroll to the requested subsection.
this._activatedRoute.params
.map(params => params['topCategory'])
.subscribe(topCategory => {
if (typeof topCategory !== 'undefined' &&
topCategory !== null
) {
self.UiState.startArrowWasDismised = true;
self.UiState.selectedTopCategory = topCategory;
}
});
All works fine except when I click the back button. If previous page was a different section, the app router behaves as expected. However if the previous page/url was a subsection, the url changes to the previous one, but nothing happens in the UI. How can I detect if the back button was pressed in order to invoke the scrollTo() function to do it's job again?
Most answers I saw relly on the event onhashchange, but this event does not get fired in my app since I have no hash in the URL afterall...
I don't know if the other answers are dated, but neither of them worked well for me in Angular 7. What I did was add an Angular event listener by importing it into my component:
import { HostListener } from '#angular/core';
and then listening for popstate on the window object (as Adrian recommended):
#HostListener('window:popstate', ['$event'])
onPopState(event) {
console.log('Back button pressed');
}
This worked for me.
Another alternative for this issue would be to subscribe to the events emitted by the Angular Router service. Since we are dealing with routing, it seems to me that using Router events makes more sense.
constructor(router: Router) {
router.events
.subscribe((event: NavigationStart) => {
if (event.navigationTrigger === 'popstate') {
// Perform actions
}
});
}
I would like to note that popstate happens when pressing back and forward on the browser. So in order to do this efficiently, you would have to find a way to determine which one is occurring. For me, that was just using the event object of type NavigationStart which gives information about where the user is coming from and where they are going to.
To detect browser back button click
import platformlocation from '#angular/common and place the below code in your constructor :
constructor(location: PlatformLocation) {
location.onPopState(() => {
alert(window.location);
}); }
This is the latest update for Angular 13
You have to first import NavigationStart from the angular router
import { NavigationStart, Router } from '#angular/router';
Then add the following code to the constructor
constructor(private router: Router) {
router.events.forEach((event) => {
if(event instanceof NavigationStart) {
if (event.navigationTrigger === 'popstate') {
/* Do something here */
}
}
});
}
Angular documentation states directly in PlatformLocation class...
This class should not be used directly by an application developer.
I used LocationStrategy in the constructor
constructor(location: LocationStrategy) {
location.onPopState(() => {
alert(window.location);
});
}
A great clean way is to import 'fromEvent' from rxjs and use it this way.
fromEvent(window, 'popstate')
.subscribe((e) => {
console.log(e, 'back button');
});
Using onpopstate event did the trick:
window.addEventListener('popstate',
// Add your callback here
() => self.events.scrollToTopCategory.emit({ categId: self.state.selectedTopCategory })
);
I agree with Adrian Moisa answer,
but you can use "more Angular 2 way" using class PlatformLocation by injecting to your component or service, then you can define onPopState callback this way:
this.location.onPopState(()=>{
// your code here...
this.logger.debug('onpopstate event');
});
Simpler way - Link
import { PlatformLocation } from '#angular/common';
import { NgbModal } from '#ng-bootstrap/ng-bootstrap';
...
constructor(
private platformLocation: PlatformLocation ,
private modalService: NgbModal
)
{
platformLocation.onPopState(() => this.modalService.dismissAll());
}
I honestly don't know your use case. And the thread is quite old. But this was the first hit on Google. And if someone else is looking for this with Angular 2 and ui-router (just as you are using).
It's not technically detecting the back button. It's more detecting whether you as a developer triggered the state change or whether the user updated the URL themselves.
You can add custom options to state changes, this can be done via uiSref and via stateService.go. In your transitions, you can check whether this option is set. (It won't be set on back button clicks and such).
Using ui-sref
<a uiSref="destination-name" [uiOptions]="{custom: {viaSref: true}}">Bar</a>
Using state.go
import {StateService} from '#uirouter/core';
...
#Component(...)
export class MyComponent {
constructor(
private stateService: StateService
) {}
public myAction(): void {
this.stateService.go('destination-name', {}, {custom: {viaGo: true}});
}
}
You can detect it in any transition hook, for example onSucces!
import {Transition, TransitionOptions, TransitionService} from '#uirouter/core';
...
#Component(...)
export class MyComponent implements OnInit {
constructor(
private transitionService: TransitionService
) {}
public ngOnInit(): void {
this.transitionService.onSuccess({}, (transition: Transition) => this.handleTransition(Transition));
}
private handleTransition(transition: Transition): void {
let transitionOptions: TransitionOptions = transition.options();
if (transitionOptions.custom?.viaSref) {
console.log('viaSref!');
return;
}
if (transitionOptions.custom?.viaGo) {
console.log('viaGo!');
return;
}
console.log('User transition?');
}
}
You can check the size of "window.history", if the size is 1 you can't go back.
isCanGoBack(){
window.history.length > 1;
}

Categories