a pleasant descent into madness

Tag: angular

crying in angular

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!

host angular web-app in wsl2, access from windows host, develop in vs-code

Or: How do i work in a modern, efficient and really really nice way?

The goal of this article is to set up a system where you will be running an angular web application in WSL2 and access it via a Browser on the Windows Host.
It’s pretty straightforward, because i prepared a little something.

Prepare following docker-compose.yml file (which you want to place somewhere on the same file-system level as your application, or above it, not below!)

services:
my-angular-app:
build:
image: cryptng/angular:node14ng13
volumes:
– ./my-angular-app:/app
ports:
– “4200:4200”
command: bash -c “npm install -f –loglevel verbose && ng serve –verbose — watch=true –host=0.0.0.0 –proxy-config proxy.config.json –disable-host-check”
volumes:
node_modules:
version: ‘3.5’

You WILL have to fix the indentation to match yml standards.

You might notice that we are using an existing image from the docker hub.
It’s actually a repository that i manage and maintain with a friend.
If you check it out in detail, you will notice that this images matches the latest
angular-web-development standards as per november of 2021 (nearing the end of the corona crisis, probably, maybe).

The image basically uses Node:14 as a base image, containing NPM and YARN, and installs Angular-CLI 13 and Angular-Core 13 on a local and global level, then exposes port 4200.

THIS IMAGE ASSUMES THAT THE DOCKER COMPOSE FILE IS ON THE SAME LEVEL AS THE APPLICATION DIRECTORY & THE APPLICATION DIRECTORY IS NAMED “MY-ANGULAR-APP”

Of course, you can easily modify the “ports” section of the compose file to change the port you want to serve to, the LEFT side is the host side.


That means that if you run this compose file with a valid application, you will be able to reach your application from a browser on the windows host via “http://localhost:4200”.
If you change the LEFT element from 4200 to 4201, the address will be “http://localhost:4201” independently from the actual EXPOSE command in the dockerfile providing the container.

bash -c “npm install -f –loglevel verbose && ng serve –verbose — watch=true –host=0.0.0.0 –proxy-config proxy.config.json –disable-host-check”

This command assumes you have a proxy.config.json in the application directory, remove the –proxy-config proxy.config.json part if you do not have a backend running on the windows host, else, create a proxy.config.json and configure it to lead to the windows host.

The npm command will install all dependencies whenever the docker-compose has been run.


Now, if you also want to be developing in visual studio code, install it in your WSL and run
“code path-to-your-app-directory/. “
it will open VS code in this directory, vs code will automagically understand that this is an angular application and will give you some prompts depending on its complexity and components.

If you now have a valid application in the “my-angular-app” directory, you can have fun and start developing.

Enjoy!

Fix “Unsupported platform for fsevents” in npm

When trying to install npm packages via “npm install”, you might encounter an error “EBADPLATFORM”

npm ERR! notsup Unsupported platform for fsevents@1.1.3: wanted 
{"os":"darwin","arch":"any"}

This Error is indicating that you are installing the fsevents module in another OS rather than Mac, but the fsevents module only works for Mac OS.
If you did not purposefully include fsevents in your package.json, this hints at a sub-dependency.

To fix this error, you can use following parameters in the npm install command:

npm install -f --loglevel verbose 

The loglevel will help you debug any further problems. “-f” will force npm to fetch remote resources even if a local copy exists on disk.

I was unable to obtain information on how this fix actually corrects the sub-dependency issue, i could guess, but that doesn’t really help anyone.

If anyone has an explanation that’s more than just guessing, please feel free to leave a comment!

© 2022 Yavuz-Support Blog

Theme by Anders NorenUp ↑