I am trying to update Observable array after click on load more but it result in multiple call to API.
Here is my html:
<app-productlistother *ngFor="let item of favs$ | async" [item]="item"></app-productlistother>
<a (click)="loadMore()">Load More</a>
Component:
ngOnInit() {
this.favs$ = this._favoriteService.getFavorites(this.pageNum);
}
private loadMore() {
this.pageNum = this.pageNum + 1;
this.favs$ = this.favs$.merge(this._favoriteService.getFavorites(this.pageNum));
}
And service:
getFavorites(page): Observable<Response> {
return this._http.get(environment.baseUrl + 'favorite?page=' + page)
.map((result: Response) => result.json().data);
}
How can I solve this? On each new request to getFavorites(page) I need to push new results on bottom of array...
#Injectable()
export class MyBooleanService {
myBool$: Observable<any>;
private boolSubject: Subject<any>;
constructor() {
this.boolSubject = new Subject<any>();
this.myBool$ = this.boolSubject.asObservable();
}
...some code that emits new values using this.boolSubject...
}
Then in your component you would have something like this:
#Component({...})
export class MyComponent {
currentBool: any;
constructor(service: MyBooleanService) {
service.myBool$.subscribe((newBool: any) => { this.currentBool = newBool; });
}
}
Now depending on what you need to do with that value you may need to do chnages in your component to update, but this is the gist of using an observable
Another option is you use the async pipe within your template instead of explicitly subscribing to the stream in the constructor. Again though, that depends on what exactly you need to do with the bool values.
So you have no HTML and <app-productionother> template is just {{item.name}} ? In that case, why not structure it like this:
<ng-template *ngFor="let item of favs$ | async">
<app-productlistother [item]="item"></app-productlistother>
</ng-template>
This keeps the async pipe off the repeating element so the async pipe isn't being repeated on each iteration of item. I think that's why it's being called multiple times (likely the same number as you have items).
Also, if merge isn't working out, try concat. Concat will wait until the first observable is finished emitting and then will join the second observable. Merge doesn't wait for the first observable to finish. I imagine you'd want your data to load in order if you click load more and not have initial data interweaved with the second batch, or so on.
Related
I am working on an angular project that needs to load up pages and then display them one by one / two by two.
As per this article and some other sources, subscribing in services is almost never necessary. So is there a way to rewrite this in pure reactive style using RxJS operators?
Here's what I have (simplified) :
export class NavigationService {
private pages: Page[] = [];
private mode = Mode.SinglePage;
private index = 0;
private currentPages = new BehaviorSubject<Page[]>([]);
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.pageService.pages$.subscribe(pages => {
this.setPages(pages);
});
this.view.mode$.subscribe(mode => {
this.setMode(mode);
});
}
private setPages(pages: Page[]) {
this.pages = pages;
this.updateCurrentPages();
}
private setMode(mode: Mode) {
this.mode = mode;
this.updateCurrentPages();
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.index += 1;
this.updateCurrentPages();
}
public get currentPages$() {
return this.currentPages.asObservable();
}
}
I've tried multiple solutions and didn't manage to get it right. The closest I got was using scan(), but it always reset my accumulated value when the outer observables (pages, mode) got updated.
Any help is appreciated, thanks !
You can use merge to create reducer functions from observables. These functions will update part of a state maintained by the service. They are past along to the scan operator which will update the prior state from the reducer. After the reducer is run, currentPages is set on the new state and that new state is returned.
export class NavigationService {
private readonly relativePageChangeSubject = new Subject<number>();
readonly state$ = merge(
this.pageService.pages$.pipe(map((pages) => (vm) => ({ ...vm, pages }))),
this.relativePageChangeSubject.pipe(map((rel) => (vm) => ({ ...vm, index: vm.index + rel }))),
this.view.mode$.pipe(map((mode) => (vm) => ({ ...vm, mode })))
).pipe(
startWith((s) => s), // if necessary, force an initial value to be emitted from the initial value in scan.
scan((s, reducer) => {
const next = reducer(s);
// update currentPages on the next state here.
return next;
}, { currentPages: [], index: 0, mode: Mode.SinglePage, pages: [] }),
shareReplay(1)
)
readonly currentPages$ = this.state$.pipe(
map(x => x.currentPages),
distinctUntilChanged()
);
constructor(private pageService: PageService, private view: ViewService) { }
goToNextPage() {
this.relativePageChangeSubject.next(1);
}
}
Notes:
Instead of having a nextPage Subject, a more flexible relative change subject is used that will modify the index from the value in the prior state.
The currentPage$ observable isn't necessary, as a consumer could just attach to the main state$ and map as needed. Feel free to make state$ private or remove currentPage$.
Let's first detail what you are doing:
You subscribe to pageService.pages$ and view.mode$
Those subscriptions take the values and put them in a private variabkle
Then fire a function to use those two variables
Finally, trigger a value push.
All this can be done in a simple pipeline. You'd need to include the index as an observable (behaviour subject in our case) to react to that change too.
Use combineLatest, this will subscribe to all observables we want, and trigger the pipe WHEN ALL have fired once, and every time one changes afterwards. You may want to use .pipe(startWith("something")) on observables that should have a default value so your observable pipe triggers asap.
CombineLatest will then provide an object as value, with each value in the object key passed when created. Here pages, mode and index. I've used a switchMap to demo here if updateCurrentPages passes an observable, but you could use a map if there is no async task to be done.
export class NavigationService {
readonly currentPages$:Observable<Pages[]>;
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.paginator = new Paginator(this.pageService.pages$);
this.currentPages$ = combineLatest({
pages:this.pageService.pages$,
mode:this.view.mode$,
index:this.this.paginator.pageChange$
}).pipe(
switchMap(({pages,mode,index})=>{
return this.updateCurrentPages(pages,mode);
}),
);
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.paginator.next();
}
}
class Paginator{
pageChange$ = combineLatest({
total:this.pages$.pipe(map(pages=>pages.length)),
wanted:this.pageMove$}).pipe(map({total,wanted}=>{
// Make sure it is between 0 and maximum according to pages.
return Math.max(Math.min(total-1,wanted),0);
}),
// Do not emit twice the same page (pressing next when already at last)
dinstinctUntilChanged());
);
pageMove$ = new BehaviorSubject<number>(0);
constructor(pages$: Observable<Pages[]>){
}
next(){
this.pageMove$.next(this.pageMove$.value()+1);
}
previous(){
this.pageMove$.next(this.pageMove$.value()-1);
}
to(i:number){
this.pageMove$.next(i);
}
}
Beware tough about stating that subscribe is never needed. You may want to subscribe to some events for some reason. It is just that combining everything in a pipeline makes things easier to handle... In the example above, the observables will be unsubscribed to when the consumer of your service unsubscribes to currentPages$. So one thing less to handle.
Also, note that if multiple consumers subscribe to this service's currentPages$ the pipeline will be duplicated and unecessary work will be done, once for each subscriber. While this MAY be good, you might want to have everyone subscribe to the same "final" observable. This is easily do-able by adding share() or shareReplay(1) at the end of your pipeline. Share will make sure the same observable pipeline will be used for the new subscriber, and they will receive new values starting from then. Using shareReplay(1), will do the same but also emit the latest value directly on subscribe (just like BehaviourSubject) the 1 as parameter is indeed the number of replays to send out...
Hope this helps! When you master the RxJS you'll see that things will get easier and easier (see the difference in code amount!) but getting the hang of it take a little bit of time. Do not worry, just perseverate you'll get there. (Hint to get better, using outside variables/properties are the evil of handling pipelines)
I get data from an API-Call. I have got a many to many relationship - I will explain it with people an movies. One movie is seen by many people and one person can watch many movies.
So in the Angular Frontend, when you click on a person, you should get a more detailled view of this person including a list of its watched movies.
When I run this code (person-detail.component.ts):
public selectedperson: any = []
/*Array that contains data from the selected person*/
public movie: any = []
/*Array that contains all the movie-person connections (movie-name and person_id)*/
public selectedmovie: any = []
/*array that should contain all the movies the selectedperson has watched*/
/*some code*/
getMovies(){
const url ='http://localhost:4000/api/people-movies';
this.http.get(url).subscribe(movie => {
this.movie = movie;
this.selectedmovie=this.movie.find(item=>
{
for (var i = 0; i < this.movie.arrayLength; i++) {
if(item['person_id']===this.selectedperson.person_id)
{
return true;
}
return false;
}
});
console.log(this.selectedmovie, this.selectedperson.person_id);
return this.selectedmovie;
});
}
It does not work - but without this loop (and if I leave the If-statement):
for (var i = 0; i < this.movie.arrayLength; i++) {
}
It returns only one movie of this person - so it stops as soon as it has found a movie. But since I want to list all the movies I need the loop.
So what am I doing wrong with the loop?
Edit:
sample Data in selectedperson (is already filled):
selectedperson:
[{"person_id"=34, "name"="john"}]
movie (get this data from API Call with the mentioned URL):
[{"person_id"=33, "movie"="Titanic"}{"person_id"=33, "movie"="Star Wars"}{"person_id"=34, "movie"="Titanic"}{"person_id"=34, "movie"="Star Wars"}{"person_id"=34, "movie"="Indiana Jones"}{"person_id"=35, "movie"="Titanic"}]
wanted final result (for the person with the id 34)
Titanic
Star Wars
Indiana Jones
There are a few things wrong with your code, but i'll start with what I think is the solution for you:
getMovies(){
const url ='http://localhost:4000/api/people-movies';
this.http.get(url).subscribe(movie => {
this.movie = movie;
this.selectedmovie=this.movie.filter(item=> item['person_id'] === this.selectedperson.person_id );
console.log(this.selectedmovie, this.selectedperson.person_id);
return this.selectedmovie.map(movie.movie);
});
}
Now, what is wrong with your code:
you try to populate an empty array selectedmovie by activating find on itself, but it is empty... Instead, populate it with the movies- found in the movie array.
you're using find, which a simple look at documentation (here) would make it clear it only returns the first element satisfying the test function. Instead, used filter, which is kinda the same but goes through the whole array and returns all the objects satisfying the test.
you make a for loop without using the i inside it - a strong sign you don't actually need that loop.
There is no need to return "true" or "false" when the whole check is a single boolean expression, it would return it inline.
just a side not, your varibale names are really hard to follow. For example, an array containing movies is better be called "movies", and when you iterate over it with an array function, each element can be called "movie". using same name for both is really confusing. Also try to use camel-case notation (selectedMovie), would make names clearer.
**NOTE: my solution will only return the array from "getMovies" the way you want it, because .map is chained on the return value. If you want selectedmovie to be populated with only movie-names as string and not the whole movie-perosn_id object, just chain the .map to the filter, like so:
this.selectedmovie=this.movie.filter(item=> item['person_id'] === this.selectedperson.person_id ).map(movie => movie.movie);
Hope it helped :)
You can get your desired result like this:
const movie = [
{"person_id":33, "movie":"Titanic"},
{"person_id":33, "movie":"Star Wars"},
{"person_id":34, "movie":"Titanic"},
{"person_id":34, "movie":"Star Wars"},
{"person_id":34, "movie":"Indiana Jones"},
{"person_id":35, "movie":"Titanic"}
];
let selectedperson = [{"person_id":34, "name":"john"}];
let moviesByPerson = movie.filter(f=>
selectedperson.some(s=> f['person_id'] == s['person_id']));
console.log(`moviesByPerson: `, moviesByPerson);
In your code:
getMovies(){
const url ='http://localhost:4000/api/people-movies';
this.http.get(url).subscribe(movie => {
this.movie = movie;
this.selectedmovie = movie.filter(f=>
selectedperson.some(s=> f['person_id'] == s['person_id']));
console.log(this.selectedmovie, this.selectedperson.person_id);
return this.selectedmovie;
});
}
In addition, try to move your HTTP calls into services as it is good practice to separate concerns. As Angular docs says:
Components shouldn't fetch or save data directly and they certainly
shouldn't knowingly present fake data. They should focus on presenting
data and delegate data access to a service.
You can try like this.
this.selectedmovie = this.selectedmovie.filter(item => {
for (var i = 0; i < this.movie.arrayLength; i++) {
if (item['person_id'] === this.selectedperson.person_id) {
return true;
}
}
return false;
});
Note: Javascript array.find method returns the value of the first element in the provided array that satisfies the provided testing function.
Some issues, like mentioned, you are using find, which will return just the first match (if exists). You are looking for filter to filter the movies that match the id. Also you are trying to return from subscribe, which does not work. Also I would change your practices a bit. DON'T use any, it defeats the purpose of TypeScript. So type your data, for example using interfaces, it will help you, since compiler can tell you if your doing something that does not conform to your models, which makes debugging far easier!
Also I would move all http-requests to a service, the component would only subscribe to the result of that http-request. So all in all, I suggest the following:
interface Movie {
person_id: number;
movie: string;
}
interface Person {
person_id: number;
name: string;
}
Service:
import { map } from "rxjs/operators";
// ...
getFilteredMovies(id: number) {
const url ='http://localhost:4000/api/people-movies';
return this.http.get<Movie[]>(url).pipe(
map((movies: Movie[]) => {
return movies.filter((m: Movie) => m.person_id === id)
})
)
}
Component:
movies = <Movie[]>[];
selectedPerson = <Person>{ person_id: 34, name: "john" };
constructor(private myService: MyService) {}
ngOnInit() {
this.myService
.getFilteredMovies(this.selectedPerson.person_id)
.subscribe((data: Movie[]) => {
this.movies = data;
});
}
So we handle the filtering in the service already and component only subscribes. IF you though are calling this function in other places where you don't want the filtering to be performed, then instead do the filtering in the component.
STACKBLITZ demo with the above code.
I finally solved it - thanks erveryone for trying to help, especially #Gibor!
Here is the code for people who face a similar problem:
However - know that this code is by no meand perfect and has to be improved in many ways (e. g. with an loop to find all movies). I just post it here in the hope it helps someone with similar problems ...
component.ts:
import { Component, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { ActivatedRoute} from '#angular/router';
#Component({
selector: 'app-people-detail',
templateUrl: './people-detail.component.html',
styleUrls: ['./people-detail.component.css']
})
export class PeopleDetailComponent implements OnInit {
public data: any = []
public selectedperson: any = []
public movies: any = []
public moviesByPerson: any = []
constructor(
public http: HttpClient,
public route: ActivatedRoute) { }
getPeople()
{
const person_id = this.route.snapshot.paramMap.get('person_id')
const people_url ='http://localhost:4000/api/people'
const movies_url ='http://localhost:4000/api/person-movie'
this.http.get(people_url).subscribe
(data =>
{
this.data = data;
this.selectedperson=this.data.find
(item=>
{
if(item['person_id']===person_id)
{
return true;
}
return false;
}
)
this.http.get(movies_url).subscribe
(movies =>
{
this.movies = movies;
this.moviesByPerson=this.movies.filter(item => item.person_pk === this.selectedperson.person_pk)
}
)
console.log(this.selectedperson, this.moviesByPerson)
return this.selectedperson, this.moviesByPerson
}
)
}
ngOnInit()
{
this.getPeople()
}
}
component.html:
<div><span>ID: </span>{{selectedperson.person_id}}</div>
<h1>{{selectedperson.name}}</h1>
<p>{{selectedperson.age}}</p>
<p></p>
<table>
<thead>
<tr>
<th>Movies</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{moviesByPerson[0].movie}}</td>
</tr>
<tr>
<td>{{moviesByPerson[1].movie}}</td>
</tr>
<tr>
<td>{{moviesByPerson[2].movie}}</td>
</tr>
</tbody>
</table>
I am working on angular 2 project and I am having an issue when I am trying to change the list . NgFor not recognizing the changes , and displaying only the list loaded at first time .
here is an example code when I am loading all list and imminently after loading I reset it with null . the view still displaying all the list ...
this is my component constructor for example :
constructor( private songService : SongService)
this.songService.getSongs()
.subscribe(songsList => {
this.songs = songsList;
});
this.songs = null;
}
and this is the html :
<div class="row">
<div *ngFor= "let song of songs" class="col-md-4">
<app-song-item [song]="song"></app-song-item>
<br>
</div>
</div>
Loops in Angular sometimes screw up, in the way that they don't track your items the way you would want it to.
To prevent that, you can use a custom track by function like this
<div *ngFor="let song of songs; let i = index; trackBy: customTB" class="col-md-4">
In your TS
customTB(index, song) { return `${index}-${song.id}`; }
This way, you set up a custom trackBy function, that will update your view (the view wasn't getting updated because the tracking ID wasn't changing).
The reason why you are still seeing your list is because it is async. You can't be sure when the subscribe method is executed. It can be be direct, within seconds, take hours or not even at all. So in your case you are resetting the list before you are even getting one.
constructor( private songService : SongService)
this.songService.getSongs()
.subscribe(songsList => { //Might take a while before executed.
this.songs = songsList;
});
this.songs = null; //executed directly
}
The above explanation might be the cause of your problem, but there could also be another explanation. The constructor is only called when the component is created. Changing a router parameter doesn't necessarily create a component. Angular might re-use the component if it can.
Instead of null you should set an empty array, also have it inside a method, otherwise it never gets called
this.songService.getSongs()
.subscribe(songsList => {
this.songs = songsList;
});
clear(){
this.songs = [];
}
Try this
constructor(private songService: SongService) {
this.songService.getSongs()
.subscribe(songsList => {
this.songs = songsList;
this.reset();
});
}
reset() {
this.songs = [];
}
I have a component that fetches data from a service and pushes data to an array upon button click. If that data already exists in the array, I don't want it to be pushed again.
import { ImportResults } from '../shared/mass.interface';
import { Component, Input, OnChanges, OnInit } from '#angular/core';
import { MassEmpService } from '../shared/mass.service';
#Component({
selector: 'app-employee-selection',
templateUrl: './employee-selection.component.html',
styleUrls: ['./employee-selection.component.css']
})
export class EmployeeSelectionComponent implements OnInit {
// Define our search results
public searchResults: ImportResults[] = [];
constructor(
private _massEmpService: MassEmpService
) {
}
ngOnInit() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults.subscribe(
data => (this.searchResults.indexOf(data) === -1 ? this.searchResults.push(...data) : '')
);
}
}
Is there any quick way to test if an identical object already exists in an array without having to look for a specific key or value?
Scenario
I enter a username into a search field and press "search". It provides me the results, I push them to an array and then use *ngFor to loop over them and display them. However, if I then searched by supervisor, I don't want anyone thats already in the results to appear again, only new data that has not yet been seen.
I know this is more of a generic Javascript question but I am curious if ES6 or angular has anything short that will accomplish this?
Try to use underscore.js and findIndex method.
The code would be something like that:
import * as _ from 'underscore';
/*
rest of your code
*/
export class EmployeeSelectionComponent implements OnInit {
ngOnInit() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults.subscribe(data => {
if (this.searchResults.findIndex(elem => _.isEqual(elem,data)) == -1)
this.searchResults.push(data);
});
}
/*
rest of your code
*/
}
Other solution would be to use .filter() method, but here you need to traverse all entries as well:
ngOnInit() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults.filter(data => {
return this.searchResults.findIndex(elem => _.isEqual(elem,data)) == -1})
.subscribe(data => this.searchResults.push(data));
}
I think it could be also good, to immediatelly reject objects with the same reference:
this._massEmpService.importedResults.filter(data => data != data).... //another chain here
You could also try to experiment with _.indexOf: http://underscorejs.org/#indexOf
try below myaaray is your array wityh duplicate entries and uniue array will contain array without duplicate entries
var unique = myArray.filter((v, i, a) => a.indexOf(v) === i);
Let's say I have an array of elements and in addition to displaying the list in my app, I want to sync the list to the server with HttpClient. How can I observe changes to the array? I tried:
#inject(ObserverLocator)
export class ViewModel {
constructor(obsLoc) {
this.list = [];
obsLoc.getObserver(this, 'list');
.subscribe(li => console.log(li));
}
}
But I got neither error nor log message.
getObserver returns a property observer which will notify you when the ViewModel class instance's list property changes. This will only happen when you assign a new value to the list property, ie this.list = [1,2,3]. If you're not assigning new values to the list property and instead are mutating the value of the property via push, pop, splice, etc, you'll want to use an array observer. Use the ObserverLocator's getArrayObserver method- it takes one parameter, the array you want to observe:
import {ObserverLocator} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(ObserverLocator)
export class ViewModel {
constructor(obsLoc) {
this.list = [];
obsLoc.getArrayObserver(this.list);
.subscribe(splices => console.log(splices));
}
}
October 2015 update
The ObserverLocator is Aurelia's internal "bare metal" API. There's now a public API for the binding engine that could be used:
import {BindingEngine} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(BindingEngine)
export class ViewModel {
constructor(bindingEngine) {
this.list = []; // any Array, Map and soon Set will be supported
// subscribe
let subscription = bindingEngine.collectionObserver(this.list)
.subscribe(splices => console.log(splices));
// be sure to unsubscribe **later**
subscription.dispose();
}
}