In Angular, components expose output properties that use EventEmitters to fire an event. However, sometimes we may want to cancel or prevent the emission based on logic provided by the parent component. Out-of-the-box Angular does not provide a mechanism for canceling events. How can we enable our parent component to cancel events emitted by a child component?

Let’s first dive into a concrete example. Suppose you’re building a tab control component that allows the user to display different content within tabs.

The tab component owns both the display of the projected content and navigation of the selected tab with an architecture similar to Angular Material Tabs.

<tab-group>
    <tab label="my label">
    	<!-- tab content -->
    </tab>
</tab-group>

See a working version of the component on StackBlitz.

Now the consumer of the component wants to prevent the tabs from navigating when the user has stale or unsaved data. How can we provide the consumer cancellation hooks to make this happen?

Using an Input Property

Let’s update the tab-group component to take an input property to control the cancelation event. This property is named canActivateTab, and will accept a function that returns a boolean value. Also, by default, if the consumer does not pass a value to the canActivateTab Input, we will default to true. When a tab is selected, we will invoke the canActivateTab function to see if the consumer wants to cancel the event.

@Component({
  selector: 'tab-group',
  templateUrl: './tab-group.component.html',
  styleUrls: ['./tab-group.component.css']
})
export class TabGroupComponent {

  // Additional tab group component code omitted

  @Input()
  canActivateTab = () => true;

  _onSelectTab(tab: TabComponent) {
    if(this.canActivate()) {
      this._setTab(tab);
    }
  }

}
tab-group.component.ts

To use the canActivateTab property, the consumer of our tab-group component provides a function as an input.

<tab-group [canActivateTab]="_onCanActivateTab">
    <tab label="my label">
    	<!-- tab content -->
    </tab>
</tab-group>
app.component.html
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  _onCanActivateTab() {
    // insert conditional tab switching logic
    return true;
  }
}
app.component.ts

At first glance, this looks like a great solution! Consumers can can easily create a function that returns a boolean value to control the behavior. However, you will quickly realize that within your _onCanActivateTab function, you no longer have access to other properties of the class.

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  private cancelTab = true;

  _onCanActivateTab() {
    return this.cancelTab; // ❌ Returns undefined!
  }
}

Since the tab-group component invokes the provided function, the context of “this” is incorrect. (read more on this)

In order to ensure the context of “this” is set to our AppComponent class we either need to invoke the bind method passing the correct context of “this” (the component class).

<tab-group [canActivateTab]="_onCanActivateTab.bind(this)">
    <tab label="my label">
    	<!-- tab content -->
    </tab>
</tab-group>

Using bind isn’t a deal-breaker by any means and is the solution to pass the proper context. However, I dislike that the consumer of my component needs to have an understanding of JavaScript’s “this” context. While an essential part of the JavaScript language, we’re trying to build easy to use components that just work within the Angular ecosystem. With that in mind, let’s try another solution.

Input Property StackBlitz Demo

Using an Output Property

Instead of relying on an input property, what about an output property?

Let’s modify the tab-group component to output an event when the user attempts to change the tab content. First, create an interface that contains a cancel property.

export interface TabActivateArgs {
  cancel: boolean;
}

Next, add a new Output that emits the TabActivateArgs.

@Component({
  selector: 'tab-group',
  templateUrl: './tab-group.component.html',
  styleUrls: ['./tab-group.component.css']
})
export class TabGroupComponent {

  @Output()
  canTabActivate = new EventEmitter<TabActivateArgs>();
  
  _selectTab(tab: TabComponent) {
    const activateArgs = {cancel: false};

    this.canTabActivate.emit(activateArgs);

    if(activateArgs.cancel) {
      return;
    }

    this._setTab(tab);
  }

}
tab-group.component.ts

When the user attempts to switch tabs, we will emit the canTabSwitch output property. The consumer of the component binds to this event within the parent component and updates the cancel property to true to cancel the event.

<tab-group (canTabActivate)="_onCanTabActivate($event)">
    <tab label="my label">
    	<!-- tab content -->
    </tab>
</tab-group>
app.component.html
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  _onCanTabActivate(args: TabActivateArgs) {
    // logic to determine if tab switching should be cancelled
    args.cancel = true;
  }
}

The output property works because, by default, EventEmitters are synchronous.

Thinking about the solution a little more, I was concerned this pattern would fall apart when using the component as a Web Component (Angular Elements). Good news! While EventEmitters are modified to dispatch a custom events, custom events dispatch synchronously!

If providing a mutable property to your consumer feels a bit awkward, consider providing a function cancel within the event arguments.

_selectTab(tab: TabComponent) {
   let cancelled = false;
   const activateArgs = {
     cancel: () => { cancelled = true; }
   };

   this.canTabActivate.emit(activateArgs);

   if(cancelled) {
     return;
   }

   this._setTab(tab);
 }
}

The consumer invokes this function to cancel the event.

To me, this is a much cleaner solution to the problem. No fancy knowledge of JavaScript required, just the built-in conventions of Angular.

Output Property StackBlitz Demo

Conclusion

While Angular does not provide an out-of-the-box pattern for canceling events, utilizing Output properties to facilitate this behavior feels like a cleaner solution.

Have another solution? I’d love to hear about it! Fork my StackBlitz and show me what you’re thinking.