Write Compound Components with Angular’s ContentChild

Isaac Mann
InstructorIsaac Mann
Share this video with your friends

Social Share Links

Send Tweet
Published 6 years ago
Updated 6 years ago

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.

Chandan
Chandan
~ 6 years ago

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(() => {}) }

chihab
chihab
~ 6 years ago

@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.

Isaac Mann
Isaac Manninstructor
~ 6 years ago

@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?

chihab
chihab
~ 6 years ago

@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 ?

Isaac Mann
Isaac Manninstructor
~ 6 years ago

@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

Eugene Vedensky
Eugene Vedensky
~ 5 years ago

Perhaps a dumb question, but is @ContentChild meant to be used with a component that has an <ng-content></ng-content> template?

Isaac Mann
Isaac Manninstructor
~ 5 years ago

Not dumb. Yes @ContentChild gives a component a way to access items that are placed inside of its <ng-content></ng-content> area.

Christian Crowhurst
Christian Crowhurst
~ 5 years ago

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?

Christian Crowhurst
Christian Crowhurst
~ 5 years ago

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

Markdown supported.
Become a member to join the discussionEnroll Today