Allow the user to control the view of the toggle component. Break the toggle component up into multiple composable components that can be rearranged by the app developer.
Instructor: [00:00] This toggle component is exposing its internal state to us through inputs and outputs. However, it doesn't give us any control over what's actually displayed inside of the toggle component.
[00:10] We're going to try to remedy that using a pattern called compound components. This is where multiple components work together to give the parent component more control over how the whole system works.
[00:25] We will add three child components to the toggle component -- toggle button, toggle-on, and toggle-off. Toggle button will display the switch that toggles on and off.
[00:43] Toggle-on, we'll use to display some message when the toggle is on. Toggle-off will display a message when the toggle is off. Now all of these have red squiggles on them because we haven't defined them yet, so let's go do that.
[00:58] First, we'll create the toggle button component. This component looks very similar to the toggle component that we defined previously.
[01:08] Next, we'll make the toggle-on component. The toggle-on component just has one input and displays its contents if that input is true. Finally, we'll make the toggle-off component. The toggle-off component also takes one boolean input and displays its content only if that value is not true.
[01:39] We'll also need a toggle module to bundle up all of these components together. That simply takes the toggle component, toggle-on/off, and toggle button declares them here. This switch component is just the UI that displays our toggle. Then it exports them so that other components can use them.
[02:04] All the complexity of this compound toggle component is in this toggle component here. I'm going to get rid of this toggle component HTML file since we don't really need it and change the template to the inline here, and set it to ngContent, which simply displays whatever is inside of the root title component tags.
[02:30] Since the switch itself is no longer on this toggle component, we can remove the on-click function. We need to get a reference to all three child components. We'll use the ContentChild decorator and get a reference to the toggle-on component that's inside of this.
[02:52] We'll call this toggle-on, so type toggle-on component. We'll do the same thing for toggle-off and toggle button. We'll fix up our imports. Now we have referenced all three of those child components.
[03:25] Anything referenced with the @ContentChild decorator is not guaranteed to be present when the component is initialized, so we need to use a Lifecycle hook of AfterContentInit. That allows us to use the ngAfterContentInit function in our component.
[03:48] Inside this function, we know that any content of the component has been processed, so we now have a real reference to the toggle button. When the toggled output is emitted from the toggle button, we can get a subscription to that.
[04:13] When that value comes through, we can update our current state and emit to the parent. We also need to update the state of all of our child components.
[04:42] Let's include the toggle module in our app module out here. Import toggle module from toggle.module. We don't need the switch component, and we don't need that. Let's add the toggle-on module here. Let's go back here. Now we have toggle button and toggle-on, toggle-off. We can toggle this button on and off. The toggle component works.
[05:22] Now we have the flexibility to reposition these child components and add HTML, or other Angular components if we want to.
@Isaac is there a way to query content child component by a directive so that we could have access to its hosting component instance:
@ContentChild(ToggleOnDirective) toggleButton: ToggleButtonInterface
This way we could declare our own toggle-button like components.
<toggle (toggled)="showMessage($event)">
<toggle-on>On</toggle-on>
<toggle-off>Off</toggle-off>
<toggle-button-other appToggleButton></toggle-button-other>
</toggle>
Otherwise we would be limited to toggle predefined "sub components" and the only benefit would be being able to organize them in our view.
@chihab, I'm not sure what you're trying to do here. Where is this code?
@ContentChild(ToggleOnDirective) toggleButton: ToggleButtonInterface
Is that in the appToggleButton
directive? What are you expecting to show up in toggleButton
?
@Isaac my bad, ToggleOnDirective should be ToggleButtonDirective which is the selector in ContentChild.
Please consider this code:
export interface ToggleButtonInterface {
reset();
}
toggle-button-other.component.ts
@Component({
selector: 'toggle-button-other',
...
})
export class ToggleButtonOtherComponent implements ToggleButtonInterface {
...
reset() {
}
}
toggle-button-awesome.component.ts
@Component({
selector: 'toggle-button-awesome',
...
})
export class ToggleButtonAwesome implements ToggleButtonInterface {
...
reset() {
}
}
app.component.html
<toggle>
...
<toggle-button-other appToggleButton></toggle-button-other>
</toggle>
<toggle>
...
<toggle-button-awesome appToggleButton></toggle-button-awesome>
</toggle>
toggle.component.ts
...
@ContentChild(ToggleButtonDirective) toggleButton: ToggleButtonInterface
ngAfterContentInit() {
this.toggleButton.reset(); // the problem here is that we get ToggleButtonDirective instance, how to get the instance of the hosting component ?
}
...
The question is how to specify to ContentChild right above how to get ToggleButtonOtherComponent or ToggleButtonAwesome instance instead of ToggleButtonDirective instance without explicitly naming class name as we don't konw what to expect. How to query the component hosting ToggleButtonDirective ?
@chihab, I don't think you can do exactly what you want.
One possible workaround is to have the two types of buttons extend
a parent class and then inject that parent class.
Another option is to use template references in your template:
<toggle-button-other appToggleButton #mybutton></toggle-button-other>
And use that string to find the button:
@ContentChild('mybutton') toggleButton: ToggleButtonInterface
More info: https://blog.angularindepth.com/handle-template-reference-variables-with-directives-223081bc70c2
Perhaps a dumb question, but is
@ContentChild
meant to be used with a component that has an <ng-content></ng-content>
template?
Not dumb. Yes @ContentChild
gives a component a way to access items that are placed inside of its <ng-content></ng-content>
area.
There's a problem with querying for the toggle-button using ContentChild
. In essence the query is not "live. so whenever the toggle-button is destroyed and recreated, the toggle component breaks.
Here's a demo of the problem: https://stackblitz.com/edit/adv-ng-patterns-02-compound-components-problem?file=app%2Fapp.component.html
Try unchecking then checking the "Create/destroy ToggleButton". And then use the toggle button. The component is broken.
I had a go at fixing it using ContentChildren
and a dose of observables. See here: https://stackblitz.com/edit/adv-ng-patterns-02-compound-components-fixed
My solution is "ok" but considerably more code. Not sure there is any simpler solution - what do you think?
just watched the next video. much better solution than trying to orchestrate things from the parent. Solves the problem I identified with much less code than my solution - nice
This video has a typo as shown below which kept me guessing where the toggled is defined. Checked the stackblitz to confirm which has correct one.
ngAfterContentInit { this.toggleButton.toggle(d).subscribe(() => {}) }