Angular #Input() Not Updating UI in Child - javascript

I've implemented a child component to render a table based on a list provided via #Input(). The data is loaded via http, however the UI (child component) is not updated unless I wave my mouse over the screen. I've seen people post about implementing ngOnChanges() in my child, but I thought Angular was supposed to do this by default? Am I missing something? Why would the UI not update with this?
Child code looks something like this:
child.component.ts
#Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss'],
})
export class ChildComponent implements {
#Input() data: any[] = [];
constructor() {}
}
child.component.html
<table>
<tr *ngFor="let item of data"><td>{{ item }}</td></tr>
</table>
Parent code that uses the component looks something like this:
parent.component.ts
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor(private endpointService: EndpointService) {}
ngOnInit() {
// response is a string array like: ['hello', 'world']
this.endpointService.loadData().subscribe((response) => {
this.data = response;
});
}
}
parent.component.html
<child [data]="data"></child>
============================= EDIT ==================================
I verified that it only fails to load when updating inside of the subscribe callback (if I set a static array, it loads just fine).
So it looks like I'm able to resolve this by running changeDetectorRef.detectChanges() in the parent component, but this feels hackish like I shouldn't have to do this. Is this a good way to resolve this? Or does this indicate something wrong with my implementation?
parent.component.ts
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor(private endpointService: EndpointService,
private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
// response is a string array like: ['hello', 'world']
this.endpointService.loadData().subscribe((response) => {
this.data = response;
this.changeDetectorRef.detectChanges();
});
}
}

You can also try to force change detection by forcing the value reference update via, for example, the spread operator:
this.endpointService.loadData().subscribe((response) => {
this.data = [...response];
});

hummm well .. when the component is rendered as first time it will show with the empty array becouse the api call stills happening and needs the onchanges method in child component in order to listen the complete api call and the list will re render

Seems that you have some other errors in template expressions which force the whole template to fail. Here's a stackblitz I've created and everything works: https://stackblitz.com/edit/angular-ivy-w2ptbb?file=src%2Fapp%2Fhello.component.ts
Do you have maybe some errors in console?

I replaced the service with a static string array and it worked well. I think there is problem with the observable subscription.
child.component.ts
import { Component, OnInit,Input } from '#angular/core';
#Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() data: any[] = [];
constructor() { }
ngOnInit() {
}
}
parent.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.css'],
})
export class ParentComponent implements OnInit {
data: string[] = [];
constructor() {}
ngOnInit() {
this.data = ['hello', 'world','aaa','ddd'];
}
}

Related

Expression Changed After it has Been Checked When Using #HostBinding

I created a breadcrumb component, I have a service BreadcrumbService that has a function that reads the url path and converts them to an array of segments (this.breadService.getUrlPathSegments()). When breadcrumbs is loaded or updated, I get the following error:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'click-enabled': 'true'. Current value: 'false'.
What is the correct way to handle this? The code works the way I intended, but I need to handle the error message.
What I am trying to do is disable click events on the last item in the breadcrumb list, so when you click on it, none of the events fire. This all works even though I receive the error message.
What I am doing is when the view is checked, update the value of each breadcurmb's clickable state. This is done just like this:
#Component({
selector: 'breadcrumbs',
styleUrls: ['./breadcrumbs.component.scss'],
template: `
<ng-content select="breadcrumb"></ng-content>
`,
encapsulation: ViewEncapsulation.None
})
export class Breadcrumbs implements AfterViewChecked {
#Input() disableLast = true;
#ContentChildren(Breadcrumb, { descendants: false })
breadcrumbs!: QueryList<Breadcrumb>;
ngAfterViewChecked() {
this.enableDisableLast();
}
enableDisableLast() {
if (this.breadcrumbs && this.breadcrumbs.length > 0) {
this.breadcrumbs.forEach(item => { item.clickable = true; });
this.breadcrumbs.last.clickable = !this.disableLast;
}
}
}
Next in the breadcrumb I have a #HostBinding(), that updates the class of the element. Which is done like this:
#Component({
selector: 'breadcrumb',
styleUrls: ['./breadcrumb.component.scss'],
template: `
<button>{{label}}</button>
`
})
export class Breadcrumb {
#HostBinding('class.click-enabled')
get clickEnabled() { return this.clickable; }
}
I then combine the two in the component that I am using them with a forEach to create the child breadcrumbs. I also listen for navigation changes to re-generate the array of breadcrumb segments to keep the breadcrumb display up-to-date with the current path.
#Component({
selector: 'app-root',
templateUrl: `
<breadcrumbs>
<breadcrumb *ngFor="let crumb of breadcrumbs" [label]="crumb.label|titlecase" [routerLink]="crumb.uri"></breadcrumb>
</breadcrumbs>
`,
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
breadcrumbs: BreadcrumbSegment[] = [];
constructor(
private router: Router,
private breadService: BreadcrumbService
) { }
ngOnInit() {
this.router.events.subscribe(val => {
if (val instanceof NavigationEnd) {
// Returns an array formatted as: {label:string; uri:string;}[]
this.breadcrumbs = this.breadService.getUrlPathSegments();
}
});
}
}
I am not sure if this is the optimal solution, but it is working for my needs. When the Breadcrumbs component view is initialized, I set the QueryList to dirty, then pipe a delay before I subscribe to the changes. This stops the error from showing up and runs the change detection.
#Component({...})
export class Breadcrumbs implements AfterViewChecked {
ngAfterViewInit() {
// Set to dirty so the changes emit at least one time.
this.breadcrumbs.setDirty();
this.breadcrumbs.changes.pipe(delay(1)).subscribe(() => {
this.enableDisableLast();
});
}
}

Angular - How can I toggle the visibility of an element in a component from another component?

I have the following scenario in my Angular app:
A component MainDashboardComponent that is visible when I have the route /. Obviously I have the <router-outlet> tag in my app.component.html file, which looks like this:
<app-side-menu></app-side-menu>
<div class="main-container">
<div class="content">
<router-outlet></router-outlet>
</div>
</div>
As you can see I have a SideMenuComponent I use to have a side menu on all my routes. In MainDashboardComponent I have a method that for some reason needs to toggle a chat element that is situated on the side menu.
Inside the SideMenuComponent I have a method that handles the visibility toggle for the chat element and it works as expected. How can I call this method from my MainDashboardComponent and toggle the chat element from there?
What I tried with no success
I tried to inject the SideMenuComponent inside my MainDashboardComponent but, though the method toggleChat() is called, the element doesn't change it's visibility. Looks like I have a kind of multiple instance of the same component I guess...
Can you please help me with this? Thank you!
MainDashboardComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-main-dashboard',
templateUrl: './main-dashboard.component.html',
styleUrls: ['./main-dashboard.component.scss']
})
export class MainDashboardComponent implements OnInit {
constructor() { }
ngOnInit() {}
setFocus(id) {
// here I'd like to call SideMenuComponent togglechat() ...
}
}
SideMenuComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-side-menu',
templateUrl: './side-menu.component.html',
styleUrls: ['./side-menu.component.scss']
})
export class SideMenuComponent implements OnInit {
showChat: boolean;
constructor() {
this.showChat = false;
}
ngOnInit() {
}
toggleChat() {
this.showChat = !this.showChat;
}
}
To communicate between different components, there are different ways.
If you want to communicate between parent and child component, you can use EventEmitter to emit event from child component and handle the event in your parent component
If you want to communicate between any components, you can use Service and implement communication with the help of EventEmitter or Subject/BehaviorSubject
In your case, we can create a service, myService.ts and declare and eventEmitter
.service.ts
#Injectable()
export class AppCommonService {
toggle : EventEmitter<boolean> = new EventEmitter<boolean>()
}
mainDashboard.component.ts
constructor(private myService : myService){}
chatStatus : boolean = false;
ngOnInit(){
this.myService.toggle.subscribe(status=>this.chatStatus = status);
}
toggleChat(){
this.myService.toggle.emit(!this.chatStatus);
}
sideMenu.component.ts
constructor(private myService : myService){}
chatStatus : boolean = false;
ngOnInit(){
this.myService.toggle.subscribe(status=>this.chatStatus = status);
}
Generally this is the domain of a service!
Just create a service and add the "showCat" property.
Inject the service into both components
Alter SideMenuComponent to:
toggleChat() {
this.myService.showChat = !this.myService.showChat;
}
Alter MainDashboardComponent, also use this.myService.showChat to show / hide your chat window
Service TS
#Injectable()
export class MyService{
showCat:boolean = true
}
MainDashboardComponent
toggleChat() {
this.myService.showChat = !this.myService.showChat;
}
SideMenuComponent
chatVisiblity = this.myService.showCat //<-- bind this to the element attribute
You could efficiently use child to parent communication in this scenario. You'll need to create a custom event using angular's EventEmitter in your SideMenuComponent and use it in your MainDashboardComponent.
So, here is some code that may help you -
// SideMenuComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-side-menu',
templateUrl: './side-menu.component.html',
styleUrls: ['./side-menu.component.scss']
})
export class SideMenuComponent implements OnInit {
#Output() valueChange = new EventEmitter();
showChat: boolean;
constructor() {
this.showChat = false;
}
ngOnInit() {
}
toggleChat() {
this.showChat = !this.showChat;
this.valueChange.emit(this.showChat);
}
}
// MainDashboardComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-main-dashboard',
template: `<app-side-menu (valueChange)='setFocus($event)'></app-side-menu>`
styleUrls: ['./main-dashboard.component.scss']
})
export class MainDashboardComponent implements OnInit {
constructor() { }
ngOnInit() { }
setFocus(event) {
// check for required input value
console.log(event);
}
}
Refer these tutorials if required -
https://dzone.com/articles/understanding-output-and-eventemitter-in-angular,
https://angular-2-training-book.rangle.io/handout/components/app_structure/responding_to_component_events.html

Pass changing array to child component #Input field

I am trying to pass an array to a child component. The array is being defined in the subscribe method of a parent component's onInit lifecycle hook.
Parent component:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'selector-parent',
templateUrl: 'parent.component.html'
})
export class ParentComponent implements OnInit {
array: [];
constructor() { }
ngOnInit() {
this.appDataService.getValues()
.subscribe(
value => {
value.item
.filter(loaded => !this.array.some(existing => existing.key === loaded.key))
.forEach(loaded => { this.array.push(loaded) })
}
)
}
}
Child component:
import { Component, OnInit } from '#angular/core';
#Component({
selector: '[selector-child]',
templateUrl: 'child.component.html'
})
export class ChildComponent implements OnInit {
#Input() inputArray: [];
constructor() { }
ngOnInit() {
console.log(this.inputArray)
}
}
Binding is: <tr selector-child [inputArray]="array"></tr>
Unfortunately, the console log on the inputArray is turning up undefined. I've tried using ngOnChanges in the child component but for some reason, it won't recognize the change in the parent. Thinking of using a service to pass the data if there isn't a simpler way to solve the problem.
You have to initialize the array in your parent component:
array: [] = [];
I don't see ngOnChanges in your child component. Also, make sure you do it as how it specified in the angular docs. https://angular.io/guide/component-interaction#intercept-input-property-changes-with-ngonchanges

Angular #Output not working

Trying to do child to parent communication with #Output event emitter but is no working
here is the child component
import { Component, OnInit, Output, Input, EventEmitter } from '#angular/core';
#Component({
selector: 'app-emiter',
templateUrl: './emiter.component.html',
styleUrls: ['./emiter.component.css']
})
export class EmiterComponent implements OnInit {
#Output() emitor: EventEmitter<any>
constructor() { this.emitor = new EventEmitter()}
touchHere(){this.emitor.emit('Should Work');
console.log('<><><><>',this.emitor) // this comes empty
}
ngOnInit() {
}
}
this is the html template
<p>
<button (click)=" touchHere()" class="btn btn-success btn-block">touch</button>
</p>
The console.log inside the touchHere it shows nothing
even if I put this inside the parent component it show nothing as well
parent component
import { Component , OnInit} from '#angular/core';
// service I use for other stuff//
import { SenderService } from './sender.service';
// I dont know if I have to import this but did it just in case
import { EmiterComponent } from './emiter/emiter.component'
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
user: any;
touchThis(message: string) {
console.log('Not working: ${message}');
}
constructor(private mySessionService: SenderService) { }
}
and here is the html template
<div>
<app-emiter>(touchHere)='touchThis($event)'</app-emiter>
</div>
Parent component template:
<app-emitor (emitor)='touchThis($event)'></app-emiter>
In parent template #Output should be 'called', not the child method.
Also, see: https://angular.io/guide/component-interaction#parent-listens-for-child-event
Here’s an example of how we write a component that has outputs:
#Component({
selector: 'single-component',
template: `<button (click)="liked()">Like it?</button>`
})
class SingleComponent {
#Output() putRingOnIt: EventEmitter<string>;
constructor() {
this.putRingOnIt = new EventEmitter();
}
liked(): void {
this.putRingOnIt.emit("oh oh oh");
}
}
Notice that we did all three steps: 1. specified outputs, 2. created an EventEmitter that we attached
to the output property putRingOnIt and 3. Emitted an event when liked is called.
If we wanted to use this output in a parent component we could do something like this:
#Component({
selector: 'club',
template: `
<div>
<single-component
(putRingOnIt)="ringWasPlaced($event)"
></single-component>
</div>`
})
class ClubComponent {
ringWasPlaced(message: string) { console.log(`Put your hands up: ${message}`);
} }
// logged -> "Put your hands up: oh oh oh"
Again, notice that:
putRingOnIt comes from the outputs of SingleComponent
ringWasPlaced is a function on the ClubComponent
$event contains the thing that wasemitted, in this case a string
<app-emiter (emitor)="touchThis($event)" ></app-emiter>
By using #Output() you should apply the event you need to emit in the directive of the emitter component.Adding the name of the variable to the the directive and but the emitted over function inside the quotation passing the $event.
touchHere() is the method from which you are binding some value to emit with your EventEmitter. And your EventEmitter is 'emitor'.
So your code will work if you simply do the below:
<app-emiter (emitor)='touchThis($event)'></app-emiter>

Multiple instance of angular 4 directive called from a component mesed up the input values

I have a component in angular 4 that is called three times. In template metadata I have a div with a directive with some bindings like this.
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {};
this.cOptions = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
//this.report.opt is binded to a component when is instantiated.
//this.gServ.objectMerge is a function that merge the two objects
}
}
this.cOptions change for every instance of the component, then in the directive I have this:
import { Directive, ElementRef, HostListener, Input, OnInit } from '#angular/core';
#Directive({
selector: '[gDirective]'
})
export class SGDirective implements OnInit {
public _element: any;
#Input() public cOptions: string;
constructor(public element: ElementRef) {
this._element = this.element.nativeElement;
}
ngOnInit() {
console.log(this.cOptions);
}
}
The problem is that console.log(this.cOptions); always print the same object, even when component set cOptions with diferent values in ngOnInit method of the compnent.
Do you have some idea what is wrong?
Your component property binding [cOptions]="dataChart" doesn't look good, reason being your dataChart is not even defined. it should be like [DIRECTIVE_PROPERTY]="COMPONENT_PROPERTY" and your COMPONENT_PROPERTY is not even defined in SGComponent component class.
Your component class should be something like this:
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="Options">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = {};
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
#Ashwani points out a valid problem with your code. The way your template is wiring things up, nothing will ever be passed to the SGDirective input.
Another potential problem you could be running into has to do with the gServ code. If gServ is a singleton (which is probably the case) and it is returning the same object to each of the SGComponents, then all the SGDirectives will have the same value. A simple way to test this is to put {{Options | json}} in the SGComponent template.
To create a new instance of the gServ service for each SGComponent you can add a providers array to the #Component metadata. It would look like this:
import {gServ} from '../gServ.service';
#Component({
selector: 'sr-comp',
template: `{{Options | json}}<div gDirective [cOptions]="Options"></div>`
providers: [gServ],
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
You have probably the same return/value at this.gServ.objectMerge) (you can test it wihtout calling the service, and passing each one one different objet make by you)
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
//#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {nicolas: 'nicolas1'}; //change this in the next component that use the directive
}
}
If that is the case, your problem is that gServ is provide at the same rootComponent. with angular, service provider at the same rootComponent are singleton.
And use the same type in your directive and your component!!

Categories