Angular parent component "scoping" child component - javascript

Suppose I have components Parent and Child. Child can be used either as a standalone component, or within a Parent component. However, I want Child to have different behavior based upon where it lives.
Suppose Child has an optional #Input [isHappy], which is either true or false. However, whenever a Child component is hosted by a Parent component, suppose isHappy must always be true.
AFAICT there are two ways of doing this:
1) User must just know to always specify [isHappy]="true" whenever a Child is hosted by a Parent.
<parent>
<child [isHappy]="true"></child>
</parent>
2) Parent manually sets this.child.isHappy = true within its ngOnInit lifecycle hook.
Which approach is preferred? In my opinion, approach #2 makes more sense, users don't have to know to set [isHappy]="true" when a Child is hosted by a Parent. On the other hand, I'm aware that it's frowned upon in Angular for components to programmatically change one another, especially if all components are OnPush (please correct me if I'm wrong here).

I want Child to have different behavior based upon where it lives.
How about letting Child component know where it lives with ElementRef:
export class ChildComponent {
hasParent: boolean;
constructor (
private elRef: ElementRef
) {}
ngOnInit() {
const el = this.elRef.nativeElement.parentElement as HTMLElement
this.hasParent = el.localName === 'app-parent'
}
}
stackblitz: https://stackblitz.com/edit/angular-kagdsu

In my opinion 2nd way would work, but that could add couple of things into your solution
Tight coupling between two component
What if parent component don't have child component?
To solve it by better way, I'd suggest you to use Host decorator, that will ask for Parent component dependency from Child component. If that exists the make isHappy property to be true
#Component({...})
export class Child {
#Input() isHappy: boolean = false;
constructor(#Optional() #Host() private parent: Parent) {}
ngOnInit() {
// Do only if parent exists
if (this.parent) {
this.parent.isHappy = true
}
}
}
I understand even the way suggested above has tight coupling between the Parent component. We should think of removing that dependency from the inner child component.
Yes, we can do that by little hackish way, where we would be checking current component's immediate parent component name like below. For achieving the same you had to add ViewContainerRef dependency to get hold of parent/host component.
constructor(private viewContainer: ViewContainerRef) {}
ngOnInit() {
if(this.viewContainer[ '_data' ].componentView.parent.component.constructor.name === 'Parent') {
this.parent.isHappy = true
}
}

Related

Cannot pass data from Parent to Child component

Do you have any idea about the strange problem below?
I am passing data from a parent to a child component that is returned from a service method returning data as Observable<DemoModel>. But, when child component is loading, the data is undefined and it is only filled after ngAfterViewInit (I also tried getting the data on this method, but the data is still undefined). So, I also tried to apply some ngOnchanges approach, but the problem is much more related to that the data retrieved from Parent Component is not ready while Child Component is loading (I also tried to use async, etc. instead of subscribe. How should I get data to make it ready while child component is loading?
Parent and Child Components are shown below:
Parent Comp
<child-component
[courses]="courses|async"
>
</child-component>
courses: any;
this.service.listCourses().subscribe((course: Course) => {
this.courses = course;
});
Child Comp
private courses: any;
#Input()
set data(data: any) {
this.courses.next(data);
}
myControl = new FormControl('');
ngAfterViewInit() {
// >>> THIS THROWS ERROR AS this.courses is undefined
this.myControl.setValidators([
Validators.required,
forbiddenNamesValidator(this.courses)
]);
}
I also tried to use some *ngIf in html, but as the this.courses parameter is used in the methods, it does not make any sense to check the data in html.
The problem may be caused by subscribe method, but I also tried to use promise (I am not sure if I used it properly).
There's a couple things wrong with your current implementation:
In your Parent component, courses is an array (I assume) not an observable - no need to use the async pipe
In your Child component you've named the input field data, and used a setter to call .next on a variable that should be an array - i.e. .next wont exist.
The below should fix your current implementation
Parent Comp
<child-component
[courses]="courses"
>
</child-component>
courses: any;
this.service.listCourses().subscribe((course: Course) => {
this.courses = course;
});
Child Comp
#Input() courses: any;
It's important to note that listCourses is asynchronous
What this means is that courses won't necessarily be guaranteed to have a value when ngAfterViewInit is called and will likely then throw a similar error.
What I can suggest to solve this is the following:
<child-component
*ngIf="courses?.length"
[courses]="courses"
>
</child-component>
You won't then have to wait for ngAfterViewInit, and instead can just wait for ngOnInit.
ngOnInit(): void {
this.myControl.setValidators([
Validators.required,
forbiddenNamesValidator(this.courses)
]);
}
Comments
Passing a list from parent to child, should I use an observable/promise/array etc?
That's entirely up to you, I prefer using the async pipe when dealing with observables, because then I don't need to worry about unsubscribing from my subscriptions.
<child-component
[courses]="courses | async"
>
</child-component>
courses = this.service.listCourses()
I think there is no need to use get/set for the courses in Child Component as the list will not be changed
You don't necessarily need to even use a get/set when engaging with data that changes. Angular will update the #Input data for you, so you don't need worry about using a get/set unless you explicitly need that functionality.
Should I call the this.myControl.setValidators([]} and the filter method in the onInit or afterViewInit
There's no need to shift setting the validator into your afterViewInit, you don't need to wait for your Component's View to be initialized before you set the validator.
1st way: Add ngIf to check whether you have data or not.
<child-component [courses]="courses" *ngIf="courses.length > 0"> </child-component>
2nd Way : If you want use async then don't subscribe it in your component.
<child-component [courses]="courses$ | async" *ngIf="(courses$| async)?.length > 0"> </child-component>
Component:
courses$: Observable<any>;
this.courses$ = this.service.listCourses().pipe(shareReplay());

How can I access a property of a step component from the parent page of a Vue stepper?

Not very experienced with Vue, and am struggling to fix an issue with an existing component made by a previous developer.
On the parent page, the stepper component is declared like this, with these as the names of the sub-components that make up the individual steps:
#Component({
name: 'switch-stepper',
components: {
Stepper,
StepIndicator,
FirstStep,
SecondStep,
ThirdStep
}
})
There's a property in ThirdStep that needs to be changed, either in the parent page or in FirstStep. The property in question is public, and declared like this:
#Prop({ default: true })
public myBooleanProperty: boolean;
However, inside the parent page, if I try this:
ThirdStep.myBooleanProperty
It is not recognised by the intellisense and is undefined. I've tried also creating a public method on ThirdStep that I can call to use ForceUpdate but public methods likewise seem to be inaccessible.
I've also tried setting the public property via a function in the parent page when the step is created:
<third-step :page="page"
:step="steps[4]"
:myBooleanProperty="setMyBooleanProperty()"
v-show="currentStep === steps[4]">
</third-step>
But as far as I can tell this is only called once when the step is created and never accessed again.
What can I do to set the property of this child step via other components in the stepper?
There's a property in ThirdStep that needs to be changed, either in the parent page or in FirstStep. The property in question is public,
and declared like this:
If a property needs to be changed, it should be changed in the component which returns the property in the data property. By this, I mean, the component that has
data() {
return {
myBooleanProperty: false // or true. This is the local data that will be initialised and passed as props to the ThirdStep component
}
}
If this is in the parent page, change ThirdStep.myBooleanProperty to this.myBooleanProperty = *enter value*. The change of the value of myBooleanProperty can be done in a method, watcher, computed property etc. The reason ThirdStep.myBooleanProperty is not working in the parent component, is that each vue component is a Vue instance and ThirdStep cannot have access to an instance property in the parent component.
ParentComponent.vue
#Component({
name: 'switch-stepper',
components: {
Stepper,
StepIndicator,
FirstStep,
SecondStep,
ThirdStep
}
})
<third-step>
...
:myBooleanProperty="myBooleanProperty" // my boolean property is passed as props from the parent component, I assume
....>
</third-step>
When myBooleanProperty is changed where it is initialised (parent component, I assume), this will cause the value of the myBooleanProperty props, passed into the ThirdStep component, to change and there will be a re-render of the parts of the ThirdStep component that use myBooleanProperty props.

how to pass dynamic data from parent to child in angular2+

As we know , in angular parent to child communication happened using #Input decorator but how to pass dynamic data from parent to child.for example 'name' property is defined in parent and it's value changes dynamically and updated value of 'name' we are going to using in child component so how to achieve it without services.
you can watch your input changes in the child component with ngOnChanges lifecycle
in your child component you should implements OnChanges
ngOnChanges(changes: SimpleChanges): void {
for (let propName in changes) {
let change = changes[propName];
if (propName === "YOUR_INPUT_NAME") {
this.YOUR_INPUT_NAME = (change.currentValue);
}
}
}
You can make the use of service to get the updated value in child when it gets changed in parent.
For that you need to declare the variable in the service of type observable.
your myService.service.ts would be having the following code:
#Injectable()
export class myService {
myInput:BehaviorSubject<boolean> = new BehaviorSubject(false)
}
now from your parent component change the value of this myInput by using:
myServie.myInput.next(true)
and subscribe this variable in your child component by using:
myService.myInput.subscribe((data)=>{console.log(data)})
In both the parent and child component add the line private mySerivce:MyService in the arguments of constructor.

Host component changing behavior of child component

Suppose I have the following component Child.
#Component({...})
class Child {
#Input() mySpecialFlag: boolean;
}
Child can either be used as a stand-alone component, or can be wrapped by a Special component, which "scopes" the behavior of Child. When Child is used stand-alone, the user can choose to either set [mySpecialFlag] to true or false. When Child is used within Special, I want the Special component to force the input [mySpecialFlag] to be true, via something like the following:
#Component({...})
class Special {
#ContentChild(Child) child;
...
ngAfterContentInit() {
this.child.mySpecialFlag = true;
}
}
The issue with the above is that it sets [mySpecialFlag] too late. I want to be able to set this flag to true before any of the child's lifecycle methods have run, or at least before the child's ngAfterContentInit has ran. Is this possible?
Setting the flag in ngOnInit of the parent appears to work: the flag is set before ngOnInit of the child component.
ngOnInit() {
this.child.mySpecialFlag = true;
}
See this stackblitz for a demo.
An alternative method to access the child component as early as possible is to associate the #ContentChild decorator to a getter/setter property, and to set the child flag in the setter:
export class ParentComponent {
private _child: ChildComponent;
#ContentChild(ChildComponent) get child(): ChildComponent {
return this._child;
}
set child(val: ChildComponent) {
this._child = val;
this._child.mySpecialFlag = true;
}
}
See this stackblitz for a demo. Please note that the private variable _child and the getter are not needed to set the flag. I added them in case you need to refer to the child component for other purposes.
I believe you could use ngOnInit method. Refer to this link explaining angular Lifecycle hooks: https://angular.io/guide/lifecycle-hooks

How to pass data from parrent constructor to child component in angular 2

I have a angular 2 page where i need to show 2 different components using same array of data from external API. Parent is regular component, and child is shared among several other components using same functionality.
In parent component class i have output property declared:
public weatherList: WeatherForecast[];
#Output() public weatherListData: any;
Inside constructor of parent component, i populate weatherListData property with data from an external API
http.get(url)
.subscribe(result => {
this.weatherList= result.json() as WeatherForecast[];
this.weatherListData = this.weatherList;
});
and i'm using it inside parent template with success, something like: {{ weatherList.someValue }}
Also, inside parent component template, i have a call to a child component
<daily-temperature-chart [weatherListData]='weatherListData'></daily-temperature-chart>
In child component class i have declared property
#Input() weatherListData: any;
but, when i try to access weatherListData property in constructor, or init of child component, i get undefined result.
EDIT: I have played with console.log() and noticed that child component Constructor and OnInit() methods return before http.get() from parent component. Maybe this is problem, but i'm still new to angular and can't tell.
Can someone point me how to solve this?
You've a service call so you can't go for constructor or OnInit because component initialization is not dependent on your service call for this situation angular provides OnChanges whenever your input value is updated OnChanges fired.
ngOnChanges(changes: any){
console.log(changes);
console.log(this.weatherListData);
}
OnChanges passes as well an argument which informs about the current state and pervious state now you are able to use input values. If your components are bases on input and input is based on any other operation you can handle it in this block.

Categories

Resources