Or: building a confirm external webpage modal dialog with angular directives

Or: angular global directive for links

Or: get target url from pointerevent

Well, as the title suggests, i had a pretty duckin’ long day so i’ll make this as short as possible given the complexity of my research.


I’m going to explain what i was doing, and how i solved the problem, at the same time, i want to mention that i come from the world of EmberJS, where things are easy, nice, beautiful and also horrifying at the same time.. still easier to handle than googles product in my eyes, but i digress.

The task was to have a modal pop-up on every external link on an angular app.
My first thought was to add a click handler to external links only, but that’s obviously a dumb idea so i didn’t even start with that.

I looked up a solution to my problem and quickly found @Directives in Angular.
They sounded like the real thing, i implemented an externalUrlPrompt directive and generated a component to show inside a modal.
Hooked up MatDialog with the component and put the directive on a link to check it out.

What happened?
If you click the link, the dialog will pop up, but the click will still execute and open the external website in a new tab, hiding the popup.
Not ideal, so what was the problem?

I needed the “click” event, and stop it from executing.
Looking into the documentation, i found “HostListeners” do exactly what i need, they’ll listen to a specific event executed on the directive.. beautiful.

 @HostListener(‘click’, [‘$event’]) onClick($event:Event)

{
//my event is named $event because angular requires typescript because
//[REDACTED]

//anyway, you’ll want to stop the event executing by preventing its default action
//like so:

  $event.preventDefault();

//and then run the modal

 const dialogRef = this.dialog.open(ExternalUrlDialogComponent);  return dialogRef.afterClosed().subscribe(result => {

//i return the result here

}


}

Now, the next problem is that if the dialog result is “true”, i want to continue to the target page, but i prevented the event from propagating and there’s no “continue” function for that…
so i tried to clone the event before like this:

 const  originalEvent = event;

 const cloned = new PointerEvent('click', event);


And in the case the dialog result is true, i’d just execute

originalEvent.target?.dispatchEvent(cloned);  


Looks fine, but trying it out will lead to an endless loop,
the manually triggered clone-event will just ping the HostListener and restart the whole ordeal.
Well.. let’s continue, shall we?

I needed a way to navigate to the target page, so i needed the target page url…
turns out that the event actually contains that.

If you cast the $event.target into an actual “Element”, it’ll have a little more useful functionality.

 const target = $event.target as Element;


now target has a “closest” function that you can call to look for a specific HTML tag.
This tag contains the “href” attribute which has the URL we are looking for.

 let uri = target.closest('a')?.href ?? '';

Now we are getting somewhere.

We have a directive that listens to clicks, it stops the click event, takes the URL content from the clicked element, and in case the user really wants to leave your beautiful website, he can be lead there now by calling

    window.location.href = uri;

if he does not, we don’t really need to do something, but to be clean, i still call

$event.stopPropagation();


Works like a charm, but considering the fact that this application is pretty big, it’s still a dumb idea to look up every external url reference and add the directive, especially if i leave the position of developer at some point and people just forget to add the directive in the future.. no wai.

So what needs to happen?

Currently, the directive is on all external elements, alternatively, we could add the directive to ALL link elements by default, which is not a good idea, instead, it would be good to have the directive applied to all link elements on the whole application by default, and then filter for external websites.

How to we do that?

It’s easy, just change the directive selector to this:

@Directive({
  selector: 'a'
})

Now, the listener will be called on all calls of the “a” element, we just need to add a filter.
Since we already know how to retrieve the URL from the target, we can use the application host name to filter for the host, and if needed, manually add more allowed hosts.

    let uri = target.closest('a')?.href ?? '';
    if(!uri.includes(location.hostname))
    {
      this.showModal($event);
    }

Now, location is a global variable which provides the hostname, if the URL does not contain the hostname, it’s most likely external.
You might want to extend that to a list, anyway, this is a good solution.

Now let me show the whole thing so people have something to copy-paste, i will not be including the logic for the dialog, because i might do another post especially on how to integrate material dialogs with components, but i’m sad, and tired now.


import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { MyOwnModalComponent} from 'redacted';
import { Directive } from '@angular/core';
import { HostListener, Component } from "@angular/core";


@Directive({
  selector: 'a' //global a selector, yay
})
export class MyUrlPromptDirective {

  constructor(public dialog: MatDialog) {}

   @HostListener('click', ['$event']) onClick($event:Event){


    const  oEvent= $event;
    const target = oEvent.target as Element;


    let uri = target.closest('a')?.href ?? ''; //verify if this always exists
    if(!uri.includes(location.hostname))
    {
      this.showModalDialog($event);
    }




}

 
showModalDialog(event:Event) {

  const  oEvent= event;
  const target = oEvent.target as Element;


  oEvent.preventDefault();



  const dRef = this.dialog.open(MyOwnModalComponent);
  return dRef.afterClosed().subscribe(result => {
    return Promise.resolve(result).then(function(dRes){
      if(dRes) //if user wants to leave
      {

        let uri = target.closest('a')?.href ?? '';
        window.location.href = uri; //navigate to target

      }
      else //kill event
      {

        oEvent.stopPropagation();
      }

    });
  });
}

}

It’s my pleasure to help you out, kind stranger.
And it would be much appreciated if you’d show some credit when you copy my stugg, and especially leave some encouraging comments.

I feel like shouting from the mountaintops into an empty void, but i see high numbers of visitors on this blog, so if you’re not a bot, why not leave a complex mathematical problem, a logical riddle, or just some constructive criticism?

Enjoy!