Add element with RouterLink dynamically - javascript

When I put an anchor element in somewhere in an Angular component like this:
<a [routerLink]="['/LoggedIn/Profile']">Static Link</a>
everything is working fine. When clicking the link, the Angular router navigates to the target component.
Now, I would like to add the same link dynamically. Somewhere in my app I have a "notification component", its single responsibility is to display notifications.
The notification component does something like this:
<div [innerHTML]="notification.content"></div>
Where notification.content is a public string variable in the NotificationComponent class that contains the HTML to display.
The notification.content variable can contain something like:
<div>Click on this <a [routerLink]="['/LoggedIn/Profile']">Dynamic Link</a> please</div>
Everything works fine and shows up on my screen, but nothing happens when I click the dynamic link.
Is there a way to let the Angular router work with this dynamically added link?
PS: I know about DynamicComponentLoader, but I really need a more unrestricted solution where I can send all kinds of HTML to my notification component, with all kind of different links.

routerLink cannot be added after the content is already rendered but you can still achieve the desired result:
Create a href with dynamic data and give it a class:
`<a class="routerlink" href="${someDynamicUrl}">${someDynamicValue}</a>`
Add a HostListener to app.component that listens for the click and uses the router to
navigate
#HostListener('document:click', ['$event'])
public handleClick(event: Event): void {
if (event.target instanceof HTMLAnchorElement) {
const element = event.target as HTMLAnchorElement;
if (element.className === 'routerlink') {
event.preventDefault();
const route = element?.getAttribute('href');
if (route) {
this.router.navigate([`/${route}`]);
}
}
}
}

routerLink is a directive. Directives and Components are not created for HTML that is added using [innerHTML]. This HTML is not process by Angular in any way.
The recommended way is to not use [innerHTML] but DynamicComponentLoaderViewContainerRef.createComponent where you wrap the HTML in a component and add it dynamically.
For an example see Angular 2 dynamic tabs with user-click chosen components

Since angular 9, AOT is the default recommended way to compile angular projects.
Unlike JIT, AOT doesn't hold an instance for the compiler at runtime, which means you can't dynamically compile angular code.
It's possible to disable AOT in angular 9, but it's not recommended as your bundle size will be bigger and your application slower.
The way I solve this is by adding a click listener at runtime using renderer api, preventing the default behavior of urls and calling angular router
import { Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '#angular/core';
import { Router } from '#angular/router';
#Directive({
selector: '[hrefToRouterLink]'
})
export class HrefToRouterLinkDirective implements OnInit, OnDestroy {
private _listeners: { destroy: () => void }[] = [];
constructor(private _router: Router,
private _el: ElementRef,
private _renderer: Renderer2) {
}
ngOnInit() {
// TODO how to guarantee this directive running after all other directives without setTimeout?
setTimeout(() => {
const links = this._el.nativeElement.querySelectorAll('a');
links.forEach(link => {
this._renderer.setAttribute(link, 'routerLink', link?.getAttribute('href'));
const destroyListener = this._renderer.listen(link, 'click',
(_event) => {
_event.preventDefault();
_event.stopPropagation();
this._router.navigateByUrl(link?.getAttribute('href'));
});
this._listeners.push({ destroy: destroyListener });
});
}, 0);
}
ngOnDestroy(): void {
this._listeners?.forEach(listener => listener.destroy());
this._listeners = null;
}
}
You can find an example here : https://stackblitz.com/edit/angular-dynamic-routerlink-2
Obviously the method explained above work for both JIT & AOT, but If you are still using JIT and want to dynamically compile component (which may help solve other problems) . You can find an example here :
https://stackblitz.com/edit/angular-dynamic-routerlink-1
Used resources :
https://stackoverflow.com/a/35082441/6209801
https://indepth.dev/here-is-what-you-need-to-know-about-dynamic-components-in-angular

Combining some of the other answers - I wanted this as a Directive so I could target specific elements that are being innerHTML'd, but to avoid using querySelector (etc) to keep everything Angulary.
I also found an issue with the approaches above, in that if the href is a full URL (i.e, https://www.example.com/abc) feeding that whole thing to the router would result in navigating to /https.
I also needed checks to ensure we only router'd hrefs that were within our domain.
#Directive({
selector: '[hrefToRouterLink]'
})
export class HrefToRouterLinkDirective {
constructor(private _router: Router){}
private _baseHref = quotemeta(environment.root_url.replace(`^https?://`, ''));
private _hrefRe: RegExp = new RegExp(`^(https?:)?(\\/+)?(www\\.)?${this._baseHref}`, `i`);
#HostListener('click', ['$event'])
onClick(e) {
// Is it a link?
if (!(e.target instanceof HTMLAnchorElement))
return;
let href: string = e.target?.getAttribute('href')
.replace(/(^\s+|\s+$)/gs, '');
// Is this a URL in our site?
if (!this._hrefRe.test(href))
return;
// If we're here, it's a link to our site, stop normal navigation
e.preventDefault();
e.stopPropagation();
// Feed the router.
this._router.navigateByUrl(
href.replace(this._hrefRe, '')
);
}
}
In the above environment.root_url describes our base domain, and quotemeta is a rough implementation of a Perl-ish quotemeta function just to escape special characters.
YMMV and I've definitely missed some edge cases, but this seems to work fine.

Related

How do I add an attribute to a dynamically created element in Angular2+?

I have had a look at this but it does not work -- maybe because the elements I am targeting are dynamically generated inside a custom component (<c-tabs>).
I have also had a look at this but this wouldn't work, I think, as i am not able to touch the custom component's code.
Relevant Elements
HTML
<c-tabs #mainComponentTabs1 [items]="tabItems"></c-tabs>
I have tried the following ways to target it. None works.
Method 1. Using plain old Javascript:
ngAfterViewInit() {
let att = document.createAttribute("id");
att.value = "yada1";
document.querySelectorAll(".c-tabs .hydrated")[0].setAttributeNode(att);
}
I have tried to have the above in the ngOnInit and ngOnViewInit methods but that didn't help.
This method, oddly enough, works in the Chrome console window after the page has rendered. That is, the querySelected items get id attribute.
Method2. Using Angular's Renderer2. (Admittedly, I don't know how to get to the particular native elements that need the id's.
//first, declare target custom component element with viewchild:
export class MainComponent implements OnInit, AfterViewChecked, AfterViewInit {
#ViewChild('mainComponentTabs1', { static: true }) c_tabs1: ElementRef;
...
ngAfterViewChecked() {
//neither of these approaches works.
const c_tabs1El = this.c_tabs1.nativeElement;
this.renderer.setAttribute(c_tabs1El.items[0], 'id', 'dashboard-tab-link-search');
const c_tabs1El2 = document.querySelectorAll("button.c-item");
this.renderer.setAttribute(c_tabs1El2[0].nativeElement, 'id', 'dashboard-tab-link-dashboard');
}
...
}
I'm not sure what elements are you trying to target, if you're trying to target the elements by class or tag but I think the following code will help you:
export class MainComponent implements OnInit, AfterViewChecked, AfterViewInit {
// the static property must be true only if you are using it in ngOnInit
#ViewChild('mainComponentTabs1', { static: false }) c_tabs1: ElementRef;
...
ngAfterViewInit() {
const targetElements = this.c_tabs1.nativeElement.querySelectorAll('button.c-item')
targetELements.forEach((el, index) => this.renderer.setAttribute(el, 'id', `dashboard-tab-link-search-${index}`));
}
...
}
Don't touch the DOM directly.

RxJS: Mimic Angulars #Input() binding?

Im pretty new to Angular 8 and RxJS and stumbled upon an issue:
I make an angular app which relies heavily on an externally loaded THREE.js scene. Services handle those scenes.
So most of the time there is No HTML Template (only maintaining scene via js object) => no bindings ?
So I was thinking... is there a way to use Rxjs subjects/observables to achieve something like Input() binding?
thats what i basically want to
this.sub = myService.watch('window.deviceorientation')
.subscribe({next(x => { if(x) this.sub.unsubscribe; doStuff();})})
I want to continously check a certain object (any really), be notified as soon as it exists (or instantly if it already does). I bet there is some weird combination of RxJS Operators which can do exactly this?
(So basically its a little bit like AngularJS scope.$watch but to keep performance I'd of course clean up subscriptions.)
You can use the fromEvent rxjs creation utility to achieve the desired effect.
import { fromEvent } from 'rxjs';
orientation$ = fromEvent(window, 'deviceorientation');
const subscription = orientation$.subscribe((event)=>{
//do stuff
});
// when disponse is required
subscription.unsubscribe();
So I worked my way around this.
I created a root-level component that allows me to watch for specific changes on scope:
//HTML
<app-change-detector [watch]="window.anyObject" (onChange)="anyRootService.onObjectChanged($event)">
//TS
#Component({
selector: 'app-change-detector'
})
export class ChangeDetectorComponent implements OnChanges {
#Input() watch: any;
#Output() onChange = new EventEmitter<any>();
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.watch) {
this.onChange.emit(changes.watch.currentValue);
}
}
}

How can I make application title static after user update it though user page?

I am setting title on a template of the app component. The title has been updating by user input on the user page. How can available that title all over the app?
I have tried through local storage which works but does there any other way to make it available throughout the app?
Stackblitz URL: https://stackblitz.com/edit/angular-kfptvs
[Updates]
When I have implemented this same on my another app, I started getting error:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null: Bob'. Current value: 'null: Rob'.
The above error occurs when execute the below code:
this._userService.titleObs.subscribe((title) => {
this.title = title;
})
I have fixed through below code but can anyone suggest the better way to fix it?
this._userService.titleObs.subscribe(
title => setTimeout(() => this.title = title, 0)
);
Using the service for this is a good approach. For navigation, you should use router links instead of navigating via the <a> tag. This avoids a reload of the page.
As Maryannah said you used a service which is a good way to solve the problem. The reason it doesn't work for you might be because your service is under the temp folder. If you put a service in the root folder, all components will be able to access the same data.
The issue has been fixed though adding ChangeDetectorRef
import { Component, OnInit, ChangeDetectorRef } from '#angular/core';
constructor(
private ref: ChangeDetectorRef
){}
this._userService.titleObs.subscribe((title) => {
this.title = title;
this.ref.detectChanges()
})

How can I access DOM elements in angular

I am tetsing a template driven form in angular, just testing not validating it.
I have read that it can be done using a viewChild property but it seems to not work for me.
I create a reference like this in my one of my forms label:
<label #ref id=.. class=...>
And now in my component I do this:
#ViewChild('ref') ref:ElementRef;
So, I suppose I created a valiable of type ElementRef that is viewChild of my input. So now I can use ref in my tests.
Inside my tests I do this:
let ref: HTMLElement:
it(....=>
{
ref = fixture.debugElement.query(By.css('ref')).nativeElement;
fixture.detectChanges();
expect(ref.innerHTML)toContain('Name');// or whatever
}
)
Now consider that the test, html and component files are separated from one another.
I still get errors of nativeElemnt property cannot be read. eventhough I have imported ElemntRef.
Is this the right way to access the DOM elemnts?? Or this viewChild doesnt make a referece to my label??
And again can I use the ID to access the form elements? What I have read they use a reference with #.
Thanks!!
For direct access to DOM in Angular you can make judicious use of ElementRef
However Direct access to DOM elements is not a good practice because
it leaves you vulnerable to XSS attacks.
Your AppComponent
import {Component, ElementRef} from '#angular/core';
#Component({
selector: 'my-app'
})
export class AppComponent implements ngOnInit {
constructor(private _elementRef : ElementRef) { }
ngOnInit(): void
{
this.ModifyDOMElement();
}
ModifyDOMElement() : void
{
//Do whatever you wish with the DOM element.
let domElement = this._elementRef.nativeElement.querySelector(`#someID`);
}
}
Your HTML
<p id="someID"></p>

How to expose angular 2 methods publicly?

I am currently working on porting a Backbone project to an Angular 2 project (obviously with a lot of changes), and one of the project requirements requires certain methods to be accessible publicly.
A quick example:
Component
#component({...})
class MyTest {
private text:string = '';
public setText(text:string) {
this.text = text;
}
}
Obviously, I could have <button (click)="setText('hello world')>Click me!</button>, and I would want to do that as well. However, I'd like to be able to access it publicly.
Like this
<button onclick="angular.MyTest.setText('Hello from outside angular!')"></click>
Or this
// in the js console
angular.MyTest.setText('Hello from outside angular!');
Either way, I would like the method to be publicly exposed so it can be called from outside the angular 2 app.
This is something we've done in backbone, but I guess my Google foo isn't strong enough to find a good solution for this using angular.
We would prefer to only expose some methods and have a list of public apis, so if you have tips for doing that as well, it'd be an added bonus. (I have ideas, but others are welcomed.)
Just make the component register itself in a global map and you can access it from there.
Use either the constructor or ngOnInit() or any of the other lifecycle hooks to register the component and ngOnDestroy() to unregister it.
When you call Angular methods from outside Angular, Angular doesn't recognize model change. This is what Angulars NgZone is for.
To get a reference to Angular zone just inject it to the constructor
constructor(zone:NgZone) {
}
You can either make zone itself available in a global object as well or just execute the code inside the component within the zone.
For example
calledFromOutside(newValue:String) {
this.zone.run(() => {
this.value = newValue;
});
}
or use the global zone reference like
zone.run(() => { component.calledFromOutside(newValue); });
https://plnkr.co/edit/6gv2MbT4yzUhVUfv5u1b?p=preview
In the browser console you have to switch from <topframe> to plunkerPreviewTarget.... because Plunker executes the code in an iFrame. Then run
window.angularComponentRef.zone.run(() => {window.angularComponentRef.component.callFromOutside('1');})
or
window.angularComponentRef.zone.run(() => {window.angularComponentRef.componentFn('2');})
This is how i did it. My component is given below. Don't forget to import NgZone. It is the most important part here. It's NgZone that lets angular understand outside external context. Running functions via zone allows you to reenter Angular zone from a task that was executed outside of the Angular zone. We need it here since we are dealing with an outside call that's not in angular zone.
import { Component, Input , NgZone } from '#angular/core';
import { Router } from '#angular/router';
#Component({
selector: 'example',
templateUrl: './example.html',
})
export class ExampleComponent {
public constructor(private zone: NgZone, private router: Router) {
//exposing component to the outside here
//componentFn called from outside and it in return calls callExampleFunction()
window['angularComponentReference'] = {
zone: this.zone,
componentFn: (value) => this.callExampleFunction(value),
component: this,
};
}
public callExampleFunction(value: any): any {
console.log('this works perfect');
}
}
now lets call this from outside.in my case i wanted to reach here through the script tags of my index.html.my index.html is given below.
<script>
//my listener to outside clicks
ipc.on('send-click-to-AT', (evt, entitlement) =>
electronClick(entitlement));;
//function invoked upon the outside click event
function electronClick(entitlement){
//this is the important part.call the exposed function inside angular
//component
window.angularComponentReference.zone.run(() =
{window.angularComponentReference.componentFn(entitlement);});
}
</script>
if you just type the below in developer console and hit enter it will invoke the exposed method and 'this works perfect ' will be printed on console.
window.angularComponentReference.zone.run(() =>
{window.angularComponentReference.componentFn(1);});
entitlement is just some value that is passed here as a parameter.
I was checking the code, and I have faced that the Zone is not probably necessary.
It works well without the NgZone.
In component constructor do this:
constructor(....) {
window['fncIdentifierCompRef'] = {
component = this
};
}
And in the root script try this:
<script>
function theGlobalJavascriptFnc(value) {
try {
if (!window.fncIdentifierCompRef) {
alert('No window.fncIdentifierCompRef');
return;
}
if (!window.fncIdentifierCompRef.component) {
alert('No window.fncIdentifierCompRef.component');
return;
}
window.fncIdentifierCompRef.component.PublicCmpFunc(value);
} catch(ex) {alert('Error on Cmp.PublicCmpFunc Method Call')}
}
</script>
This works to me.
The problem is that Angular's components are transpiled into modules that aren't as easy to access as regular JavaScript code. The process of accessing a module's features depends on the module's format.
An Angular2 class can contain static members that can be defined without instantiating a new object. You might want to change your code to something like:
#component({...})
class MyTest {
private static text: string = '';
public static setText(text:string) {
this.text = text;
}
}
Super simple solution!! save component or function with an alias outside
declare var exposedFunction;
#Component({
templateUrl: 'app.html'
})
export class MyApp {
constructor(public service:MyService){
exposedFunction = service.myFunction;
}
at index.html add in head
<script>
var exposedFunction;
</script>
Inside exposed function do not use this. parameters if you need them you will have to use closures to get it to work
This is particularly useful in ionic to test device notifications on web instead of device

Categories

Resources