Child component cant get array from its parent after removing an element. - arrays

I'm a beginner to angular. I've got small ap with 3 components.
One component is just input and button which sends input value to the parent, and parent adds the incoming input to an array which is send forward to child where I want to print out all objects of array. Whenever I run function remove() and try to add another element afterwards by add() it is added only to listOfTasks but it's not added to taskList. Can someone explain why?
Component with input:
export class InputTaskComponent implements OnInit {
#Output('newTask') newTask = new EventEmitter<string>();
input: string;
constructor() { }
ngOnInit() {
}
add() {
this.newTask.emit(this.input);
this.input='';
}
Main component:
export class AppComponent {
addedTask: string;
listOfTasks: string[]=[];
doneTask:string[]=[];
constructor() {
}
receiveNewTask(event) {
this.addedTask=event;
this.listOfTasks.push(this.addedTask);
}
receiveDoneTask(event) {
this.doneTask.push(event);
}
}
Second child:
export class AddTaskComponent implements OnInit {
#Input('tasksFromInput') taskList: string[];
#Output('doneTask') doneTask = new EventEmitter<string>();
constructor() {
}
ngOnInit() {
}
done(task) {
this.doneTask.emit(task);
}
remove(task) {
this.taskList = this.taskList.filter(e => e !== task);
console.log(this.taskList);
}
HTML of main component:
<div>
<div style="float:left; width:300px;">
<app-input-task (newTask)="receiveNewTask($event)">
</app-input-task>
</div>
<div style="float:left; width:300px;">
<app-add-task [tasksFromInput]="listOfTasks" (doneTask)="receiveDoneTask($event)">
</app-add-task>
</div>
<div style="float:left; width:300px;">
<app-done-task [done]="doneTask">
</app-done-task>
</div>
</div>

This is due to how change detection works in Angular. You have arrays in several places and use them as inputs for components.
When you add a task to them you use the push method which adds the element to the array, but the array itself is the same one, basically the reference does not change.
When you want to add an object to the tasks list and trigger the change detection you have to create a new array, for example:
this.listOfTasks = [...this.listOfTasks, this.addedTask];
In this way the example app will work. More info about change detection in Angular here.

Related

Angular2 ngOnChanges not firing when input is array

I'm using a ParentComponent that sets inputs to a ChildComponent.
If the changed input is number, the ngOnChanges hook fires, but if it's an array, it does not.
Can someone tell me what am I doing wrong, or how to make ngOnChanges firing when the array is changed?
Thank you.
child ts:
export class ChildComponent implements OnInit {
#Input() num = 0;
#Input() arr: Array<string> = [];
constructor() { }
ngOnInit(): void {
}
ngOnChanges() {
console.log('input changed');
}
}
parent ts:
export class ParentComponent implements OnInit {
constructor() { }
num = 0;
arr : Array<string> =[];
ngOnInit(): void {
}
changeNumber() {
this.num = this.num + 1;
}
changeArray() {
this.arr.push('some value');
}
}
parent html:
<button (click)="changeNumber()">change num</button>
<button (click)="changeArray()">change array</button>
<app-child [num]="num" [arr]="arr"></app-child>
That's because num is a primitive data type, whereas an array is not. An array is basically an object, where the reference is stored in the variable arr. When you "push" a new entry to the array, the reference itself is not changed.
This is the reason ngOnChange does not fire. If you want to create a new reference to the array and "push" a value to it, you should use the following code:
this.arr = [...this.arr, 'some value'];
ngOnChanges can't detect array changes since the array reference remain as it is even when the content gets changed. instead, you can use a setter method
private _arr: Array<string> = [];
#Input() set arr(data) {
this._arr= data;
console.log(this._arr);
};

Dynamic array with angular

Basically I have an api that returns data when I do a search, I push them into an array and I display them by doing an ngFor in my html.
When I want to do a new search it's the same function that is called, but the html is not updated while I get new data.
It always appears the old data recovered the first time.
To search, i used this code :
SearchBar.component.ts
export class RechercheToutComponent implements OnInit {
searchInput = new FormControl('');
constructor(
private router: Router,
private recherche: RechercheComponent
) { }
ngOnInit(): void {
}
search() {
if(this.router.url.match(/recherche.*!/)){
this.recherche.searchResult(this.searchInput.value)
}else{
this.router.navigate(['/recherche'], {queryParams: {search: this.searchInput.value}}).then(r => this.recherche.searchResult(this.searchInput.value))
}
}
}
SearBar.component.html
<form class="catalogue-search-form" (ngSubmit)="search()">
<div class="search-bar">
<input type="text"
[formControl]="searchInput"
placeholder="Rechercher dans Intra"
/>
<button type="submit" class="text-button">
<mat-icon>search</mat-icon>
</button>
</div>
</form>
Search.component.ts
export class RechercheComponent implements OnInit {
searchParam: any;
results$: Observable<Array<any>>;
isResultLoading: boolean = true;
constructor(
private route: ActivatedRoute,
private http: HttpClient
) {
}
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
this.searchParam = params['search']
});
this.searchResult(this.searchParam);
}
searchResult(searchParam) {
this.http.get(`${environment.apiUrl}/Recherchetout.php`, {params: {search: searchParam}}).subscribe(
(data: Array<any>) => {
this.results$ = of(data)
this.isResultLoading = false;
}
);
}
}
Search.component.html
<div class="recherche">
<div class="spinner-search" *ngIf="isResultLoading">
<app-spinner></app-spinner>
</div>
<div class="content" *ngIf="!isResultLoading">
<div *ngFor="let oui of results$ | async">
<div *ngIf="oui.produit != undefined">
{{ oui.produit.pdf }}
</div>
</div>
</div>
</div>
I tried to create observables but it didn't work, with a simple array too.
So my question is: Why is my data not updating on my html?
And how to do it ?
Sorry in advance for the mistakes, or for the disastrous code I begin in angular
you are injecting RechercheComponent inside the SearchBar component, angular will create different instance than the one used on the UI.
to send data between multiple components create a parent component and use it to allow communication between the two components (use the Input and Output attributes).
make your system navigate to the parent then
Parent.Component.ts
export class ParentComponent {
data: any[];
isResultLoading = false;
updateData(data: any[]) {
this.data = data
}
}
parent.component.html
<app-search-bar (change)="dataChange($event)" [(isResultLoading)]="isResultLoading"></app-search-bar>
<app-search [data]="data" *ngIf="!isResultLoading"></app-search>
update your Search.component.ts
//add outputs and inputs
#Output() change = new EventEmitter<any[]>(); //make sure to import EventEmitter from #angular/core
#Input() isResultLoading : boolean
// update searchResult subscription function
searchResult(searchParam) {
this.isResultLoading = true;
this.http.get(`${environment.apiUrl}/Recherchetout.php`, { params: { search: searchParam } }).subscribe(
(data: Array<any>) => {
this.change.emmit(data);
this.isResultLoading = false;
}
);
}
and finally instead of having observable result$ inside Search.component.ts
replace it with #Input() data:Any[]
Assuming you want to refresh the search every time a URL change, update you ngOnInit by moving this.searchResult(this.searchParam); inside the subscribe method
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
this.searchParam = params['search']
this.searchResult(this.searchParam);
});
}
with this searchResult will be called every time the router parameters get change instead of updating for the first time ngOnInit get called

Angular: handle object changes in ngFor, force print

I have my app.component with a list of objects
class Hero {
alias: string;
constructor(public firstName: string,
public lastName: string) {
}
}
class AppComponent {
...
heroes: Hero[] = [
new Hero("foo", "bar")
];
...
onHeroChange($event: Hero, index: number): void {
this.heroes[index] = $event;
}
<div *ngFor="let hero of heroes; let index=index">
<hero [hero]="hero" (heroChange)="onHeroChange($event, index)"></hero>
</div>
The HeroComponent is
export class HeroComponent {
#Input()
set hero(newValue: Hero) {
this._hero = newValue;
this.showAlias = !!newValue.alias;
}
get hero(): Hero {
return this._hero;
}
#Output() heroChange: EventEmitter<Hero> = new EventEmitter<Hero>();
showAlias: boolean = !1;
private _hero: Hero;
//
#HostListener('click')
onHeroClick(): void {
this.hero.alias = `alias_${+new Date()}`;
console.info('HERO TO EMIT', this.hero);
this.heroChange.emit(this.hero);
}
}
My problem is that even by assigning the changed hero in app.component, the set hero inside hero.component is not called, so showAlias in the example is not updated and I don't see the alias in the hero.component.
Do I need to force the ngFor by assigning the entire array?
Maybe a workaround could be removing the object from the array and then inserting again?
Sounds like useless computation though.
Note: this is just an example, it's not what I'm really working on, so something like
Update the showAlias prop in the onHeroClick method
or
Assign hero in the hero.component
unfortunately don't solve the issue. I need the changes to be on the outside because other stuff happens.
Could be another option changing the detection to onPush and marking for check manually?
Blitz ==> https://stackblitz.com/edit/angular-ivy-kpn3ds
You're not setting a new hero, you're just modifying a property on the existing one:
this.hero.alias = `alias_${+new Date()}`;
That doesn't fire the setter. Change the line like this:
this.hero = {...this.hero, alias: `alias_${+new Date()}`};

Angular 2 - Create Component Dynamically - Is it mandatory to call 'detach()' method on the component reference?

I have an Angular 2 app where I need to create child components dynamically.
Is it mandatory to call 'detectChanges()' and 'detach()' method on the component reference variable 'componentRef.changeDetectorRef' ?
I see things work properly even if I dont use them.
Are these methods are actually meant component injection performance improvement ?
#Component({
selector: 'container',
template: '<template #content></template>'
})
export class ContainerComponet implements AfterViewInit {
contentComponentRef:any;
#ViewChild('content', {read: ViewContainerRef}) contentHandle;
constructor(private componentResolver:ComponentResolver) {
super();
}
ngAfterViewInit() {
if (this.contentComponentRef)
this.contentComponentRef.destroy();
this.componentResolver.resolveComponent(ChildComponent)
.then((factory:ComponentFactory<any>) => {
let componentRef = this.contentHandle.createComponent(factory);
componentRef.instance['child_component_property'] = 'dummy value for child';
componentRef.changeDetectorRef.detectChanges();
componentRef.onDestroy(() => {
componentRef.changeDetectorRef.detach();
});
this.contentComponentRef = componentRef;
return componentRef;
});
}
}

Angular2 component view updated continuously

I have an Angular 2 component that displays a list of Items, and that registers to a service that publishes events. The problem is that even if I don't do anything when I receive an event, Angular updates the view (or at least do something when, I guess, it shouldn't).
Here is a plunker.
As you can see in the console, the "getTitle()" method of my items is called each time my service publishes a message.
Even if I don't register to my service and if my component doesn't implement the MyServiceListener interface, getTitle is called each time the service gets a message. If I don't give the service to my component in its constructor, everything is fine. So, something seems wrong with my dependency injection, but what?
Here is the relevant code of the plunker:
My service and its listeners interface:
export interface MyServiceListener {
onMessage(_message: any);
}
export class MyService {
private m_listener: MyServiceListener;
constructor() {
window.setInterval(() => {
if (this.m_listener !== undefined) {
this.m_listener.onMessage("Hi");
}
}, 500);
}
setListener(_listener: MyServiceListener) { this.m_listener = _listener; }
}
The Item class:
export class Item {
m_title: string;
constructor(_title: string) {
this.m_title = _title;
}
getTitle(): string { console.log("getTitle"); return this.m_title; }
}
My component:
#Component({
selector: 'my-app',
template : `
<div>
<ul>
<li *ng-for="#item of m_items">
{{item.getTitle()}}
</li>
</ul>
</div>
`
})
export class App implements TestBugAngularServiceListener {
private m_items: Array<Item> = new Array<Item>();
constructor(_communicationService: MyService) {
this.m_items.push(new Item("A"));
this.m_items.push(new Item("B"));
this.m_items.push(new Item("C"));
_communicationService.setListener(this);
}
onMessage(_message: any) {
}
}
bootstrap(App, [MyService]).catch(err => console.error(err));
Both articles : Change detection and Angular immutability explain a lot of thing about how Angular 2 detect changes of object, and how the tree of components in angular 2 is traversed to perform data binding...
In your sample, I think your component "my-app" can be considered to be "Immutable", so changing its "change detection strategy" to OnPush solve your problem.
You can write this :
#Component({
selector: 'my-app',
changeDetection: ChangeDetectionStrategy.OnPush,
template : `
<div>
<ul>
<li *ng-for="#item of m_items">
{{item.getTitle()}}
</li>
</ul>
</div>
`
})
And after adding the import to ChangeDetectionStrategy, databinding of "my-app" will not be computed after each browser event, but only when its input change, so never...

Resources