Subscribing to an observable before it's been populated - javascript

I'm trying to create a user profile service for an Angular 4 project and struggling a little with how to properly initialize and update the observable Profile object. Currently, when the user authenticates (via Firebase), AuthService passes the user's auth info to UserProfileService via the latter's initialize() function. UserProfileService then looks up the user's profile (or creates one if none exists yet) and populates a public observable with the profile.
The problem I'm running into is with other parts of the application trying to subscribe to the profile observable before all this has happened. I'd originally been initializing the observable via ...
public profileObservable: UserProfile = null;
... which of course resulted in a "subscribe() does not exist on null" error, so I changed it to ...
public profileObservable: Observable<UserProfile> = Observable.of();
This at least doesn't throw any errors, but anything that subscribes to profileObservable before I've mapped the Firebase object to it never updates.
Complete code for user-profile.service.ts below. I'm still struggling to get my head around how some of this is meant to work, so hopefully someone can shed some light. Thanks!
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { FirebaseListObservable, FirebaseObjectObservable, AngularFireDatabase } from 'angularfire2/database';
import * as firebase from 'firebase/app';
export class UserProfile {
$exists: Function;
display_name: string;
created_at: Date;
}
#Injectable()
export class UserProfileService {
private basePath: string = '/user-profiles';
private profileRef: FirebaseObjectObservable<UserProfile>;
public profileObservable: Observable<UserProfile> = Observable.of();
constructor(private db: AngularFireDatabase) {
// This subscription will never return anything
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
this.profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
const subscription = this.profileRef.subscribe(profile => {
if (!profile.$exists()) {
this.profileRef.update({
display_name: auth.displayName || auth.email,
created_at: new Date().toString(),
});
} else subscription.unsubscribe();
});
this.profileObservable = this.profileRef.map(profile => profile);
// This subscription will return the profile once it's retrieved (and any updates)
this.profileObservable.subscribe(profile => console.log(profile));
}
};

You must not change observable references once you constructed them. The way I found to properly decouple subscribers from the datasource is to use an intermediate Subject, which is both an observer and an observable.
Your code would look something like this:
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
...
export class UserProfileService {
...
public profileObservable = new Subject<UserProfile>();
constructor(private db: AngularFireDatabase) {
// This subscription now works
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
const profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
...
profileRef.subscribe(this.profileObservable);
}
};

Related

How to display a navbar component after logging in without reloading the page in angular 12

My landing page does not show a navbar, but I want to display a navbar after logging in successfully. Currently, I'm able to show navbar if I do a full page reload after logging in successfully. I'm very sure that there is a better way than this approach.
app.component.html
<app-navbar></app-navbar>
<router-outlet></router-outlet>
login.component.ts
login(){
this.credentials = this.myForm.value;
if(this.credentials){
this.loginService.authenticate(this.credentials)
.subscribe(data => {
this.storageService.setLocalStorageItem('auth', JSON.stringify(data));
this.dataService.global.showNav = true;
this.sharedService.getProjectMetadata()
.subscribe(metadata => {
this.storageService.setLocalStorageItem('projectMetaData', JSON.stringify(metadata));
this.router.navigate(['/home']);
})
}, err => console.log(err));
} else {
console.log('Please enter your username and password');
}
}
data.service.ts
import { Injectable } from '#angular/core';
import { Subject, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { IGlobal, IMessage } from '../../Shared/interfaces';
import { MessageCallback } from '../../Shared/types';
#Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
date: string = (new Date()).toString();
global: IGlobal = {
showNav: false,
sessionTimedOut: false,
timezone: this.date.substring(this.date.indexOf('GMT')),
projectMetaData: {
name: ''
},
isAdmin: false,
auth: {
roles: {
admin: false,
developer: false
}
}
}
private handler: Subject<IMessage> = new Subject<IMessage>();
broadcast(type: string, payload: any){
this.handler.next({type, payload});
}
subscribe(type: string, callback: MessageCallback): Subscription {
return this.handler.pipe(filter(message => message.type === type), map(message => message.payload))
.subscribe(callback);
}
}
navbar.component.html
<mat-toolbar fxLayout="row" color="primary" *ngIf='showNavbar'></mat-toolbar>
navbar.component.ts
export class NavbarComponent implements OnInit {
user: IAuth = {};
showNavbar: boolean;
progressbar: number = 0;
constructor(
private storageService: StorageService,
private dataService: DataService
) {
this.showNavbar = this.dataService.global.showNav;
}
ngOnInit(): void {
this.user = JSON.parse(this.storageService.getLocalStorageItem('auth'));
if(this.user){
this.showNavbar = true;
}
}
}
Please help me out. Your help is highly appreciated. Thank you.
The problem lies here,
once authentication is completed successfully in login() function, it is not communicated to navbar.component.ts
showNavbar in navbar.component.ts is used to display/hide navbar template.
Though dataService.global.showNav is set to true, it will not trigger change detection in navbar components. Since it is copied to `showNavbar' only in constructor.
So before login, navbar is already loaded, probably with showNavbar evaluated as false, and never recomputed until page reload.
During pagereload value is read from localStorage which provides latest value to showNavbar
I have two suggestions{S1,S2} to fix this.
S1.
1.broadcast via subject from login component about successful login status
2.And subscribe for that status in navbar component and upon subscription , control rendering of navbar template
3. Looks like as per your business logic,broadcast and subscribe functions in dataservice does that for you in IMessage type subject.
4. Consider example code below and update according to your application.
For eg:
login.component.ts
this.dataService.broadcast('authSuccess',{auth:'successful'})
in navbar.component.ts
OnInit() {
this.dataService.subscribe('authSuccess',setShowNavbar);
}
setShowNavbar() {
this.showNavbar=true;
}
S2:
This is not a clean approach and difficult for tracking, but it works for quick and dirty solutions.
navbar.component.html
<mat-toolbar fxLayout="row" color="primary" *ngIf="dataService.global.showNav"></mat-toolbar>
This case will run change detection whenever value in dataService.global.showNav is updated and evaluate ngIf accordingly.
Note: Better to add a small working proto in stackblitz/jsfiddle/codesandbox etc when posting questions in public forum. So it will be easier for everyone to identify exact problem and arrive on specific solutions quickly.

RxJS Subject never pushed the first next

Im having issues with subjects and a comment system Im building out. It works fine but the first comment that is posted never shows up until someone posts another comment, all other comments work fine after this. I have tried with BehaviourSubject giving an initial value of null or "" and ReplaySubject but I think I might be missing something with how these work, is there anyway to have this work in realtime i.e when a user pushes a comment it goes to the server and is added back onto the stack without having to submit a second comment right away ?
import { Injectable } from '#angular/core';
import { Observable } from "rxjs/Observable"
import { BehaviorSubject } from "rxjs/BehaviorSubject"
// import {Subject} from "rxjs/Subject"
import { ServerAPI } from '../serverapi';
import { ReplaySubject } from 'rxjs/internal/ReplaySubject';
import { Subject } from 'rxjs/Subject';
#Injectable({
providedIn: 'root'
})
export class Comments {
private _comments = new Subject<any[]>();
$comments: Observable<any> = this._comments.asObservable();
constructor(private api: ServerAPI) {
this.$comments.subscribe(() => {
console.log('comments sub\'d');
});
}
async comment(threadID: string, commentData: any): Promise<any> {
await this.api.postComment(threadID, commentData);
const fetchComments = await this.api.getComments(threadID);
this._comments.next(fetchComments);
}
}
In my template I received the comments via subscribe li
this.comment.$comments.subscribe(chats => {
this.CommentData = chats;
});
You need to have a function to subscribe on the subject like this
getData() {
return this._comments.asObservable();
}
So this is how you subscribe to getData() in your component
this.service.getData().subscribe(x=> {
console.log(x);
});
Also subscribe in constructor like you done right now will only execute one
constructor(private api: ServerAPI) {
this.$comments.subscribe(() => {
console.log('comments sub\'d');
});
}

Ionic & AngularFireDatabase list result - Property 'map' does not exist on type '{}'

I am trying to get data from Firebase by the list function and to use it in Json format (to load markers on my Google Map). But I'm facing an issue with the returned snapshots: Property 'map' does not exist on type '{}'
My imports:
import { Component, ViewChild, ElementRef } from '#angular/core';
import { NavController, IonicPage } from 'ionic-angular';
import { Geolocation } from '#ionic-native/geolocation';
import { AngularFireDatabase } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
And the code snippet with the error:
markers: Observable<any[]>;
constructor(public navCtrl: NavController,
public geolocation: Geolocation,
public db: AngularFireDatabase) {
this.markers = this.db.list("empresa/", {preserveSnapshot: true})
.map(snapshots => {
return snapshots.map(snapshot => { // this second .map displays the error
return snapshot.val();
})
.subscribe((marcas)=> {
console.log(marcas);
});
});
}
Because of this, I think, the subscribe does not display anything on the console. I also tried to import in some different ways the rxjs, as in this link Property 'map' does not exist on type 'Observable<Response>', but without success.
Seems like a migration issue - since AF 5.0 list returns an AngularFireList<T> service, not FirebaseListObservable, you have to call an action on it to stream data into an observable collection, e.g.:
const subscription = this.db.list("empresa/").snapshotChanges()
.map(snapshots => {
return snapshots.map(snapshot => snapshot.payload.val());
})
.subscribe(values => {
console.log(values);
this.markers = values;
});
Note also that the subscribe is chained after the outer map, not the inner as in your example; and this.markers are being assigned results inside the subscription, so you have to change their type from Observable<any[]> to any[] (or whatever they are, just not observable).
See 3. Retrieving data as lists in the docs for details; there's an example at the end of the page on using the listed items inside an angular template with async pipe, where you don't need to subscribe, then you'd keep this.markers as Observable<any[]> and the rest would be:
this.markers = this.db.list("empresa/").snapshotChanges()
.map(snapshots => {
return snapshots.map(snapshot => snapshot.payload.val());
});
This is preferred as the async pipe manages the subscription for you (because if you are subscribing manually you should also unsubscribe).

'Angular5' Service (rxjs/observable) two components rendering issue

I have a master list app with two component: usersList and usersMap both of which, are holding a private users variable, that gets its data from
the service.
Both are subscribed to a users-service that has a Users array with users data.
Every change on this array will cause both components to render and update their users variable.
Photo of the app on runtime
When I create a new user, the users array gets updated, and the usersList gets re-rendered,
but the usersMap isn`t.
I tried some solutions provided on stack, but they didn't work.
for example: Angular2 Observable BehaviorSubject service not working
A link to the github: https://github.com/TomerOmri/angular-users-map/tree/master/test-app/src/app
UserList component:
https://github.com/TomerOmri/angular-users-map/blob/master/test-app/src/app/userlist/userlist.component.ts
UserMap component: https://github.com/TomerOmri/angular-users-map/blob/master/test-app/src/app/usersmap/usersmap.component.ts
User Service:
import { Injectable } from '#angular/core';
import { Observable } from "rxjs/Observable";
import { of } from "rxjs/observable/of";
import { User } from "./user";
import { USERS } from "./users-mock";
#Injectable()
export class UserService {
constructor() { }
getUsers(): Observable<User[]> {
var userList = of(USERS);
return userList;
}
}
Use following code to do your task:
#Injectable()
export class UserService {
private userList = USERS;
public users = new BehaviourSubject<any>(userList);
constructor() { }
addUser(user): {
this.userList.push(user);
this.users.next(this.userList);
}
Now use this users property in your both component to get data. When you need to add just call addUser method of service.
Hope it will help.

Angular 2 - http.get never being call

Im learning Angular 4 and have run into a problem that I cannot seem to find a solution to. Here is the context:
I have a simple app that displays info about US Presidents.
The backend is a rest API provided by webapi...this works fine.
The front end is an Angular app.
Ive distilled the problem down to 3 components, 1 data service and 1 model.
Here is the model:
export class President {
constructor(
public id: number,
public presidentName: string,
public presidentNumber: number,
public yearsServed: string,
public partyAffiliation: string,
public spouse: string) {}
}
The 3 components are
1. SearchComponent
2. HomeComponent
3. PresidentComponent
When the app bootstraps, it loads the ApplicationComponent - it is the root component:
import { Component, ViewEncapsulation } from '#angular/core';
#Component({
selector: 'my-app',
template: `
<search-component></search-component>
<home-component></home-component>
`
})
export class ApplicationComponent {}
PresidentComponent is a child component of HomeComponent. When home component loads, it makes an http call to the api to get a list of presidents and renders 1 presidentComponent for each row returned. This works fine.
What Im trying to do is implement a search feature where the dataService exposes an EventEmitter and provides the search method as shown here:
import { Injectable, EventEmitter, Output } from '#angular/core'
import { President } from '../models/President'
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DataService {
constructor(private http: Http) {
}
searchEvent: EventEmitter<any> = new EventEmitter();
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any): Observable<President[]> {
let encParams = encodeParams(params);
console.log(encParams);
return this.http
.get(this.presidentUrl, {search: encParams})
.map(response => response.json());
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this.http.get(this.presidentUrl)
.map(response => response.json());
}
}
/**
* Encodes the object into a valid query string.
* this function is from the book Angular2 Development with TypeScript
*/
function encodeParams(params: any): URLSearchParams {
return Object.keys(params)
.filter(key => params[key])
.reduce((accum: URLSearchParams, key: string) => {
accum.append(key, params[key]);
return accum;
}, new URLSearchParams());
}
The Search Component houses the search form and when the search button is clicked, it executes the onSearch() function and calls emit on the data service:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.searchEvent.emit(this.formModel.value);
}
}
Then, in the HomeComponent, I want to subscribe to this event and execute a search via the dataservice when it fires:
ngOnInit(): void {
//when component loads, get list of presidents
this.dataService.getPresidents()
.subscribe(
presidents => {
console.log('sub');
this.presidents = presidents;
},
error => console.error(error)
)
//when search event is fired, do a search
this.dataService.searchEvent
.subscribe(
params => {
console.log('in home.ts subscribe ' + JSON.stringify(params));
this.result = this.dataService.search(params);
},
err => console.log("cant get presidents. error code: %s, URL: %s"),
() => console.log('done')
);
}
When I run this in the browser, everything works except the http call is never executed. If I subscribe() to the http.get call in the dataservice itself, it executes but why should I have to do that when I have a subscription being setup on the HomeComponent?
I want to handle the Observable in the HomeComponent and update the list of presidents that is being displayed in the UI based on the search result.
Any advice is greatly appreciated.
The entire idea of using EventEmitter in the service is not right. The EventEmitter should be used with #Output properties to send data from the child component to its parent.
Even though the EventEmitter is a subclass of the Subject, you shouldn't be using it in services. So inject the service into your component, subscribe to its observable in the component, and emit an event using EventEmitter to the parent component if need be.
In the code this.result = this.dataService.search(params);, result is an observable. You have not made a subscription.
In that case you should have used the async pipe to display the data.
Why not use Subject from rxjs. Here is what i am proposing:
DataService:
import { Observable, Subject } from "rxjs";
import 'rxjs/add/operator/catch';
#Injectable()
export class DataService {
private _dataSubject = new Subject();
constructor(private http: Http) {
this.http.get(this.presidentUrl)
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
);
}
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any){
let encParams = encodeParams(params);
console.log(encParams);
this.http
.get(this.presidentUrl, {search: encParams})
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this._dataSubject;
}
SearchComponent:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.search(this.formModel.value);
}
}
With these modifications you should be able to have only 1 subscriber in homeCompoent and then get new data emitted every time onSearch() is called.

Categories

Resources