Calling Angular Component Functions from JavaScript

I recently had to work on an Angular application that used the Bootstrap library to show a popover. The popover content needed to have a link that would open up a dialog (using the Dialog component from Angular’s Material library) when clicked.

Putting the weird requirement aside for a moment, this kind of interaction posed a unique technical challenge. There were two things that needed to be done:

  • There would be a button on the UI. Clicking this button would open a popover with a link.
  • Clicking on the link in the popover would open a dialog.

The first part is very straight forward. Create a button and initialise the popover on it. This can be done in the ngAfterViewInit lifecycle hook:

public ngAfterViewInit(): void {
$('[data-toggle="popover"]').popover();
}

For the second part, the content of the popover has to be in HTML:

<button type="button"
class="btn btn-primary"
data-container="body"
data-toggle="popover"
data-placement="bottom"
data-html=true
data-content="<p>Click<a href='#' (click)='openDialog()'>here</a>...</p>">
Click to open
</button>

This doesn't work because the click handler that we attached was interpreted as a string and it doesn’t actually trigger anything on click. This is because we can only call “normal” JavaScript functions from the popover. For example, the following alert would work:

data-content = `<p>This is a <a href='#' onclick="alert('Thanks for clicking...')">link</a> you need to click</p>`

Similarly, we can call a function that would handle the click event. This click handler needs to be accessible to the popover content in the global execution context.

To do this, we first make the following changes in the component:

  1. Create a namespace for the functions we will create
declare global {
interface Window { MyCustomNamespace: any; }
}
window.MyCustomNamespace = window.MyCustomNamespace || {};
  1. Inject NgZone

The function that will be run from the global execution context will be running outside Angular's zone. We need to hook back into the zone in the code we write.

import { NgZone } from '@angular/core';
...
constructor( private zone: NgZone ){}
  1. Use a function in the created namespace as a click handler
<button type="button"
class="btn btn-primary"
data-container="body"
data-toggle="popover"
data-placement="bottom"
data-html=true
data-content="<p>Click <a href='#' onclick='MyCustomNamespace.openDialog()'>here</a>...</p>">
Click to open
</button>
  1. Define the function used as a click handler
window.ExternalPlannedNamespace.openDialog = () => {
// Since this function runs outside Angular's zone, we need to get back inside!
this.zone.run(() => {
// Put angular code that has to be called on click on the link in the popover here...
});
}

Of course this entire problem could have been avoided by not dropping the popover library into an Angular application in the first place. Using a wrapper like ngx-bootstrap would have been a better option.

However, this is an interesting hack to have in your toolbox if you ever want to hook back into an Angular context from “regular” JavaScript (especially when working with legacy codebases).

Reference: StackOverflow