I have a service where i emit a value to a replaysubject observable as below. MyService.update() is called from app initializer.
Service:
#Injectable({ providedIn: 'root' })
export class MyService {
private _userSubject$ = new ReplaySubject<UserDetails>(1);
user$: Observable<UserDetails> = this._userSubject$;
test$ = new BehaviorSubject<string>('test');
constructor() {
this.user$.subscribe((x) => console.log('it subscribes here'))
}
update(details: SessionDetailsResponse): void {
this._userSubject$.next(details.userDetails);
this.test$.next('updated');
}
}
Component:
ngOnInit() {
this.myService.user$
.pipe(takeUntil(this.unSubscribe$))
.subscribe((u) => {
console.log('u', u); //not coming here
this.currentUserId = u.id; //this is not subscribing here.
});
this.sessionDetails.test$
.subscribe(x => {
console.log('test', x); //this one subsribig with only default value and not the updated value.
})
}
Not sure why it is not subscribing.Can anyone tell me what am i doing wrong here?
Thanks
It was my mistake. The service has already been decorated as ProvidedIn: Root as singleton service. In addition to this i have added to providers array of the module. Due to this the constructor is called twice and the values are lost.
Related
I have implemented FireDatabaseService as #Injectable({ providedIn: 'root' }) in my app. It of course works with DI. In this service I need Firebase UID from another service, but this UID is being fetched with subscription. Because of this methods that need UID are called before this UID is available. I have no idea how to get UID before getUserGroup() is called. Here is my service:
...
#Injectable({ providedIn: 'root' })
export class FireDatabaseService {
uid: string;
constructor(
private readonly db: AngularFireDatabase,
private readonly authService: AuthService
) {
this.authService.authState.subscribe(
(state: firebase.User) => this.uid = state.uid
);
}
getUserGroup(): Observable<string> {
return this.db
.object(`users/${this.uid}`)
.valueChanges()
.pipe(map((userData: any) => userData?.group as string));
...
}
Calling fireDatabaseService.getUserGroup() goes with uid = ''. Is there any possibility to fix it? Thanks for help.
It is a little unclear to me what your requirements are. The easiest way to accomplish what I think you are asking would be the following:
...
#Injectable({ providedIn: 'root' })
export class FireDatabaseService {
constructor(
private readonly db: AngularFireDatabase,
private readonly authService: AuthService
) {
}
getUserGroup(): Observable<string> {
return this.authService
.authState
.pipe(
switchMap((state: firebase.User) =>
this.db
.object(`users/${state.uid}`)
.valueChanges()
),
map((userData: any) => userData?.group as string)
);
...
}
Everytime getUserGroup is called, there would be another call to the authService. That might be undesireable, but that could be mitigated through the use of shareReplay(1) or through another strategy depending on the functionality you are expecting.
I suggest that you do not subscribe at all in your service, but rather define your uid and userGroup as observables. The userGroup$ can be defined from the uid$ observable:
#Injectable({ providedIn: 'root' })
export class FireDatabaseService {
uid$ = this.authService.authState.pipe(
map(state => state.uid)
);
userGroup$ = this.uid$.pipe(
switchMap(uid => this.db.object(`users/${uid}`).valueChanges())
map(userData => userData?.group as string)
);
constructor(
private readonly db: AngularFireDatabase,
private readonly authService: AuthService
) { }
}
Observables are lazy, so it is not necessary to wrap them as functions. They don't emit a value until .subscribe() is called on them.
I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.
Tested with Android sdk emulator and on real device.
AppComponent
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';
import { AuthenticationService } from './authentication.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
public user$: Observable<any>;
private subscriptions: Subscription[] = [];
constructor(private appService: AppService, private authenticationService: AuthenticationService) {}
public ngOnInit(): void {
this.user$ = this.authenticationService.user$;
this.subscriptions.push(
this.authenticationService.user$.subscribe((user: any) => {
console.log('user', !!user);
})
);
}
public ngOnDestroy(): void {
if (this.subscriptions) {
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
async signIn() {
await this.appService.signIn();
}
async signOut() {
await this.appService.signOut();
}
}
AuthenticationService
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
public user$: Observable<any>;
constructor(private appService: AppService) {
this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
}
}
AppService
import { Injectable, NgZone } from '#angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const user$ = new BehaviorSubject<User>(null);
#Injectable({
providedIn: 'root',
})
export class AppService {
constructor(private ngZone: NgZone) {
addAuthStateListener({
onAuthStateChanged: ({ user }) => {
this.ngZone.run(() => {
user$.next(user);
});
},
});
}
public authState(): Observable<User> {
return user$.asObservable().pipe(distinctUntilChanged());
}
async signIn() {
return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
(error: string) => {
throw {
message: error,
};
}
);
}
signOut() {
logout();
}
}
ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.
If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.
If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.
Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.
const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));
timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions
Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:
subscription.add(() => console.log(`everybody's done.`));
One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)
<page-router-outlet
(loaded)="outletLoaded($event)"
(unloaded)="outletUnLoaded($event)"></page-router-outlet>
Then you cn use outletLoaded(args: EventData) {} to initialise your code
respectively outletUnLoaded to destroy your subscriptions.
This is helpful in cases where you have access to the router outlet (in App Component for instance)
In case when you are somewhere inside the navigation tree you can listen for suspend event
Application.on(Application.suspendEvent, (data: EventData) => {
this.backFromBackground = true;
});
Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.
It works pretty well for me.
Hope that help you as well.
I am making a request using observable. and trying to subcribe the value. But getting error on typescript. any on help me?
I like to do this:
public getCountry(lat,lan):Observable<any>{
return this.http.get(this.googleApi+lat+','+lan+'&sensor=false').subscribe(data => {
return this.genertedData(data);
} );
}
But getting error as follows:
UPDATES
public getCountry(lat,lan):Observable<any>{
return this.http.get(this.googleApi+lat+','+lan+'&sensor=false').map( data => {
data.results.map( array => {
let details = array.address_components.find( obj => obj.types.includes("country") );
this.countryDetails.countryLongName = details.long_name;
this.countryDetails.countryShortName = details.short_name;
})
return this.countryDetails;
})
}
The problem is that your return type states Observable<any>, where as you actually return whatever this.genertedData(data) returns (Hint: Sounds like a typo in your function. Guess it should be called generatedData ?).
Best practice would be to move the http call into a service and subscribe to its returned Observable within your component.
So to speak:
// => service.ts
public getCountryObservable(lat,lan):Observable<any> {
return this.http.get(this.googleApi+lat+','+lan+'&sensor=false');
}
Your component would look something like:
// => component.ts
export class YourComponent {
constructor(public yourService: YourService) {}
getCountry(lat, lan): whateverDataTypeYourGeneratedDataReturns {
this.yourService.getCountryObservable(lat, lan).subscribe((data) => {
this.data = this.generatedData(data);
});
}
}
Since the return type of the function is Observable<any>, I guess it should just return this.http.get(this.googleApi+lat+','+lan+'&sensor=false')
I often find myself using the following code:
export class Component implements OnDestroy {
private subscription: Subscription;
user: string;
constructor(private store: UserStore) {
this.subscription = store.select(fromUsers.getUser)
.subscribe(user => this.user = user);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
logout(): void {
this.store.dispatch({
type: LOGOUT,
payload: {
user: this.user
}
})
}
}
As you can see I need to store the user string as a member within the component to send it with my payload.
I would rather use the user string as an observable and make use of the async pipe.
How do I need to change my code to leverage the observable of the user when dispatching the action without storing it in a member variable?
You can use ngrx effects and enhance the LOGOUT command with current user.
#Effect() logoutEffect$ = this.actions$
.ofType(LOGOUT)
.withLatestFrom(this.store$)
.map(([action: Action, storeState: AppState]) => {
return storeState.getUser;
})
.map(payload => ({type: 'LOGOUT_USER', payload}))
Since couple evening I've played with form validation in augular2.
All basic cases were easy to implement and they works fine but I stick with asynchronous validation. I have created a very tiny example http://plnkr.co/edit/Xo8xwJjhlHkXrunzS8ZE and it didn't work.
According to test "should fire an event after the status has been updated to pending" from model_spec.ts Registration via creation of control group suppose to work in a way
builder.group({login: ["",Validators.required,validationFuctionWhichReturnsPromise]
I spent a full evening to discovered that this code has been released in alfa-46 (and I used alfa-45) and after update depencies the async validation started to work. The feature is very fresh and is not fully documented but
(for those who haven't tried it yet) Basically async validator is a function which have a Control argument and return a promise which validation result. There are two ways to register a validator. 1) the one which I used in my example and 2) as a directive which Provide validators via NG_ASYNC_VALIDATORS (See UniqLoginValidator and NgFormControl to see how it work). You can compose more than one validator (not tested yet but functions to do this are in code, see https://github.com/angular/angular/commit/cf449dd).
But when I finally reach to up and running validators a new problem arrived. Async validator is perfect to used it in server side validation. But the validation is invoked after each change of model.fe after each keyup. So if we will send request to a server after each key up, it won't be too efficient way ;) I checked how it is done in angular 1 and they is a possibility to debounce validation events.
My questions are:
How to implement throttle or debounce with async validators? I saw some ideas but none of them were fine (mostly because they need to change angular code itself). Is there any valid way to do this without waiting for new angular release ?
I was thinking about to warping a validator function with debounce (from underscorejs) but it will not work because angular expects to get a valid promise every time.
My second though was that if all event use RxJs under the hood then maybe I can apply debounce on stream of event which is responsible for validation. In model.ts the promise returned from validator is change to observable and a new subscribed is added. We don't have any access to obs(Observable) to apply debounce there.
Is there any way or id to change,easy extend a control over the form validation ?
I spotted a close related problem in How to trigger Form Validators in angular2
PS there is other issue related to async validators and it is still open https://github.com/angular/angular/issues/1068
Here is a helper class that you can use to debounce all your async validators:
import {Component} from 'angular2/core';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import {Control} from 'angular2/common';
export class AsyncValidator {
_validate;
constructor(validator: (control: Control) => any, debounceTime = 1000) {
let source: any = new Observable((observer: Observer<Control>) => {
this._validate = (control) => observer.next(control);
});
source.debounceTime(debounceTime)
.distinctUntilChanged(null, (x) => x.control.value)
.map(x => { return { promise: validator(x.control), resolver: x.promiseResolver }; })
.subscribe(
(x) => x.promise.then(resultValue => x.resolver(resultValue),
(e) => { console.log('async validator error: %s', e); }));
}
private _getValidator() {
return (control) => {
let promiseResolver;
let p = new Promise((resolve) => {
promiseResolver = resolve;
});
this._validate({ control: control, promiseResolver: promiseResolver });
return p;
};
}
static debounce(validator: (control: Control) => any, debounceTime = 400) {
var asyncValidator = new this(validator, debounceTime);
return asyncValidator._getValidator();
}
}
Then all you have to do where use async validators is just wrap your validator with this call and write your validator the same as you would normally:
AsyncValidator.debounce(control => this.asyncValidator(control));
Here is an example usage:
export class AppComponent {
form: ControlGroup;
constructor(private _formBuilder: FormBuilder) {
var validator = AsyncValidator.debounce(control => this.asyncValidator(control));
this.form = _formBuilder.group({
name: ['', Validators.required, validator],
});
}
asyncValidator(control): any {
let p = new Promise(resolve => {
// get from server information need to validate control
if (control.value === 'valid value') {
resolve(null);
} else {
resolve({
asyncValidator: {
valid: false
}
});
}
});
return p;
}
}
There is an awesome issue on angular site that deals with the problem of both debouncing and switchMapping the validation:
https://github.com/angular/angular/issues/6895
This is mine working solution (but all the credit goes to guys from thread)
class AsyncValidator{
private validatorInput: Subject<string>;
private validatorChain: Observable<any>;
constructor(service: ManageUsersService) {
this.validatorInput = new Subject();
this.validatorChain = this.validatorInput
.debounceTime(400)
.distinctUntilChanged()
.switchMap(value => service.findUsersByName(value)
.map(() => ({error: 'Error'})) //example of failed validation
.catch(() => Observable.of(null))) //example of successful validation
.do(v => console.log('mapped', v))
.share()
.take(1);
}
validate = (control: AbstractControl) => {
// An immediate timeout is set because the next has to occur after the
// validator chain is subscribed to.
setTimeout(() => this.validatorInput.next(control.value), 0);
return this.validatorChain;
}
You use it like this:
this.createUserForm = fb.group({
login: [ null,
Validators.required,
new AsyncValidator(userService).validate
],
});
}