Angular2: router not populating params - javascript

So, I am loosing my mind over this
I have a page with many components... but for some reason I am having problems with one...
it is for mains search in the header of the page... for debugging purposes I stripped it down to bare minimum, and still doesn't work
This is my search component
import { Component, OnInit } from '#angular/core';
import { ROUTER_DIRECTIVES } from '#angular/router';
import { Router, ActivatedRoute } from '#angular/router';
#Component({
selector: 'main-search',
template: `<div></div>`,
})
export class MainSearch implements OnInit {
private sub: any;
constructor(private route: ActivatedRoute){
}
ngOnInit(){
this.sub = this.route.params.subscribe(params => {
console.log('PARAMS FROM MAIN SEARCH', params);
});
}
}
as you can see, I am trying to log the params from the URL (f.e. http://localhost:8080/indices;search=test)
NOT populating
I have a similar component with exact behaviour (subscribing to params onInit...
this.sub = this.route.params.subscribe(params => {
console.log('PARAMS FROM INDICES: ', params);
})
And that one actually logs the bloody params!
From console:
PARAMS FROM MAIN SEARCH Object {} => main-search.ts?8502:24
Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode. => lang.js?c27c:360
null => index.service.ts?0bf5:40
FROM API => index.service.ts?0bf5:49
PARAMS FROM INDICES: Object {search: "test"} => indicesList.component.ts?5ff1:63
The weird thing is that only the mainsearch gets logged to the console before Angular2 disclaimer
What could be the issue that main-search doesn't get the params?

I think you need to use the ActivatedRoute.
This should work in your case:
constuctor(
private _activatedRoute: ActivatedRoute,
) {}
ngOnInit()
this._activatedRoute.params.subscribe(params => console.log(params));
}
The thing is your 'main-search' is a few components deep and the router params observable emits params from the root url. Whereas the ActivatedRoute emits params from the current route.

Related

How to access queryParams in a Resolver Angular

I Need to implement 'search' by passing queryParams through route from the search component to the userList component (example. /search-result?user="Alfred"). Before loading the userList component, i need to make an API call using the queryParams in the userList resolver but the query params keeps showing undefined.
Search Component
search(searchTerm: string) {
if (searchTerm) {
this.router.navigate(['search-result'], { queryParams: { user: searchTerm } });
}
}
UserList Resolver
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService, private route: ActivatedRoute) { }
resolve(): Observable<User[]> {
const searchTerm: string = this.route.snapshot.queryParams['user'];
console.log(searchTerm); //Logs Undefined
return this.userService.getUsers(searchTerm);
}
}
On latest versions of Angular you can get the ActivatedRouteSnapshot on the resolver function.
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService, private route: ActivatedRoute) { }
resolve(**route: ActivatedRouteSnapshot**): Observable<User[]> {
**console.log(route.queryParams)**
return this.userService.getUsers(searchTerm);
}
}
Maybe the resolve function is running before the queryParams are populated in the url. Try doing it in an Rxjs way.
import { filter, map, switchMap, tap } from 'rxjs/operators';
...
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService, private route: ActivatedRoute) { }
resolve(): Observable<User[]> {
return this.route.queryParams.pipe(
tap(params => console.log(`Params: ${params}`)),
// wait until params has user in it
filter(params => !!params['user']),
tap(params => console.log('after filter')),
// extract the value of the user param
map(params => params['user']),
// switch to a new observable stream once we know the searchTerm
switchMap(searchTerm => this.userService.getUsers(searchTerm)),
);
}
}
Edit
Use the tap operator to debug the stream. See what the log is and make sure console.log(Params: ${params}) has the user params.
Edit2
Try
this.router.navigateByUrl(`/search-result?user=${searchTerm}`);
, I am thinking there is something wrong with how you navigate.
Edit 3
I am thinking queryParams can only be read when the component itself loads and not at the run time of the route resolvers because it is saying, I need to go to the route of search-result, give me the data before I go to search-result and it is independent of the queryParams. To fix this, I followed this guide (https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html).
1.) In app-routing-module.ts, change the registration of the path to:
{ path: 'search-result/:user', component: UserListComponent, resolve: { users: UserResolver } },
Now the user will be the parameter we are after in the URL.
2.) In search.component.ts, change search to:
search(searchTerm: string) {
if (searchTerm) {
this.router.navigate([`search-result/${searchTerm}`]);
}
}
3.) In user-resolver.service.ts, change it to this:
#Injectable({
providedIn: 'root'
})
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService) { }
resolve(route: ActivatedRouteSnapshot): Observable<User[]> {
const searchTerm: string = route.paramMap.get('user');
return this.userService.getUsers(searchTerm);
}
}
I when console logging searchTerm, it is the accurate value. Thanks for providing the StackBlitz, it helped you and me.

How to change the header on parent component according to route

Using Angular 7.x, I want to change my parent header component accordingly to the child routes and if they're activated or not. So in my case
AppComponent
Feature Module <= detect changes here
Child Components of feature module
So my routing is very simple, the app-routing.module just contains the loading of the feature module with loadChildren, nothing fancy here.
Then this feature module contains the routes for the child components. There is one parentComponent, we call it ParentComponent which contains the router-outlet. There is also some headers that I want to change accordingly to the childs.
SO i have two child components: create, and ':id' (detail page). When I trigger these routes I need the parent component to just change their text content accordingly. So for example when the create page is triggered I want to add the header: "Create new item", and for the :id page I want to show "Detail page".
Now, I have figured out I need to subscribe to the router events or on the activatedRoute (or snapshot). I'm at a loss here so I don't really know what to do here.
My parent component looks like this now:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute, Router } from '#angular/router';
#Component({
selector: 'parent-component',
templateUrl: './parent.component.html',
})
export class ParentComponent implements OnInit {
title = 'Parent Title';
// Check if it is possible to change the title according to the route
subtitle = 'Parent subtitle';
constructor(private readonly router: Router, private readonly route: ActivatedRoute) {}
ngOnInit(): void {
this.route.url.subscribe(data => {
if (this.route.snapshot.firstChild) {
console.log('yes', this.route.snapshot.firstChild);
// Change header with a if/else or switch case here
} else {
// Display standard text
console.log('no');
}
});
}
}
this is the output of the console.logs, notice in my real application the parent is named 'timesheets'.
Is there maybe another solution for this? Maybe a service, but all of the information is basically in the route already, so I'm trying to figure out if this is the way to go for my problem.
You can listen NavigationEnd events in ParentComponent or (I think) even better you can use a title service.
Solution 1:
In ParentComponent
import {NavigationEnd, Router} from '#angular/router';
import {filter} from 'rxjs/operators';
...
constructor(private router: Router, private readonly route: ActivatedRoute) {
this.subscribeRouterEvents();
}
subscribeRouterEvents = () => {
this.router.events.pipe(
filter(e => e instanceof NavigationEnd)
).subscribe(() => {
this.title = this.route.snapshot.data['title'];
// Assuming your route is like:
// {path: 'path', component: MyComponent, data: { title: 'Page Title'}}
});
Solution 2:
Using TitleService. Create a service that holds the page title, subscribe to title from ParentComponent and send new title to service from ChildComponent.
TitleService
#Injectable({
providedIn: 'root'
})
export class TitleService {
private defaultTitle = 'Page Title';
private titleSubject: BehaviorSubject<string> = new BehaviorSubject(this.defaultTitle);
public title: Observable<string>;
constructor(private titleService: Title) {
this.title = this.titleSubject.asObservable();
}
public setTitle(title: string) {
this.titleSubject.next(title);
}
}
ParentComponent
pageTitle = 'Page Title';
constructor(private titleService: TitleService) {}
ngOnInit(){
this.titleService.title.subscribe(value => this.pageTitle = value);
}
ChildComponent
pageTitle = 'Child Component Title';
constructor(private titleService: TitleService) {}
ngOnInit(){
this.titleService.setTitle(this.pageTitle);
}
You can try setting the title for a child as part of route like this.
const routes: Routes =[
{
path: 'create',
component: SomeComponent,
data : {header_title : 'some title'}
},
];
ngOnInit() {
this.title = this.route.data.subscribe(x => console.log(x));
}
and get the title in the child component and set that as header title using a service.

How do I grab a specific router value?

So when a post is clicked I do this which sends me to another page with the postId in the router:
this.router.navigate(['/annotation', postId]);
This navigates me to the annotations page where only that single post will be shown. In order for this to work, I need to get the postId which is now in the router link:
http://localhost:4200/annotation/5b3f83b86633e59b673b4a4f
How can I get that id: 5b3f83b86633e59b673b4a4f from the router and put it into my TS file. I want this id to only load posts with this ID.
Can anyone point me in the right direction to be able to grab the link http://localhost:4200/annotation/5b3f83b86633e59b673b4a4f take of everything and only get the ID at the end and store that in my TS file.
Sorry, I'm new to angular/web dev hence why I'm asking, many thanks in advance for your time.
You can read params of activated route via params observable, subscribe on it and you will get access to route params:
import { Component, OnInit, OnDestroy } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'selector',
template: ``,
})
export class LoanDetailsPage implements OnInit, OnDestroy {
private paramsSubscription$: Subscription;
constructor(private _route: ActivatedRoute) {}
ngOnInit() {
this.paramsSubscription$ = this._route.params.subscribe(params => {
console.log(params); // Full params object
console.log(params.get('paramName')); // The value of "paramName" parameter
});
}
ngOnDestroy() {
this.paramsSubscription$.unsubscribe();
}
}
PS: Don't forget to unsubscribe() in OnDestroy lifecycle hook.
You have to inject the ActivatedRoute service and subscribe to the paramMap:
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// subscribe to the parameters observable
this.route.paramMap.subscribe(params => {
this.foo = params.get('paramName');
});
}
Try in the component which you load something like:
id: string;
ngOnInit() {
this.id = this.route.snapshot.paramMap.get('postId');
}

Why is ngOnInit() being called before canActivate()?

I'm using route guards, specifically the canActivate() method, but Angular is calling ngOnInit() of my root AppComponent before canActivate is called.
I have to wait on some data in canActivate before the AppComponent can render it in the template.
How can I do this?
I was dealing with such cases, and here is what I usually do:
1. I create a Resolver service (which implements Resolve interface). It allows you to get all necessary data before activating the route:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '#angular/router';
import { DataService } from 'path/to/data.service';
#Injectable()
export class ExampleResolverService implements Resolve<any> {
constructor(private _dataService: DataService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<any> {
return this._dataService.anyAsyncCall()
.then(response => {
/* Let's imagine, that this method returns response with field "result", which can be equal to "true" or "false" */
/* "setResult" just stores passed argument to "DataService" class property */
this._dataService.setResult(response.result);
})
.catch(err => this._dataService.setResult(false););
}
}
2. Here is how we can deal with AuthGuard, which implements CanActivate interface:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '#angular/router';
import { DataService } from 'path/to/data.service';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private _dataService: DataService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
/* "getResult" method operates with the same class property as setResult, it just returns the value of it */
return this._dataService.getResult(); // will return "true" or "false"
}
}
3. Then you can include the Resolver and the AuthGuard to your routes config, here is just a part (the structure of routes can differ, here is an example with activating the parent component):
const routes: Routes = [
{
path: 'app',
component: AppComponent,
resolve: {
result: ExampleResolverService // your resolver
},
canActivate: [AuthGuard], // your AuthGuard with "canActivate" method
children: [...] // child routes goes inside the array
}
];
How it works
When you're navigating to /app, the ExampleResolverService starts, makes API call and stores the necessary part of response to class property in DataService via setResult method (it's the usual setter). Then, when the resolver finished the work, it's time for our AuthGuard. It gets stored result from DataService via getResult method (it's the usual getter), and returns this boolean result (our AuthGuard expects boolean to be returned, and the route will be activated if it returns true, and will not be activated if it returns false);
It's the simplest example without any additional operations with data, the logic is more complex usually, but this skeleton should be enough for basic understanding.
For me, I listened for ROUTE_NAVIGATED events in my app component like below
I am using ngrx/router-store to be able to listen to these router actions.
// app.component.ts
public ngOnInit(): void {
// grab the action stream
this.actions$.pipe(
// Only pay attention to completed router
ofType(ROUTER_NAVIGATED),
// Now I can guarantee that my resolve has completed, as the router has finsihed
// Switch
switchMap(() => {
// Now switch to get the stuff I was waiting for
return this.someService.getStuff();
})
// Need to subscribe to actions as we are in the component, not in an effect
// I suppose we should unsubscribe, but the app component will never destroy as far as I am aware so will always be listening
).subscribe();

Route Resolver not firing observable without subscribe

I have a route which needs some data from my Firebase db before the route is loaded. It feels like the Route is not calling subscribe so the request is never being fired off. Am I missing a step?
(Angular 5)
My router:
{
path: 'class/:idName',
component: ClassComponent,
resolve: {
classData: ClassResolver
}
},
My Resolver:
#Injectable()
export class ClassResolver implements Resolve<any> {
constructor(
private db: AngularFireDatabase
) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> | Promise<any> | any {
// return 'some data'; //This worked fine
return this.db
.list('/')
.valueChanges() // Returns Observable, I confirmed this.
//.subscribe(); // This returns a Subscriber object if I call it and I never get any data
}
// I tried this and it didnt work either
//const list = this.db
// .list('/')
// .valueChanges();
//console.log('list', list); // Is a Observable
//list.subscribe(data => {
// console.log('data', data); // returned data
// return data;
//});
//return list; // never gets to the component
}
My Component:
public idName: string;
// Other vars
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private db: AngularFireDatabase
) {
// Form stuff
}
ngOnInit() {
// Never makes it here
this.idName = this.route.snapshot.params.idName;
const myclass = this.route.snapshot.data.classData;
console.log('myclass', myclass);
}
I never makes it to the component. It waits for the component to load, which it never does. If I add the subscribe and console.out the data it returns quite quickly with the correct data, so its not the service.
After calling .subscribe() in my Resolver that now returns a Subscriber object. Because my return signature allows for any its returning this Subscriber as if it was the data. This seems obvious now.
My question now becomes why isn't it resolving my Observable?
Your resolve function is returning an Observable that never completes. The Observable is indeed firing (and this can be verified by adding a tap to its pipeline with some console-logging)—but the resolve phase won't end (and therefore your component won't load) until the Observable completes. (The docs are not great at highlighting this.)
Obviously you don't want your Observable to complete either, because then you wouldn't get further data updates.
The simplest “fix” is to wrap your Observable in a Promise:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
return this.db.list('/').valueChanges();
}
but this won't guarantee that Firebase has emitted its initial response, which I feel is what you're trying to ensure before the route loads.
The only approach I can see that would:
ensure that the component doesn't load until Firebase has returned data at least once; and
prevent two different Firebase reads (one by the resolver and then one by the component) for one effective operation
is to wrap your Firebase Observable in a service:
import { Injectable, OnDestroy, OnInit } from '#angular/core';
import { AngularFireDatabase } from '#angular/fire/database';
import { Subscription } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
#Injectable({
providedIn: 'root',
})
export class DataService implements OnInit, OnDestroy {
constructor(private readonly db: AngularFireDatabase) {}
/**
* Observable to the data.
* shareReplay so that multiple listeners don't trigger multiple reads.
*/
public readonly data$ = this.db
.list('/')
.valueChanges()
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
/**
* To trigger the first read as soon as the service is initialised,
* and to keep the subscription active for the life of the service
* (so that as components come and go, multiple reads aren't triggered).
*/
private subscription?: Subscription;
ngOnInit(): void {
this.subscription = this.data$.subscribe();
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
and then your resolver would look like this:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
// ensure at least one emission has occurred
await this.dataService.data$.pipe(take(1)).toPromise();
// ...then permit the route to load
return this.dataService.data$;
}
By wrapping your Firebase Observable in a service, you get OnInit and OnDestroy lifecycle hooks, which you can use to ensure that the observable "lives on" between component loads (and prevent multiple Firebase reads where one would suffice). Because the data is then hanging around, subsequent loads of the data would also be quicker. Lastly, this still enables you to use a resolver to ensure that the data will be instantly available before proceeding to load the component.
Your code looks to be correct. Have you been passing a parameter to your class route? It wont resolve without a parameter, that might be why you are not reaching your ngOnInit function. I would suggest console logging your route snapshots as well to make sure you are grabbing the right objects. I'll also post a resolve example that I got working:
Component.ts
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
public data: Observable<any>;
constructor(private router: ActivatedRoute) { }
ngOnInit() {
this.data = this.router.snapshot.data.test;
}
}
Routing.ts
{ path: 'home/:id', component: HomeComponent, resolve: { test: ResolverService } },
ResolverService
import { Injectable } from '#angular/core';
import { Resolve } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
#Injectable()
export class ResolverService implements Resolve<Observable<any>> {
constructor() { }
public resolve(route: ActivateRouteSnapShot): Observable<any> {
return Observable.of({test: 'Test Observable'});
}
}
HTML
{{this.data.test}}
You just need to add a take(1) operator to the Observable the resolver returns so that it completes.
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.db.list('/').valueChanges()
.pipe(take(1)); // <-- The Magic
}
#AlexPeters was on the right track, but you don't have to go so far as to return a promise. Just force the completion with take(1). Alex is also spot-on that the docs are not very clear on this. I just spent an couple hours debugging this same issue.

Categories