Angular 2 components inside ngFor are not updated - javascript

I'm struggling for a few hours trying to update the view of my list of item.
I have a component called document-list.
This component goes through each document passed to it and include a component called document-list-item.
<div *ngFor="let document of documents">
<document-list-item [document]="document"></document-list-item>
</div>
When I update the list of document, the children views are not updated.
I tried those in the documents accessor of the document-list component :
public set documents(value: DocumentDetails[]) {
this._documents = value;
// used one by one
this.changeDetector.detectChanges(); => doesn't work
this.changeDetector.markForCheck(); => doesn't work
this.appRef.tick(); => throw an error about recursivity
}
I also tried to manually update the list in the setters (well placed console.log showed me that it goes in) :
private _documents: DocumentDetails[] = [];
#Input()
public set documents(value: DocumentDetails[]) {
this._documents = [];
for (let i = 0; i < value.length; i++) {
this._documents[i] = value[i];
}
}
public get documents() {
return this._documents;
}
The only thing that works was doing this in the accessor :
public set documents(value: DocumentDetails[]) {
this.documents = [];
setTimeout(() => {
this._documents = value;
}, 0);
}
It worked but it showed a blinking list each time the documents array were updated, which is not acceptable.
Any help is welcome

You have:
<document-list-item [document]="document"></document-list-item>
So you should have in your child class:
#Input()
document: any;

Related

listen component for change #Input() value in angular 8

hi i have this component . in this component i have #Input() userId: number[] = []; for send list of id to this component .
i use this component in other component , for example i use it in the news component :
<kt-user-post-list-select [userId]="noWriterIdList" (selectedUserId)="getSelectionUserList($event)">
</kt-user-post-list-select>
when i send a request to server for add news it return to me list of id : [1,2,3] and then i must send that ids to the kt-user-post-list-select with this [userId]="noWriterIdList" , But i have Problem : i need when pass the list to this component it track the changes and execute this function :
validateUSerIsWriter(ids: number[]): void {
for (let id = 0; id < ids.length; id++) {
let user = this.users.find(x => x.userId = id);
if (user != null) {
user.isDeleted = true;
}
}
}
but it dosent any work .
how can i solve this problem ???
There are 2 ways
Use ngOnChanges hook inside kt-user-post-list-select component. So you can listen for new Ids and execute the function validateUSerIsWriter. But remember, this comes with a cost of compromising performance.
Use Subject to subscribe for the newIds, and execute the function validateUSerIsWriter. In this case you don't need #Input decorator. Please refer this simple example https://stackblitz.com/edit/angular-subject-observable

How can I find all Items that meet a certain condition in JSON Data?

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>

In a spec file, how do I test a component method which has event as its parameter?

I have an autocomplete menu that I based off of primeng code. The first block of code is what I have in the component file. And the filterBrands(event) is the method I'm trying to test in a separate spec file. The second block is the code in the html file which has the stuff for the actual page. I'm not really sure where to begin in starting to test for this method.
component part:
brands: string[] = ['Audi','BMW','Fiat','Ford','Honda','Jaguar','Mercedes','Renault','Volvo','VW'];
filteredBrands: any[];
brand: string;
filterBrands(event) {
this.filteredBrands = [];
for(let i = 0; i < this.brands.length; i++) {
let brand = this.brands[i];
if(brand.toLowerCase().indexOf(event.query.toLowerCase()) == 0) {
this.filteredBrands.push(brand);
}
}
}
html part:
<p-autoComplete header = "Brand Name" placeholder="Search Brand Name" [(ngModel)]="brand"
[suggestions]="filteredBrands" (completeMethod)="filterBrands($event)" [minLength] = "1"
[size] = "37" [dropdown] = "true" (input) ="dt.filter($event.target.value,'displayName','contains')" (onSelect)="dt.filter(brand,'displayName','contains')">
</p-autoComplete>
Ok, first thing is that Unit test is all about testing the component by isolating it from other dependencies, as much as possible. So, I would suggest you to test just the behavior of filterBrands function and not worry about #Output of completeMethod of primeng:
it('should have "filterBrands()" pushing data',()=>{
component.filteredBrands = [];
const event = {query: "Au"};
component.filterBrands(event);
expect(component.filteredBrands.length).toBe(1);
// and similarly more checks
})
and just on side note, you can have it in more JavaScript way by :
filterBrands(event) {
this.filteredBrands = this.brands.filter(brand =>
brand.toLowerCase().indexOf(event.query.toLowerCase()) == 0
);
}
Basically, E2E testing should cover how primeng behaves when it is integrated with your component and when some keyboard input is provided to it.

Angular 2 - NgFor not updating view

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 = [];
}

this.myService.myEvent.toRx().subscribe() called but no DOM refresh (Zone trigger)

I'm playing with angular2 alpha 40 with ng2-play starter from pawel.
Examples are in typescript.
I have a service MovieList like this:
export class Movie {
selected: boolean = false
constructor(public name:string, public year:number, public score:number) {}
}
export class MovieListService {
list: Array<Movie>
selectMovie = new EventEmitter();
constructor() {
this.list = [new Movie('Star Wars', 1977, 4.4)];
}
add(m:Movie) {
this.list.push(m);
}
remove(m:Movie) {
for(var i = this.list.length - 1; i >= 0; i--) {
if(this.list[i] === m) {
if(m.selected) this.selectMovie.next();
this.list.splice(i, 1);
}
}
}
select(m:Movie) {
this.list.map((m) => m.selected = false);
m.selected = true;
this.selectMovie.next(m);
}
}
I have a component showing the movies list and make possible to select one by clicking on it, which call select() in the service above.
And I have another component (on the same level, I don't want to use (selectmovie)="select($event)") which subscribe to the movie selection event like this:
#Component({
selector: 'movie-edit',
})
#View({
directives: [NgIf],
template: `
<div class="bloc">
<p *ng-if="currentMovie == null">No movie selected</p>
<p *ng-if="currentMovie != null">Movie edition in progress !</p>
</div>
`
})
export class MovieEditComponent {
currentMovie:Movie
constructor(public movieList: MovieListService) {
this.movieList.selectMovie.toRx().subscribe(this.movieChanged);
setTimeout(() => { this.movieChanged('foo'); }, 4000);
}
movieChanged(f:Movie = null) {
this.currentMovie = f;
console.log(this.currentMovie);
}
}
The event is subscribed using .toRx().subscribe() on the eventEmitter.
movieChanged() is called but nothing happen in the template..
I tried using a timeout() calling the same function and changes are refleted in the template.
The problem seems to be the fact that subscribe expects an Observer or three functions that work as an observer while you are passing a normal function. So in your code I just changed movieChanged to be an Observer instead of a callback function.
movieChanged: Observer = Observer.create(
(f) => { this.currentMovie = f; }, // onNext
(err) => {}, // onError
() => {} // onCompleted
);
See this plnkr for an example. It would have been nice to see a minimal working example of your requirement so my solution would be closer to what you are looking for. But if I understood correctly this should work for you. Instead of a select I just used a button to trigger the change.
Update
You can avoid creating the Òbserver just by passing a function to the subscriber method (clearly there's a difference between passing directly a function and using a class method, don't know really why is different)
this.movieList.selectMovie.toRx().subscribe((m: Movie = null) => {
this.currentMovie = m;
});
Note
EventEmitter is being refactored, so in future releases next will be renamed to emit.
Note 2
Angular2 moved to #reactivex/rxjs but in the plnkr I'm not able to use directly those libs (didn't find any cdn). But you can try in your own project using these libs.
I hope it helps.
The movieChanged function expects the movie object and not the String. Try changing below code
setTimeout(() => { this.movieChanged('foo'); }, 4000);
to
setTimeout(() => { this.movieChanged(new Movie('Troy', 2000 , 8)); }, 4000);

Categories