I am using ngrx/data and what I need to do is set a value in the store, let's call this ID. And then when I make any request to an entity to pull that ID from the store. I will use update as an example.
Here is an example of a Client Entity Service. I can easily map the returned data as super.update returns an observable.
import { Injectable } from '#angular/core';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '#ngrx/data';
import { Client } from '../../store/client/client.model';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class ClientEntityService extends EntityCollectionServiceBase<Client> {
constructor(
serviceElementsFactory: EntityCollectionServiceElementsFactory,
) {
super('Client', serviceElementsFactory);
}
public update(entity: Partial<Client>): Observable<Client> {
return super.update(entity);
}
}
However I want to use a store value to make the update. So focusing on the update I can do this:
public update(entity: Partial<Client>): Observable<Client> {
this.store.pipe(
tap((store) => {
console.log(store);
})
).subscribe();
return super.update(entity);
}
Which prints out the store and I can see the value I need, so I could do this
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1)
).subscribe((id) => {
return super.update({
id,
...update
});
});
}
However it is requiring me to subscribe to the observable to be able to trigger it. That would mean the caller could not pipe the results and is generally not ideal.
I am wondering if anyone knows of a good solution to be able to get the data from the store but not have to subscribe like I am doing above to get the data, ideally I would want to use a switchMap like this:
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
switchMap((id) => {
return super.update({
id,
...update
});
}),
take(1)
)
Thanks
You wrote it correctly in your ideal solution. The difference is that you just need to move take(1) to be right after the select.
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1),
switchMap((id) => {
return super.update({
id,
...update
});
}),
)
so store won't cause update requests on every change.
After stepping into the super.update call I could see that the dispatcher was calling:
update(entity, options) {
// update entity might be a partial of T but must at least have its key.
// pass the Update<T> structure as the payload
/** #type {?} */
const update = this.toUpdate(entity);
options = this.setSaveEntityActionOptions(options, this.defaultDispatcherOptions.optimisticUpdate);
/** #type {?} */
const action = this.createEntityAction(EntityOp.SAVE_UPDATE_ONE, update, options);
if (options.isOptimistic) {
this.guard.mustBeUpdate(action);
}
this.dispatch(action);
return this.getResponseData$(options.correlationId).pipe(
// Use the update entity data id to get the entity from the collection
// as might be different from the entity returned from the server
// because the id changed or there are unsaved changes.
map((/**
* #param {?} updateData
* #return {?}
*/
updateData => updateData.changes)), withLatestFrom(this.entityCollection$), map((/**
* #param {?} __0
* #return {?}
*/
([e, collection]) => (/** #type {?} */ (collection.entities[this.selectId((/** #type {?} */ (e)))])))), shareReplay(1));
}
Which effectively just dispatches some actions and then creates a selected observable from this.getResponseData$ using the correlationid etc.
In my use case because I am using the store to get the Id of the current client I don't need the updated client returned as I already have an observable.
On top of the ClientEntityService I have another facade which I am calling ClientService.
Which looks like this:
#Injectable({
providedIn: 'root'
})
export class ClientService {
constructor(
private clientEntityService: ClientEntityService,
private store: Store<AppState>
) {}
public getCurrentClient(): Observable<Client> {
return this.clientEntityService.entityMap$.pipe(
withLatestFrom(this.store.pipe(select(getCurrentId))),
map(([clients, currentId]) => clients[currentId])
);
}
public updateCurrentClient(update: Partial<Client>): Subscription {
return this.getCurrentClient().pipe(
take(1),
switchMap((client) => {
return this.clientEntityService.update({
id: client.id,
...update
});
})
).subscribe();
}
}
So now from within my component I have the constructor
constructor(
private clientService: ClientService,
) {
this.client$ = this.clientService.getCurrentClient();
}
And then on update I call:
this.clientService.updateCurrentClient(theUpdate);
And because I already have this.client$ as an observable of that client being updated I don't need updateCurrentClient to return Observable<Client>. So as per above I am just returning Subscription
I could modify updateCurrentClient to implement something similar to what the DefaultDataService returns, but I'd expect that could be subject to change in future versions. So for now. I am happy with this solution.
Related
I've made a facade service to avoid multiple calls to the API.
It call retrieveMyUser each time the request is made.
If the request has never been made it store the value usingBehaviorSubject. If it has already been made it take the value stored.
I want to clear the data of my BehaviorSubject in auth.service.ts when a user logout. My try to do that is that I call a clearUser() method from facade-service.ts.
facade-service.ts :
...
export class UserServiceFacade extends UserService {
public readonly user = new BehaviorSubject(null);
retrieveMyUser() {
console.log(this.user.value);
return this.user.pipe(
startWith(this.user.value),
switchMap(user => (user ? of(user) : this.getUserFromServer())),
take(1)
)
}
private getUserFromServer() {
return super.retrieveMyUser(null, environment.liveMode).pipe(tap(user => this.storeUser(user)));
}
public clearUser() {
console.log("cleared");
this.storeUser(null)
console.log(this.user.value); // Output null
}
private storeUser(user: V2UserOutput) {
this.user.next(user);
}
}
auth.service.ts :
...
logout() {
var cognitoUser = this.userPool.getCurrentUser();
if (cognitoUser) {
this.userServiceFacade.clearUser()
cognitoUser.signOut();
}
this._router.navigate(['/login']);
}
...
The method clearUser() in auth.service.ts is well called and print cleared correctly.
But when I login, after I logout the console.log(this.user.value); in retrieveMyUser still output the previous value. It was null when at logout though.
So, how do I clear BehaviorSubject cache or to reset BehaviorSubject from another service ?
There are many things in your code which sound weird at reading:
You shouldn't access immediately to the value of a BehaviorSubject without using the asObservable() as recommended by ESLint here.
Instead, you could use another variable which will keep the latest value for the user.
You should use the power of TypeScript in order to help you with types definition and quality code (in my opinion).
The use of a BehaviorSubject with a startWith operator can be simplified using a ReplaySubject with a bufferSize of 1 (replay the latest change)
Your subject acting like a source storage should be private in order to limit the accessibility from outside.
I took your code and make some updates from what I said above:
export class UserServiceFacade extends UserService {
private _user: V2UserOutput;
private readonly _userSource = new ReplaySubject<V2UserOutput>(1);
public get user(): V2UserOutput { // Use for accessing to the user data without the use of an observable.
return this._user;
}
constructor() {
super();
this.clearUser(); // It will make your ReplaySubject as "alive".
}
public retrieveMyUser$(): Observable<V2UserOutput> {
return this._userSource.asObservable()
.pipe(
switchMap(user => (user ? of(user) : this.getUserFromServer())),
take(1)
);
}
private getUserFromServer(): Observable<V2UserOutput> {
return super.retrieveMyUser(null, 'environment.liveMode')
.pipe(
tap(user => this.storeUser(user))
);
}
public clearUser() {
console.log('cleared');
this.storeUser(null);
}
private storeUser(user: V2UserOutput) {
this._user = user;
this._userSource.next(user);
}
}
Cheers!
So, I probably don't understand Observables well. I have snippet like this, and would like to access todos stored in this service via function (from another component) defined in service. Unfortunately, I have no idea how to do this.
todos;
// fetching todos from api
fetchTodos() :Observable<Todo[]>{
return this.http.get<Todo[]>(api_url);
}
constructor(private http:HttpClient) {
this.fetchTodos()
.subscribe(data => this.todos = data)
}
To do it right solve your problem as follows.
SERVICE
import { BehaviorSubject, Observable } from 'rxjs';
/* make the plain object a subject you can subscribe to */
todos: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
constructor(private http:HttpClient) {
/* get the data and put it in the subject */
/* every subscriber to this event will be updated */
this.fetchTodos().subscribe(data => this.todos.next(data));
}
getTodos(): Observable<Todo[]> {
return this.todos.asObservable();
}
// fetching todos from api
private fetchTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(api_url);
}
COMPONENT
constructor(private service: Service) {}
ngOnInit(): void {
/* here you go. this subscription gives you the current content of todos[] */
/* whenever it gets updated */
this.service.getTodos().subscribe(data => {
console.log(data);
});
}
PLEASE NOTE
Subscriptions to an Observable should always be finished when you leave a component. The best way to reach this goal in your case is:
modified COMPONENT
import { Subscription } from 'rxjs';
private subscription: Subscription = new Subscription();
constructor(private service: Service) {}
ngOnInit(): void {
/* add each subscriber to the subscription */
this.subscription.add(
this.service.getTodos().subscribe(data => {
console.log(data);
});
);
}
ngOnDestroy(): void {
/* unsubscribe all subscribers at once */
this.subscription.unsubscribe();
}
I have a simple app on Angular/rxjs/Ngrx which requests list of default films from the api.
component.ts
export class MoviesComponent implements OnInit {
private movies$: Observable<{}> =
this.store.select(fromRoot.getMoviesState);
private films = [];
constructor(public store: Store<fromRoot.State>) {}
ngOnInit() {
this.store.dispatch(new MoviesApi.RequestMovies());
this.movies$.subscribe(film => this.films.push(film));
console.log(this.films)
}
effects.ts
#Effect()
requestMovies$: Observable<MoviesApi.MoviesApiAction> = this.actions$
.pipe(
ofType(MoviesApi.REQUEST_MOVIES),
switchMap(actions => this.MoviesApiServ.getDefaultMoviesList()
.pipe(
mergeMap(movies => of(new MoviesApi.RecieveMovies(movies))),
catchError(err => {
console.log('err', err);
return of(new MoviesApi.RequestFailed(err));
})
)
)
);
service.ts
export class MoviesApiService {
private moviesList = [];
constructor(private http: HttpClient) { }
public getDefaultMoviesList(): Observable<{}> {
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item => this.moviesList.push(item)));
return from(this.moviesList);
}
public getMovieByTitle(movieTitle: string): Observable<{}> {
return this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
);
}
}
DEFAULT_MOVIES is just array with titles.
So my getDefaultMoviesList method is not sending data. But if I replace this.moviesList to hardcoced array of values it works as expected.
What I'm doing wrong?
UPD
I wanted to loop over the default list of films, then call for each film getMovieByTitle and collect them in array and send as Observable. Is there any better solution?
1) You should probably move this line to the service contructor, otherwise you will push a second array of default movies every time you getDefaultMoviesList:
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item => this.moviesList.push(item)));
2) Actually you should probably merge the output of each http.get:
public getDefaultMoviesList(): Observable<{}> {
return merge(DEFAULT_MOVIES.map(movie => this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
)))
}
3) You should actually only do that once and store it in BehaviorSubject not to make new HTTP request on each getDefaultMoviesList
private movies$: BehaviorSubject<any> = new BehaviorSubject<any>();
public getMovies$() {
return this.movies$.mergeMap(movies => {
if (movies) return of(movies);
return merge(DEFAULT_MOVIES.map(movie => this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
)))
})
}
4) Your implementation shouldn't work at all since:
public getDefaultMoviesList(): Observable<{}> {
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item =>
this.moviesList.push(item))); // This line will happen after http get completes
return from(this.moviesList); // This line will happen BEFORE the line above
}
So you will always return an Observable of empty array.
5) You shouldn't use map if you don't want to map your array to another one. You should use forEach instead.
map is used like this:
const mappedArray = toMapArray.map(element => someFunction(element));
You can try creating the observable using of operator.
Ex: of(this.moviesList);
One intersting fact to note is that Observable.of([]) will be an empty array when you subscribe to it. Where as when you subscribe to Observable.from([]) you wont get any value.
Observable.of, is a static method on Observable. It creates an Observable for you, that emits value(s) that you specify as argument(s) immediately one after the other, and then emits a complete notification.
I'm struggling with testing a service in an Angular project.
The service is pretty small but i can't figure out how to test the observer in the class.
I am trying to test a method of this class. The public methods must do what it promises to do. When I call the 'pop' method, the test fails.
Since the pop method is public and has a Message as return values, this method should return a message after invoke it. The underlying code is less relegant in the case of testing.
The reason the test fails is because the observer is still undefined at that moment. I suggest that the reason is because the callback method is not executed at the moment when i call the pop method in the test, thus the addMessage observer is not intialized yet.
Since I have just started on this project, I am cautious about assumptions about the code.
Does anyone have a suggestion on how I could test this code?
Is it right that the pop method is public or should it be private?
Edit:
The pop method is used by a few other classes an can't be private.
My question is actualy: Is this implementation of this service right?
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { share} from 'rxjs/operators';
import { Observer } from 'rxjs';
import { Message } from 'primeng/components/common/api';
#Injectable()
export class FeedBackService {
obsAddMessage: Observable;
obsClearMessages: Observable;
/** #internal */
private clearMessages: Observer;
private addMessage: Observer;
/**
* Creates an instance of FeedBackService.
*/
constructor() {
this.obsAddMessage = new Observable(observer => this.addMessage = observer).pipe(share());
this.obsClearMessages = new Observable(observer => this.clearMessages = observer).pipe(share());
}
/**
* Synchronously create and show a new message instance.
*
* #param {(string | Message)} type The type of the message, or a Message object.
* #param {string=} title The message title.
* #param {string=} body The message body.
* #returns {Message}
* The newly created Message instance.
*/
pop( type: string | Message, title?: string, body?: string ): Message {
const message: any = typeof type === 'string' ? {severity: type, summary: title, detail: body} : type;
if (!this.addMessage) {
throw new Error('No Containers have been initialized to receive messages.');
} else {
this.addMessage.next(message);
}
return message;
}
}
The Test:
import {Message} from 'primeng/components/common/api';
import {FeedBackService} from './feedback.service';
fdescribe('Service: Feedback', () => {
let feedbackService: FeedBackService;
const MESSAGE: Message = {severity: 'This is a message', summary: 'Title', detail: 'Body'};
const SEVERITY = 'severity';
const SUMMARY = 'summary';
const DETAIL = 'detail';
beforeEach(() => {
feedbackService = new FeedBackService();
});
it('#pop should return the message when passing in a message', () => {
let returnMessage = feedbackService.pop(MESSAGE);
expect(returnMessage).toEqual(MESSAGE);
});
});
The Error:
Like any Observable, obsAddMessage isn't executed until subscribed to, and therefore as you demonstrated the observer is still undefined if you attempt to push a new value in before subscribing. The solution is simply to set up a subscribe before calling feedbackService.pop().
I set up a simple Stackblitz to show what I mean. The spec from that Stackblitz is:
it('#pop should return the message when passing in a message', () => {
feedbackService.obsAddMessage.subscribe(message => console.log(message));
let returnMessage = feedbackService.pop(MESSAGE);
expect(returnMessage).toEqual(MESSAGE);
});
I hope this helps.
How do you map and use a JSON reponse that is a single object, rather than an array?
Recently, I started adding a new feature to a project I'm working on that should be taking a JSON response from an API and filling out a simple template with data from it. Shouldn't be difficult, right? Well, no... and yet, yes...
Mock version of the JSON response:
{
"id": 1,
"name": "Acaeris",
}
profile.service.ts
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Profile } from './profile';
/**
* This class provides the Profile service with methods to read profile data
*/
#Injectable()
export class ProfileService {
/**
* Creates a new ProfileService with the injected Http.
* #param {Http} http - The injected Http.
* #constructor
*/
constructor(private http: Http) {}
/**
* Returns an Observable for the HTTP GET request for the JSON resource.
* #return {Profile} The Observable for the HTTP request.
*/
get(): Observable<Profile> {
return this.http.get('assets/profile.json')
.map(res => <Profile>res.json())
.catch(this.handleError);
}
/**
* Handle HTTP error
*/
private handleError (error: any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg);
return Observable.throw(errMsg);
}
}
profile.component.ts
import { Component, OnInit } from '#angular/core';
import { ProfileService } from '../services/profile/profile.service';
import { Profile } from '../services/profile/profile';
/**
* This class represents the lazy loaded ProfileComponent
*/
#Component({
moduleId: module.id,
selector: 'sd-profile',
templateUrl: 'profile.component.html',
styleUrls: ['profile.component.css'],
})
export class ProfileComponent implements OnInit {
errorMessage: string;
profile: Profile;
/**
* Creates an instance of the ProfileComponent with the injected
* ProfileService
*
* #param {ProfileService} profileService - The injected ProfileService
*/
constructor(public profileService: ProfileService) {}
/**
* Get the profile data
*/
ngOnInit() {
this.getProfile();
}
/**
* Handles the profileService observable
*/
getProfile() {
this.profileService.get()
.subscribe(
data => this.profile = data,
error => this.errorMessage = <any>error
);
}
}
profile.ts
export interface Profile {
id: number;
name: string;
}
And I'm just trying to output it using {{profile.name}} but this ends up with the console showing a whole load of error messages and no output. If I try to check the contents of profile after it has loaded, it tells me it is undefined.
However, here's the confusing part. If I replace all the Profile references to Profile[], wrap the JSON in an array, add *ngFor="let p of profile" abd use {{p.name}} everything works fine. Unfortunately, in the actual finished application I would not have control of the JSON format. So what am I doing wrong when trying to handle it as a single object in comparison to handling as an array of objects?
Looks like at expression {{profile.name}} profile variable is undefined at page rendering moment. You can try either add some getter like this:
get profileName(): string { return this.profile ? this.profile.name ? ''; }
and use at template {{profileName}} or you can use ngIf at template like this:
<div *ngIf="profile">{{profile.name}}</div>
or shorter (as drewmoore suggested at comment below):
<div>{{profile?.name}}</div>
When you are working with array it is the same situation - at first rendering time array is undefined. ngFor handles this for you and renders nothing. When async operation of getting 'profile items' is complete - UI is rerendered again with correct values.
The mapfunction returns Observables which are a collection of elements. It basically work the same way as the map function for arrays.
Now to solve you can replace the Profile references by Profile[] and use {{profile[0].name}}.