There is a lot of documentation and examples of firestore collections getting realtime updates. However, there is very little for those who wish to have a single document have real time updates. I want to have a single document (an item), on a page where only the item will be viewed and manipulated and any changes to document, will have realtime updating.
Here is my component that wants to do stuff with the item:
import { Component, OnInit } from '#angular/core';
import { ItemsService } from '../shared/items.service';
import { ActivatedRoute, Router } from '#angular/router';
#Component({
selector: 'app-view-item',
templateUrl: './view-item.component.html',
styleUrls: ['./view-item.component.css']
})
export class ViewItem implements OnInit {
item;
private sub: any;
constructor(
// Service used for Firebase calls
private itemsService: ItemsService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
// Item retrieved from */item/:id url
this.sub = this.route.params.subscribe(params => {
this.getItem(params['id']);
});
}
getItem = (id) => {
this.itemsService.getItem(id).subscribe(res => {
console.log(res);
this.item = res;
console.log(this.item);
});
}
And the service it uses for calls:
import { Injectable } from '#angular/core';
import { AngularFirestore, AngularFirestoreDocument } from '#angular/fire/firestore';
#Injectable({
providedIn: 'root'
})
export class ItemsService {
constructor(
private firestore: AngularFirestore
)
getItem(id) {
return this.firestore.collection('items').doc(id).snapshotChanges();
}
}
The log I get for console.log(this.item) is undefined. Calling this.item in the console returns the same. I am unsure of how to proceed and would appreciate any guidance. Logging res in the console returns a byzantine object. Perhaps that's how I access the item, but if so, why is it not saved in this.item and how do I access the item's values?
snapshotChanges returns an observable of actions, not the actual value.
You should extract the value with action.payload.doc.data():
So your code should look like the following example.
getItem(id) {
return this.firestore.collection('items').doc(id).snapshotChanges()
.pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data();
const id = a.payload.doc.id;
return { id, ...data };
})
);
}
Or you can use valueChanges of doc.
getItem(id) {
return this.firestore.collection('items').doc(id).valueChanges();
}
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.
I want to navigate between two routes in Angular 7 with posting data between them. But I don;t want to show those parameter in URL. How to do it in proper way?
at this moment I am strugging with something like this:
this.router.navigate(['/my-new-route', {data1: 'test', test2: 2323, test: 'AAAAAAA'}]);
and it change my url to
http://localhost:4200/my-new-route;data1=test;test2=2323;test=AAAAAAA
how to do it to cancel those data from url:
http://localhost:4200/my-new-route
Edit:
My case:
/form - route with some form
/options - route with some data
on /form route - users have some form with empty fields to fill manually
but on /options page there is some preset configuration, when user choose one is navigated to /form and fields are fill autmatically
when they move back to another page and back again to /form - should see empty form. Only link from /options to /form should fill those fields.
You can create a service and share it between both the components (the one that you're moving from, and the one that you're moving to).
Declare all the parameters that you want to pass to the URL, in the service, and before the router.navigate([]), set the values for parameters in the service.
You can access those parameters from the other component with that service.
Example:
SharedService
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class SharedService {
data1;
test2;
test;
}
Component1
import { SharedService } from 'location';
import { Router } from '#angular/router';
...
constructor(private _sharedService: SharedService,
private _router: Router) { }
...
this._sharedService.data1 = 'test'
this._sharedService.test2 = 2323;
this._sharedService.test = 'AAAAAAAA';
this._router.navigate(['/my-new-route']);
...
Component2
import { SharedService } from 'location';
...
private test2;
private test;
private data1;
constructor(private _sharedService: SharedService){ }
ngOnInit() {
this.data1 = this._sharedService.data1;
this.test2 = this._sharedService.test2;
this.test = this._sharedService.test;
...
}
There are few ways to do it.
Try 1 :
this.router.navigate(['/some-url'], { queryParams: filter, skipLocationChange: true});
Try 2 :
We can use this work around instead by using EventEmitter and BehaviorSubject with a shared service
In component 1:
this.router.navigate(['url']).then(()=>
this.service.emmiter.emit(data)
)
In service :
emmiter : EventEmitter = new EventEmitter();
In component 2: inside constructor
this.service.emmiter.subscribe();
another solution for passing information from one route to another without touching the query params is via the state field of NavigationExtras (as of Angular 7.2+)
something along these lines
// Publish
<a
[routerLink]="['/studies', study.id]"
[state]="{ highlight: true }">
{{study.title}}
</a>
// Subscribe
constructor(private route: ActivatedRoute, ...) {
}
public highlight: boolean;
public ngOnInit() {
...
this.route.paramMap
.pipe(map(() => window.history.state))
.subscribe(state => {
this.highlight = state && state.highlight;
});
...
}
// Alternative
constructor(private router: Router, ...) {
}
public highlight: boolean;
public ngOnInit() {
...
this.router.events.pipe(
filter(e => e instanceof NavigationStart),
map(() => this.router.getCurrentNavigation().extras.state)
)
.subscribe(state => {
this.highlight = state && state.highlight;
})
...
}
pass value through "state" key from which you want to naviagte to next component:
//From where we Navigate
import {ActivatedRoute, NavigationExtras, Router} from "#angular/router";
export class MainPageComponent {
constructor(public router:Router) {}
navWithExtraValue () {
const navigationExtras: NavigationExtras = {
state: {
editMode: true
},
};
}
}
//In constructor where we Navigated
constructor(public router:Router,
public route:ActivatedRoute){
this.route.queryParams.subscribe(data=> {
if (this.router.getCurrentNavigation().extras.state) {
this.editMode = this.router.getCurrentNavigation().extras.state.editMode;
}
});
We don't see these value in url
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');
}
How do I get the RouteParams from a parent component?
App.ts:
#Component({
...
})
#RouteConfig([
{path: '/', component: HomeComponent, as: 'Home'},
{path: '/:username/...', component: ParentComponent, as: 'Parent'}
])
export class HomeComponent {
...
}
And then, in the ParentComponent, I can easily get my username param and set the child routes.
Parent.ts:
#Component({
...
})
#RouteConfig([
{ path: '/child-1', component: ChildOneComponent, as: 'ChildOne' },
{ path: '/child-2', component: ChildTwoComponent, as: 'ChildTwo' }
])
export class ParentComponent {
public username: string;
constructor(
public params: RouteParams
) {
this.username = params.get('username');
}
...
}
But then, how can I get this same 'username' parameter in those child components? Doing the same trick as above, doesn't do it. Because those params are defined at the ProfileComponent or something??
#Component({
...
})
export class ChildOneComponent {
public username: string;
constructor(
public params: RouteParams
) {
this.username = params.get('username');
// returns null
}
...
}
UPDATE:
Now that Angular2 final was officially released, the correct way to do this is the following:
export class ChildComponent {
private sub: any;
private parentRouteId: number;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.sub = this.route.parent.params.subscribe(params => {
this.parentRouteId = +params["id"];
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
ORIGINAL:
Here is how i did it using the "#angular/router": "3.0.0-alpha.6" package:
export class ChildComponent {
private sub: any;
private parentRouteId: number;
constructor(
private router: Router,
private route: ActivatedRoute) {
}
ngOnInit() {
this.sub = this.router.routerState.parent(this.route).params.subscribe(params => {
this.parentRouteId = +params["id"];
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
In this example the route has the following format: /parent/:id/child/:childid
export const routes: RouterConfig = [
{
path: '/parent/:id',
component: ParentComponent,
children: [
{ path: '/child/:childid', component: ChildComponent }]
}
];
You shouldn't try to use RouteParams in your ChildOneComponent.
Use RouteRegistry, instead!
#Component({
...
})
export class ChildOneComponent {
public username: string;
constructor(registry: RouteRegistry, location: Location) {
route_registry.recognize(location.path(), []).then((instruction) => {
console.log(instruction.component.params['username']);
})
}
...
}
UPDATE: As from this pull request (angular beta.9): https://github.com/angular/angular/pull/7163
You can now access to the current instruction without recognize(location.path(), []).
Example:
#Component({
...
})
export class ChildOneComponent {
public username: string;
constructor(_router: Router) {
let instruction = _router.currentInstruction();
this.username = instruction.component.params['username'];
}
...
}
I haven't tried it, yet
Further details here:
https://github.com/angular/angular/blob/master/CHANGELOG.md#200-beta9-2016-03-09
https://angular.io/docs/ts/latest/api/router/Router-class.html
UPDATE 2:
A small change as from angular 2.0.0.beta15:
Now currentInstruction is not a function anymore. Moreover, you have to load the root router. (thanks to #Lxrd-AJ for reporting)
#Component({
...
})
export class ChildOneComponent {
public username: string;
constructor(_router: Router) {
let instruction = _router.root.currentInstruction;
this.username = instruction.component.params['username'];
}
...
}
As mentioned by Günter Zöchbauer, I used the comment at https://github.com/angular/angular/issues/6204#issuecomment-173273143 to address my problem. I used the Injector class from angular2/core to fetch the routeparams of the parent. Turns out angular 2 does not handle deeply nested routes. Maybe they'll add that in the future.
constructor(private _issueService: IssueService,
private _injector: Injector) {}
getIssues() {
let id = this._injector.parent.parent.get(RouteParams).get('id');
this._issueService.getIssues(id).then(issues => this.issues = issues);
}
I found an ugly but working solution, by requesting the parent (precisely the 2nd ancestor) injector, and by getting the RouteParams from here.
Something like
#Component({
...
})
export class ChildOneComponent {
public username: string;
constructor(injector: Injector) {
let params = injector.parent.parent.get(RouteParams);
this.username = params.get('username');
}
}
RC5 + #angular/router": "3.0.0-rc.1 SOLUTION: It seems that this.router.routerState.queryParams has been deprecated. You can get the parent route params this way:
constructor(private activatedRoute: ActivatedRoute) {
}
this.activatedRoute.parent.params.subscribe(
(param: any) => {
let userId = param['userId'];
console.log(userId);
});
You can take component of parent route inside of child component from injector and then get any from child component. In you case like this
#Component({
...
})
export class ChildOneComponent {
public username: string;
constructor(
public params: RouteParams
private _injector: Injector
) {
var parentComponent = this._injector.get(ParentComponent)
this.username = parentComponent.username;
//or
this.username = parentComponent.params.get('username');
}
...
}
Passing Injector instance to constructor in child component may not be good if you want to write unit tests for your code.
The easiest way to work around this is to have a service class in the parent component, in which you save your required params.
#Component({
template: `<div><router-outlet></router-outlet></div>`,
directives: [RouterOutlet],
providers: [SomeServiceClass]
})
#RouteConfig([
{path: "/", name: "IssueList", component: IssueListComponent, useAsDefault: true}
])
class IssueMountComponent {
constructor(routeParams: RouteParams, someService: SomeServiceClass) {
someService.id = routeParams.get('id');
}
}
Then you just inject the same service to child components and access the params.
#Component({
template: `some template here`
})
class IssueListComponent implements OnInit {
issues: Issue[];
constructor(private someService: SomeServiceClass) {}
getIssues() {
let id = this.someService.id;
// do your magic here
}
ngOnInit() {
this.getIssues();
}
}
Note that you should scope such service to your parent component and its child components using "providers" in parent component decorator.
I recommend this article about DI and scopes in Angular 2: http://blog.thoughtram.io/angular/2015/08/20/host-and-visibility-in-angular-2-dependency-injection.html
In RC6, router 3.0.0-rc.2 (probably works in RC5 as well), you can take route params from the URL as a snapshot in case that params won't change, without observables with this one liner:
this.route.snapshot.parent.params['username'];
Don't forget to inject ActivatedRoute as follows:
constructor(private route: ActivatedRoute) {};
With RxJS's Observable.combineLatest, we can get something close to the idiomatic params handling:
import 'rxjs/add/operator/combineLatest';
import {Component} from '#angular/core';
import {ActivatedRoute, Params} from '#angular/router';
import {Observable} from 'rxjs/Observable';
#Component({ /* ... */ })
export class SomeChildComponent {
email: string;
id: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
Observable.combineLatest(this.route.params, this.route.parent.params)
.forEach((params: Params[]) => {
this.id = params[0]['id'];
this.email = params[1]['email'];
});
}
}
I ended up writing this kind of hack for Angular 2 rc.1
import { Router } from '#angular/router-deprecated';
import * as _ from 'lodash';
interface ParameterObject {
[key: string]: any[];
};
/**
* Traverse route.parent links until root router and check each level
* currentInstruction and group parameters to single object.
*
* e.g.
* {
* id: [314, 593],
* otherParam: [9]
* }
*/
export default function mergeRouteParams(router: Router): ParameterObject {
let mergedParameters: ParameterObject = {};
while (router) {
let currentInstruction = router.currentInstruction;
if (currentInstruction) {
let currentParams = currentInstruction.component.params;
_.each(currentParams, (value, key) => {
let valuesForKey = mergedParameters[key] || [];
valuesForKey.unshift(value);
mergedParameters[key] = valuesForKey;
});
}
router = router.parent;
}
return mergedParameters;
}
Now in view I collect parameters in view instead of reading RouteParams I just get them through router:
#Component({
...
})
export class ChildishComponent {
constructor(router: Router) {
let allParams = mergeRouteParams(router);
let parentRouteId = allParams['id'][0];
let childRouteId = allParams['id'][1];
let otherRandomParam = allParams.otherRandomParam[0];
}
...
}
In FINAL with little help of RXJS you can combine both maps (from child and parent):
(route) => Observable
.zip(route.params, route.parent.params)
.map(data => Object.assign({}, data[0], data[1]))
Other questions one might have:
Is it really a good idea to use above - because of coupling (couple child component with parent's param's - not on api level - hidden coupling),
Is it proper approach in term of RXJS (it would require hardcore RXJS user feedback ;)
You can do it on the snapshot with the following, but if it changes, your id property will not be updated.
This example also shows how you can subscribe to all ancestor parameter changes and look for the one you are interested in by merging all of the parameter observables. However, be careful with this method because there could be multiple ancestors that have the same parameter key/name.
import { Component } from '#angular/core';
import { ActivatedRoute, Params, ActivatedRouteSnapshot } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/merge';
// This traverses the route, following ancestors, looking for the parameter.
function getParam(route: ActivatedRouteSnapshot, key: string): any {
if (route != null) {
let param = route.params[key];
if (param === undefined) {
return getParam(route.parent, key);
} else {
return param;
}
} else {
return undefined;
}
}
#Component({ /* ... */ })
export class SomeChildComponent {
id: string;
private _parameterSubscription: Subscription;
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
// There is no need to do this if you subscribe to parameter changes like below.
this.id = getParam(this.route.snapshot, 'id');
let paramObservables: Observable<Params>[] =
this.route.pathFromRoot.map(route => route.params);
this._parametersSubscription =
Observable.merge(...paramObservables).subscribe((params: Params) => {
if ('id' in params) {
// If there are ancestor routes that have used
// the same parameter name, they will conflict!
this.id = params['id'];
}
});
}
ngOnDestroy() {
this._parameterSubscription.unsubscribe();
}
}
Getting RouteParams from parent component in Angular 8 -
I have a route http://localhost:4200/partner/student-profile/1234/info
Parent route - student-profile
Param - 1234 (student_id)
Child route - info
Accessing param in child route (info) -
Import
import { ActivatedRoute, Router, ParamMap } from '#angular/router';
Constructor
constructor(private activatedRoute: ActivatedRoute, private router: Router) { }
Accessing parent route params
this.activatedRoute.parent.paramMap.subscribe((params: ParamMap) => this.studentId = (params.get('student_id')));
Now, our variable studentId has the param value.