Refactor Angular Component State Logic into Directives

Isaac Mann
InstructorIsaac Mann
Share this video with your friends

Social Share Links

Send Tweet
Published 6 years ago
Updated 5 years ago

Allow the base toggle to be a tag (<toggle>) or attribute (<div toggle>). The <toggle> component has become less opinionated about the view, but has now taken on some responsibilities managing state. We’ll decouple the state management piece by moving it into the toggleProvider directive. The toggleProvider directive provides state for all the <toggle-off>, <toggle-on> and <toggle-button> components inside it.

Instructor: [00:00] Here, we have a toggle compound component that leverages dependency injection to communicate between the different parts of the compound component system.

[00:09] If we look at the toggle component definition, you'll notice that the template is simply an ng-content tag, which implies that we don't even need a template at all. A component without a template is called a directive. Let's refactor this toggle component into a toggle directive.

[00:28] Remove the template. Change it from a component to a directive. We'll update the selector to use the existing HTML tag selector or use an attribute selector.

[00:42] After going through and changing all the toggle component references -- the toggle directive references -- we see it's working just as it was before. We can also change the tag here to a div and put the toggle here as an attribute. It's also working as it was before.

[01:05] Another thing you may notice about this toggle directive is that it's actually doing two jobs. The first job is receiving input and sending output to the parent component. The second job is to provide the toggle state to all the child components.

[01:23] Any time you have two jobs being accomplished by one directive, you have an opportunity to split those jobs out into two different directives. Let's write a toggle provider directive to handle the communication with the child components.

[01:37] We'll create a toggle.toggle provider file, rider.directive.ts. We paste in some scaffolding here for a directive, call it toggle provider directive. Let's give it the same selectors as the toggle directive.

[01:59] What this will do is, any time the toggle directive is instantiated, Angular will also put on the toggle provider directive. We'll have both directives in the same HTML element. In order to reference the toggle directive, we are going to inject it like so.

[02:19] We actually only want to inject the toggle directive that's on the exact same element. Angular provides us a handy decorator called host, which says, "Only look on this element. Don't look at ancestors."

[02:33] We'll save a reference to this toggle directive, set it to the injected toggle directive. Now, let's update all the child components to use the toggle provider instead of the toggle directive directly. Now that we have all that set up, our button works exactly as it did before. Wait, why did we do that? Let's look at that toggle provider directive again.

[03:11] Since the toggle provider directive is decoupled from the actual toggle directive itself, we can reference it directly, giving it its own selector and giving input with the same name that will allow us to specify which toggle directive we want to provide.

[03:38] To do this, we'll implement the onChanges lifecycle hook, add ngOnChanges, and when the change is coming, we will look to see if the toggle provider is one of the changes. If so, we will set the this.toggle to the this.toggle provider input if it's provided, or the this.toggle directive that is on the host element.

[04:28] One thing we need to watch out for is if we use this selector to reference the toggle provider, we will not have a toggle directive on the host element. We need to tell Angular that that's OK by saying this is optional.

[04:42] We also need to give Angular a way to get a reference to this directive from the template, by giving it a name to export as. We'll call it toggle provider, and we'll export the toggle directive as toggle. Now we can do some pretty interesting things in our template.

[05:07] Let's say you wanted a different part of your template to be associated with the same toggle state, so something else here. These two toggle components are completely independent of each other, but if we use a toggle provider directive here, and set that to the first toggle, lets' make that an actual reference.

[05:38] We have to name the first toggle here. Now both toggles are referencing the same toggle state.

Jessy
Jessy
~ 6 years ago

I haven't seen this syntax usage before const {toggleProvider} = changes; Is there anything special about wrapping toggleProvider in curly brackets?

-- edit: I've found it in the documentation of TypeScript :) pretty neat! https://www.typescriptlang.org/docs/handbook/variable-declarations.html#object-destructuring

Isaac Mann
Isaac Manninstructor
~ 6 years ago

Yep, it's actually a feature of ES6. The equivalent ES5 would be:

var toggleProvider = changes.toggleProvider;
felikf
felikf
~ 6 years ago

Hi Isaac, thank you for the course...

I have some questions inlined in code:

export class ToggleProviderDirective implements OnChanges {
 ...
  // When this assignment happens? I would expect that assignment in the constructor.. I am not sure if this is the same
  toggle: ToggleDirective = this.toggleDirective;

  constructor(@Host() @Optional() private toggleDirective: ToggleDirective) {}

  ngOnChanges(changes: SimpleChanges) {
    const {toggleProvider} = changes;

  // this is kind of tricky for me
  // you are testing `toggleProvider` (passed as function argument) - but then you assign 
  // `this.toggleProvider` - class member - injected; is it that toggleProvider === this.toggleProvider ?

    if(toggleProvider) {
      this.toggle = this.toggleProvider || this.toggleDirective;
    }
  }
}

And why do we need the OnChanges lifecycle hook at all? We are injecting the @Input() toggleProvider: ToggleDirective so it should be already available as class member.

I see .. we dont need OnChanges but the @Inject is available later, not in the constructor. I think this is more implicit:

constructor(@Host() @Optional() private toggleDirective: ToggleDirective) {
    this.toggle = toggleDirective;
  }

  ngOnInit() {
    this.toggle = this.toggleProvider || this.toggle;
  }
Isaac Mann
Isaac Manninstructor
~ 6 years ago

@felikf

  1. toggle: ToggleDirective = this.toggleDirective; is exactly the same as doing this.toggle = this.toggleDirective at the end of your constructor.

  2. The toggleProvider that I pulled off the changes object is a SimpleChange type. Which means it has a previousValue property and a currentValue property. At the line you're referring to toggleProvider.currentValue === this.toggleProvider.

  3. The reason I used ngOnChanges instead of ngOnInit is to support the ability to change the toggleProvider input after the component has been initialized. If you used ngOnInit, you would need to make a new component any time the provider changed. I've had to refactor ngOnInit logic into ngOnChanges enough times to choose ngOnChanges by default any time I'm using @Inputs in the logic.

I think I answered all your questions. Let me know if I missed anything.

felikf
felikf
~ 6 years ago

Isaac, thank you for taking time to clarify all my questions.

Andrew Davis
Andrew Davis
~ 5 years ago

Why am I getting this error?

ERROR TypeError: Cannot assign to read only property 'toggleProvider' of object '[object Object]'

Andrew Davis
Andrew Davis
~ 5 years ago

Why am I getting this error?

ERROR TypeError: Cannot assign to read only property 'toggleProvider' of object '[object Object]'

It seems I was missing a decorator...

Alex
Alex
~ 5 years ago

In the toggle directive, you emit the state. Why is emitting an event a better option than using 2-way binding?

Kyle Westendorf
Kyle Westendorf
~ 3 years ago

Playing around with the code...why is it that even though I remove the toggle directive that is being referenced, the toggle buttons still are in sync and work? https://stackblitz.com/edit/adv-ng-patterns-03a-compound-comp-inject-parent-m9flqg?file=app/app.component.html

Markdown supported.
Become a member to join the discussionEnroll Today