I have a parent component which has two templates (Stackblitz):
the first contains a simple data-bound text
the second contains a child component which updates data model (from its constructor and ngOnInit in this demo)
Here is the parent component template:
<ng-template [ngIf]="true">
<div>
<b>Value from TemplateComponent: </b>{{ dataService.value | async }}
</div>
</ng-template>
<ng-template [ngIf]="true">
<template-component></template-component>
</ng-template>
Both parent and child components are ChangeDetectionStrategy.OnPush and this is a requirement.
The issue is that the data updates starting from ngOnInit (and later ones) of the child component don't get picked up by the change detection in the parent component. This causes the parent component in the demo to display:
Value from TemplateComponent: value from constructor
instead of
Value from TemplateComponent: value from ngOnInit
since the CD picks up an update on constructor stage only, but not in ngOnInit and later:
export class TemplateComponent implements OnInit {
public constructor(public readonly dataService: DataService) {
this.dataService.setValue("value from constructor");
}
public ngOnInit(): void {
// This update is NOT picked up by CD in parent component
this.dataService.setValue("value from ngOnInit");
}
}
Except this approach with markForCheck(), I've also tried a simple #Output event in the child, but it obviously doesn't work, since the child is created via ng-template and actually is not a direct child of the parent component.
Could someone give an idea of what is the cleanest and Angular's OnPush-friendly approach for passing data to the parent from ng-template created control?
P.S.: By
the cleanest and Angular's OnPush-friendly approach
, I mean something that keeps all parts (components and service loosely-coupled), for example, without involving setTimeout() or other dirty fixes.
You've grasped the idea of using observables, async pipes and OnPush strategy, but the problem is that you have to go a little deeper and understand how angular component lifecycle events work.
Let's see what happens (simplified version)
Parent component constructor()
Child component constructor()
Parent component onInit() - evaluate #Input templates and ngIf directives
Child component onInit() - trigger changes to some properties used in parent
Parent afterViewInit()
Once onInit in child component is called, the directives in parent component were already evaluated! From angular perspective content inside [ngIf] template is already what it is suppose to be and there's need to run change detection again or update anything.
You shouldn't change data that is used in parent component from onInit() in child component, otherwise you will surely break something.
If you wait until all components are fully instantiated and trigger change, then all views will be updated.
eg. in your child component:
setTimeout(()=>this.dataService.setValue("hello"),1000)
That also works, if you try to markForCheck in ngAfterViewInit in your parent component
ngAfterViewInit(): void {
this.changeDetector.markForCheck();
}
or if you won't use *ngIf the value will be re-evaluated.
<div><b>Value from regular div: </b>{{ dataService.value | async }}</div>
All of mentioned workarounds are not advisable, just don't change data from child component's onInit and you will be fine.
Related
I'm using two components, SelectTag and the main component, SettingsComponent. The select component is a simple directive with a selector, but the issue I'm having is updating a parent variable from the child component.
For example: I have an ngModel binding to a variable (name) in the selector component, but how can I access this from the parent component (settings)?
What I've tried so far is using the Angular EventEmitter:
#Output() onNameChange: EventEmitter<string>;
...and accessing it through
<select-tag (onNameChange)="name = $event">
Is there a better practice to doing this?
You have a few options to share data between Child and Parent Components.
The best way is probably using an EventEmitter like you already tried, just make sure to trigger the event when you want to update.
For example:
export class ChildComponent {
#Output() nameUpdate: EventEmitter<string> = new EventEmitter<string>();
updateParent(name){
this.nameUpdate.emit(name)
}
}
child.component.html:
<input (input)="updateParent($event.target.value)">
Now the parent who's using this child selector can listen to the nameUpdate event:
<app-child (nameUpdate)="nameUpdate($event)">
You will get the new value within the $event.
You can also consider using services and DI to share data across components, or any other state management tools. But, if your components are in a sibling relationship, Use the Input and Output decorators as the best practice.
You can create a centralized centralized.service.ts component to distribute the data to your components, this way you get it centralized and easy to control the data flow when your app grow:
export class CentralizedService {
private sharedData = {};
getSharedData(){
return this.sharedData
}
updateSharedData(data){
//Statements
}
}
And inject the service to your module and get access to it in both your component
constructor(private centralizedService: CentralizedService){}
ngOnInit(){
this.componentData = this.centralizedService.getSharedData();
}
//Create a method to update shared data
updateSharedData(data) {
this.centralizedService.updateSharedData(data);
}
I have two components, parent.component.ts and child.component.ts, the template, parent.component.html likes below:
<div>
<child-component [status]="'good'" (click)="changeToBad()"></child-component>
</div>
I bind a constant string good intentionally to demonstrate my concern. So, initially, the good string is passed to the child component. But if I change its value in child.component.ts, like this.status = 'bad';, what will happen?
I think the input binding of parent is not synced with the child, since from now on they have different status. If I query console.log(this.status), it will say bad. If I want to keep them in sync, I have to use some output bindings.
How I can do to make sure the input binding still work after the programmatically change. Say, it changes to bad for one tick, but it changes back to good (automatically) since the binding from parent.
The parent basically doesn't listen to the change in the child component, unless the child component use the parent as an input, like
<child-component [parent]="parent"></child-component>
and in the parent component
this.parent = this
Otherwise, you can create an Output for the status in the children component and the parent component can listen to it
#Output() public statusEvt = new EventEmitter<boolean>();
and
<child-component (statusEvt)="updateStatus($event)"></child-component>
so in the parent component, you can add this function
public updateStatus(evt: boolean) {
this.status = evt;
}
when you want to emit the change, in the child component, just call
this.statusEvt.emit(this.status)
I have a problem working with two components (pop ups) in which i have to send data from a chlid component to another one (parent) who doesn't have an event to extract this data.
logically i have to find a sort of function that makes the parent listen to the changes made in the child.
The Changes have to appear in the same time in both components.
Could any one help ?
The answer is in your question. You need an Output property, which is the Angular generalization of a JS event.
In your child component:
class ChildComponent {
#Input() someProperty: string;
#Output() dataChanged = new EventEmitter<string>();
whenSomethingHappensInChild() {
this.dataChanged.emit('something');
}
}
In your parent template:
...
<app-child [someProperty]="'someValue'" (dataChanged)="doSomething($event)"></app-child>
...
And in you Parent code:
class ParentComponent {
...
doSomething(theValue: string) {
// TA-DAA! you have the value.
}
...
}
Please, do yourself a favor and READ THE DOCS, or, better, a book ;)
In particular:
https://angular.io/guide/architecture-components has a full overview of the basics of binding, which this problem falls into.
Have a nice day.
Yes, you can use a shared BehaviorSubject to push values and both components have to subscribe to get this changes
Problem Solved: I used the #Host() tag to get the current instance of the Parent component and access the methode that changes it's attributes.
Here is what you should do.
First:
You should declate your parent component in the child
parent:ParentComponent;
Second :
you should pass your current parent instance to your new declaration in the constructor
constructor(#Host() currentParent:ParentComponent){
this.parent=currentParent
}
Third:
Now try just to access the methods and attributes in the parent components
changeParentAttribute(){
this.parent.iAmInTheParent();
}
I hope you find this helpful
I was working on Angular2 project where I have a folder called layout where I split common layouts of certain pages into 3 other components(3 folders-header,sidebar, and footer). I have an anchor tag(...) on my header. Upon hitting on it I need to extend the header upto the left end of screen and at the same time sidebar need to be hidden. On hitting the anchor tag again both should came into actual position(toggling). I tried like
[ngClass]="{'hide-sidebar' : flags}" class="fix-header"...
where flags is a boolean variable and toggles it's value on hitting anchor tag. I stored the state of this variable in localStorage so that it can be accessed on sidebar, another component.
but this helped me to extend header to left end of screen and sidebar remain unchanged.
I think there can be another logic for achieving both in same hit on anchor tag(button).
Communication using #input and #Output
if the components are in parent-child relationship e.g. navbar-component is in header-component template or header-component is in navbar-component template, then you can use angular #Input #Output decorators to pass data between components
More on #Input and #Output and Component interaction in angular documentation here
Communication Using Service
if these components are not directly related then, you can use a service to communicate between these components, where one component pushes data to service and the other gets it from the service,
More on communication among components using a service is explained in Angular documentation here
Update: Siblings with Same Parent Communication Using #Input() #Output()
If you don't want to use service for communication and keep things less complex, you can use the common parent for sibling components.
Lets suppose the variable which you want to toggle resides in header component,
now you create an EventEmitter in your header.component.ts like this
#Output() onToggle = new EventEmitter();
then in the function where you toggle the variable,
you can write this,
toggle() {
if(this.flags === true) {
this.flags = false;
}else {
this.flags = true;
}
this.onToggle.emit(this.flags);
}
then in your parent template, you'll bind the function to the header which will be executed when the above event is triggered,
so in your parent template you can write this,
<app-header (onToggle)="notifySidebar($event)"></app-header>
And the notifySidebar method in you parent.component.ts will be like this,
notifySidebar(flags) {
console.log(flags);
this.flags = flags;
}
You need to define the above flags property (you can choose any name) in parent.component.ts,
Now the final part, you need to tell the sidebar.component.ts that you flag property has changed, so you will define #Input() property in your sidebar.component.ts
#Input() flags: boolean;
and a method ngOnChanges (keep in mind to use ngOnChanges you need to implement OnChanges interface on your sidebar.component.ts more on OnChanges here )
ngOnChanges() {
if(this.flags){
//do whatever you want to do with flags
}
}
and the flags property will be passed to the sidebar from parent like this
<app-sidebar [flags]="flags"></app-sidebar>
In our Angular 2 application we use the Combobox component from Kendo. This component is wrapped into a component created dynamically during the runtime. The code of the creation is very simple :
let factory = this.resolver
.resolveComponentFactory(ComboboxComponent);
nextTo.createComponent(factory);
The nextTo variable represents where Angular has to create the component.
#ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
The container variable represents a div in the HTML Template.
NB : the component is created during the ngAfterViewInit event. No errors are thrown during the creation.
The Kendo component is properly instanciated and initialized, but when we affect data after the creation, the component seems to don't recognize the binding, and do nothing. Any ideas ?
HTML of the component :
<kendo-combobox [data]="listItems"></kendo-combobox>
TypeScript :
#Component({
templateUrl: `combobox.component.html`,
selector: 'combobox',
styleUrls: [
'combobox.component.css'
]
})
export class ComboboxComponent extends FieldComponent {
public listItems: Array<string> = ["X-Small", "Small", "Medium", "Large", "X-Large", "2X-Large"];
}
NB2 : FieldComponent is an abstract class we use for global actions for all of our components.
EDIT1 : I finally manage to find what's the problem, but I can't say what's wrong. When I inspect the DOM, I can see that a <div role='combobox'> is hidden, and this is the combobox which contain all data. So why have I a second combobox shown with no data ?
I suspect that change detection for the component is not triggered in this case.
createComponent returns a ComponentRef which has the change detector associated with that component. You can try calling detectChanges() of that ChangeDetectorRef once the dynamic component is created.
I've found what cause this strange behavior. Due to the very first beginning of the project, we use Kendo JQuery for our components, and we use kendo.all.js. I don't really know why, but it seems that kendo.all.js interfers into the kendo-combobox HTML template of the new Angular component, and it inject some intempestive HTML which cause the strange behavior.