a pleasant descent into madness

Month: November 2021

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!

Docker build fails because it has no internet

Or: Nuget Restore fails on docker build (that was my case, yay!)
Or: The command ‘cmd /S /C dotnet restore returned a non-zero code: 1 in Docker build

First of all, i found a temporary solution on Stackoverflow

Try this

docker network ls

This should list the networkIDs try running the docker build

docker build --network=<networkid> tag=<tag>

try the above with all the network listed it will work with one, that’s the host network.

(In my case, i used the ID of the NAT Network

Anyhow, temporary solutions are temporary, so i kept looking for something better.

And according to this article on CodeBucket the reason for this problem is that the docker DNS is broken, and it’s possible to set the DNS server in the docker configuration.

You’ll need to open /etc/docker/daemon.json on LINUX machines and alternatively open C:\ProgramData\docker\config\daemon.json on Windows machines.
When using Docker for Windows, you can alternatively just use the Docker for Windows UI by right clicking on the Docker Icon in the Taskbar, opening settings and navigating to “Docker Engine”

Then you’ll need to hard-config your DNS Servers by adding this line into the JSON:

“dns”: [“10.1.2.3”, “8.8.8.8”]

A fresh install with this line added will look something like this

{
“experimental”: false,
“dns”: [“10.1.2.3”, “8.8.8.8”]
}

Et viola, the problem is fixed.
Next time your build should work “out of the box”

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!

materialize.css & parallax

IMPORTANT:
I’m explaining this problem from an “ember” point of view, still, this solution fits multiple frameworks and is a general fix to get materialize parallax working – stay tuned!

If you know web frameworks, you know the dreaded word

DEPRECATED

from any log at any time, everywhere!

Some time ago i encountered a familiar error message while updating an ember project:

using this.$() in a component has been deprecated, consider using this.element
[deprecation id: ember-views.curly-components.jquery-element] 

Ember 3.9 deprecation (2019)

To read a detailed description on the reason this deprecation was introduced and the details on upgrading on it, please click the link in the quotation, it’ll lead you to deprecations.emberjs.com

The actual reasoning behind this change is not relevant for the fix that i’m going to introduce.

Anyhow, this change affected the parallax background(s) on my webpage, it didn’t break anything, but it would surely introduce a problem on the long run, so i was going to update the deprecated line.

The line looked like this:

 this.$(‘.parallax’).parallax();

The full deprecation warning looked like this:

deprecate.js:136
DEPRECATION:
Using this.$() in a component has been deprecated, consider using this.element [deprecation id: ember-views.curly-components.jquery-element]
See https://deprecations.emberjs.com/v3.x#toc_jquery-apis for more details.

I looked all around and found a solution on the original materialize documentation (and copies of this solution on the StackOverflow)

So i applied the “fix” and replaced my line with this:

 document.addEventListener('DOMContentLoaded', function() {
    var elems = document.querySelectorAll('.parallax');
    var instances = M.Parallax.init(elems, options);
  });

The above code implies that you somehow import “M” from materialize css, which would add this entry to the top of the page:

import M from ‘materialize.css’;

And you know what?

It didn’t work.
I didn’t get an error, and i didn’t get a parallax.
The page was just showing no background…

Next thing i did was contacting a good friend to ask him about his opinion.
He told me, first of all, that materialize.css does not have to be imported anymore on the latest versions of ember, it’s globally injected, automagically.

So ok, we throw out the import.
The code still doesn’t work.

He tells me that it’s not a good idea to add an eventListener like this in ember, unsure if this listener like this would actually be triggered , prefering that we use the ember native method “later” to execute the code at the end of the runloop, when the dom has been build.

So we import “later” and change the code, like following:

import { later } from ‘@ember/runloop’;


later(()=>{         
var elems = document.querySelectorAll(‘.parallax’);         
window.M.Parallax.init(elems, options);});

nope, still a white background, nothing parallaxy going on here.

So then, something caught our eyes..
what’s that “options” variable doing there?
Where does it come from?
It’s never been introduced, it’s undefined, and i assumed it would be passed on as undefined, not having an effect on further execution.

Some “stepping into” later, we notice that, since the options variable is undefined, the initialization code is never executed by materialize.

We change the code like following:

import { later } from ‘@ember/runloop’;


later(()=>{         
var elems = document.querySelectorAll(‘.parallax’);         
window.M.Parallax.init(elems);});

Passing no options, alternatively, you may pass an empty hash like so “{}” as second parameter.

Bam, fixed. Parallax’es going for you.


ENJOY!

© 2024 Yavuz-Support Blog

Theme by Anders NorenUp ↑