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.
Related
I use the Async local-storage with angular to store data in the indexedDB.
The library has a method to listen to / watch for changes.
This method returns an Observable with the complete object.
What I want is to slice the changed property value, object, or the array of the object and subscribe to it.
My example object would be:
{
fetched: boolean,
loaded: boolean,
item: null, // object
list: [], // object arry
}
Now I need to watch for the changes in each property as an Observable.
fetched$.subscribe((fetched: boolean)) => {}
loaded$.subscribe((loaded: boolean)) => {}
item$.subscribe((item: any)) => {}
list$.subscribe((list: any[])) => {}
Here is the code I used to slice the changed value so far.
// slice an individual value
function sliceValue<T>(
repo: BaseRepository<T>,
key: string
): Observable<any> {
if (!repo.key || !repo.store.has(repo.key)) {
return of (null);
}
return repo.store.watch(repo.key).pipe(
distinctUntilChanged((prev, curr) => {
if (!prev || !curr) return false;
return prev[KEY.D][key] === curr[KEY.D][key];
}),
switchMap((ctx: any) => repo.store.get(repo.key)),
map((ctx: any) => ctx[KEY.D][key]));
}
// State class
export class TestState extends BaseRepository<TestModel> {
public fetched$: Observable<boolean> = sliceValue(this, 'fetched');
public loaded$: Observable<boolean> = sliceValue(this, 'loaded');
public item$: Observable<any> = sliceObject(this, 'item');
public list$: Observable<any[]> = sliceList(this, 'list');
constructor(
public readonly store: StorageMap,
) {
super(store);
}
}
But I do not know the efficient way to do it.
If there is any other best way to do it, I would like to know and try it out.
Thank you very much.
I have a method in an Angular component that pulls data via HttpClient subscription, and assigns it to an attributes this.allData, then instantiates an empty dictionary of parameters based on this, to pass to a second function:
export class TestComponent implements OnInit {
allData: object[] = []
activeData: object = {}
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.getData()
this.makeRequestBasedOnData()
}
getData() {
this.http.get(this.url).subscribe(res => {
for (let datum of res["data"]) {
this.allData.push({
"key": Object.keys(datum)[0],
"values": Object.values(datum)[0]
})
this.activeData[Object.keys(datum)[0]] = ""
}
})
}
makeRequestBasedOnData() {
let testParams = this.activeData
console.log(testParam)
}
}
I need these steps to happen sequentially. At the moment, logging the testParams in makeRequestBasedOnData() simply shows an empty object {}. When I try to return arbitrarily in the first method, I get a TypeScript error that you cannot assign a promise to type void.
How do I enforce synchronicity here even though neither method actually returns anything?
You can return Observable from getData method and proceed with any other methods within subscribe:
ngOnInit(): void {
this.getData().subscribe(() => this.makeRequestBasedOnData());
}
getData() {
return this.http.get(this.url).pipe(
tap(res => {
// update allData
})
);
}
where:
pipe method allows us to provide any kind of transformation with the data returned from http.get(...) call.
tap is a rxjs operator for side-effects, meaning that we can do everything we want with the response without modifying Observable flow.
I have the following function that traverse the tree-like object and it working fine so far.
const traverse = async (menuInputs: MenuInput[], parent: Menu = null) => {
for (const input of menuInputs) {
const entity = toEntity(input, parent);
const parentMenu = await this.menuService.create(entity).toPromise();
if (isEmpty(input.children)) {
continue;
}
await traverse(input.children, parentMenu);
}
};
My question is how can i invoke method this.menuService.create(entity) that is actually return an Observable<Menu> without convert it to Promise, are there any RxJS way of doing this ?
I've got something like this for recursive observable calls using the HttpService. You can probably work with something similar.
#Injectable()
export class CharacterReaderApiService implements CharacterReader {
private characters: SwapiCharacter[] = [];
constructor(private readonly http: HttpService) {}
async getCharacters(): Promise<Character[]> {
console.log('Querying the API for character data...');
return this.callUrl('https://swapi.dev/api/people')
.pipe(
map((data) => {
return data.map(this.mapPerson);
}),
tap(async (data) =>
promises.writeFile(join(process.cwd(), characterFile), JSON.stringify(data)),
),
)
.toPromise();
}
private callUrl(url: string): Observable<SwapiCharacter[]> {
return this.http.get<SwapiResponse>(url).pipe(
map((resp) => resp.data),
mergeMap((data) => {
this.characters.push(...data.results);
return iif(() => data.next !== null, this.callUrl(data.next), of(this.characters));
}),
);
}
private mapPerson(data: SwapiCharacter): Character {
return {
name: data.name,
hairColor: data.hair_color,
eyeColor: data.eye_color,
gender: data.gender,
height: data.height,
};
}
}
The important thing is keeping a running array of the values so that they can be referenced later on.
By using mergeMap with iif we're able to recursively call observables and work with the result as necessary. If you don't like the idea of a class variable for it, you could make the running array a part of the callUrl (or similar) method's parameters and pass it on as needed.
I'd strive to change toEntity() and menuService.create() to receive the list of children rather than the parent so I could use forkJoin:
const buildMenu = (input: MenuInput) =>
forkJoin(
input.children.map(childInput => buildMenu(childInput, menu))
).pipe(
flatMap(childMenus => this.menuService.create(toEntity(input, childMenus)))
);
I am kind of new to typescript and javascript, and I am having a real hard time figuring out how collections and file io works. I am trying to get data from a json file, which that I am successful in although when I put it into a collection the collection does not have the data.
Here is the code:
In my service class:
private configuration = "../../assets/Configurations/testConfiguration.json";
constructor(private http: HttpClient) {}
getBlogs(blog: string): Observable<IBlogPost[]> {
return this.http.get<IBlogPost[]>(blog);
}
getConfigurations() {
var configurationsData = [];
this.http.get(this.configuration).subscribe(data => {
configurationsData.push(data);
console.log(data);
// This will work and will print all the paths
});
//This will not work and will not print them, like if the collection is empty
configurationsData.forEach(x => console.log(x));
return configurationsData;
}
Where I get my service class injected:
blogs: IBlogPost[] = [];
private blogsPaths: string[] = [];
errorMessage = "";
constructor(private appUtilityService: AppUtilityServices) {}
ngOnInit() {
//This will not work, blogsPaths will not received the full collection as it should
this.blogsPaths = this.appUtilityService.getConfigurations();
this.blogsPaths.forEach(blogPath =>
this.appUtilityService.getBlogs(blogPath).subscribe(
b =>
b.forEach(blog => {
this.blogs.push(blog);
}),
error => (this.errorMessage = <any>error)
)
);
}
testConfiguration.json:
[
"../../assets/BlogPosts/testBlog1.json",
"../../assets/BlogPosts/testBlog2.json"
]
Bonus if you include a good tutorial on how collections work in javascript and how to return them properly
Turns out to be this, yes really...
Here I made it nice:
OnInit:
this.appUtilityService.getConfigurations().subscribe(
b =>
b.forEach(x => {
this.appUtilityService.getBlogs(x).subscribe(
b =>
b.forEach(blog => {
this.blogs.push(blog);
}),
error => (this.errorMessage = <any>error)
);
}),
error => (this.errorMessage = <any>error)
);
AppUtilityService:
getBlogs(blog: string): Observable<IBlogPost[]> {
return this.http.get<IBlogPost[]>(blog);
}
getConfigurations(): Observable<string[]> {
return this.http.get<string[]>(this.configuration);
}
Make it really nice:
ngOnInit() {
this.initializeBlog();
}
private initializeBlog(): void {
this.appUtilityService.getConfigurations().subscribe(
b =>
b.forEach(x => {
this.appUtilityService.getBlogs(x).subscribe(
b =>
b.forEach(blog => {
this.blogs.push(blog);
}),
error => (this.errorMessage = <any>error)
);
}),
error => (this.errorMessage = <any>error)
);
}
Inside getConfigurations, you're invoking an asynchronous http request. So it's normal, that configurationsData is empty outside the subscribe method.
Here, I'm using the powerful of RxJS to simplify the code and avoid to use
Array.push and nested subscribe().
1) Basic async service method to get data
So getConfigurations should return an Observable:
getConfigurations(): Observable<string[]> {
return this.http.get<string[]>(this.configuration);
}
And also getBlogs should return an Observable:
getBlogs(blogsPath: string): Observable<BlogPost[]> {
return this.http.get<BlogPost[]>(blogsPath);
}
Here, to respect Angular best practice, I remove I at the beginning of each interface, so BlogPost.
2) Here is the magic of RxJS
Then, you have another method inside your service to retrieve an Observable of blog posts, which should look like this:
getBlogPosts(): Observable<BlogPost[]> {
return this.getConfigurations().pipe(
mergeAll(),
concatMap(blogsPath => this.getBlogs(blogsPath)),
concatAll(),
toArray()
)
}
Which means, retrieve all collection of blog paths, for each array of paths, for each path, get blog posts, and then return an unique array of all posts.
Finally, inside your component, you call getBlogPosts with a subscription:
ngOnInit() {
this.service.getBlogPosts().subscribe(blogs => {
...do something with your blogs array.
});
}
or even better with async pipe inside your template :
.ts file:
blogs$: Observable<BlogPost[]>;
...
this.blogs$ = this.appService.getBlogPosts();
.html file:
<ul *ngIf="blogs$ | async as blogs">
<li *ngFor="let blog of blogs">
#{{ blog.id }} - {{ blog.title }}
</li>
</ul>
Check my full demonstration of this RxJS alternative on Stackbliz.
Recommended read on medium from Tomas Trajan
In a component, in ngOnInit() I've got two subscriptions to a data service. I want to do some processing once both subscriptions have returned. Whats the best way to do this? I can just process at the end of each, this just seems a little inefficient and won't work for which ever subscription activates first,
Thanks,
Component.TS
ngOnInit()
{
this.dataService.dataA().subscribe((dataAJSON) =>
{
this.dataA= dataAJSON
}
this.dataService.dataB().subscribe((dataBJSON) =>
{
this.dataB= dataBJSON
}
DataService
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/map';
#Injectable()
export class PMDataService
{
constructor(public http : Http)
{
}
dataA()
{
var dataA: any;
var json;
dataA= this.http.get("./assets/dataa.json")
.map(res => res.json());
return dataA
}
dataB()
{
var dataB: any;
var json;
dataB= this.http.get("./assets/datab.json")
.map(res => res.json());
return dataB
}
}
You can use Observable#forkJoin function on the Observables. It emits the last value from each when all observables complete,
Observable.forkJoin(this.dataService.dataA(), this.dataService.dataB())
.subscribe(val => /* val is an array */)
The method used depends on how you want to receive the data:
You can use the zip function. Emits once when all have emitted once. So similar to Promise.all except not on completion.
Observable.zip(obs1, obs2).subscribe((val) => { ... });
You can use the forkJoin function. Emits once when all have completed. So exactly like Promise.all.
Observable.forkJoin(obs1, obs2).subscribe((val) => { ... });
You can use the merge function. Emits in order of emission so could be 1st then 2nd or 2nd then 1st:
obs1.merge(obs2).subscribe((val) => { ... });
You can use concat function. Emits in order 1st then 2nd regardless if 2nd emits first:
obs1.concat(obs2).subscribe((val) => { ... });
It's best practice to split these up into a couple lines for clarity.
const obs1 = Rx.Observable.of(1,2,3);
const obs2 = Rx.Observable.of(1,2,3);
const example = Observable.zip(obs1, obs2);
//const example = Observable.forkJoin(obs1, obs2);
//const example = obs1.merge(obs2);
//const example = obs1.concat(obs2);
example.subscribe(val => { ... });
You could use the operator Zip or CombineLatest from rxjs.
See ReactiveX operators
You could do something like this:
Observable.zip(
this.http.get("./assets/dataa.json"),
this.http.get("./assets/dataa.json")
.take(1)
.map(values => [values[0].json(), values[1].json()])
.subscribe(values => {
// do something with my values
});
You could You can use concat to combine the observables and return a single observable.
Subscribe to observables in order as previous completes, emit values
changed service code
import 'rxjs/add/operator/concat';
export class PMDataService
{
data(){
return this.dataA().concat(this.dataB());
}
// methods dataA and dataB are unchanged, some of the constructor
}
Component code
ngOnInit(){
this.dataService.data().subscribe((dataJSON) =>
{
this.dataA= dataAJSON[0];
this.dataB= dataAJSON[1];
}
}