I'm trying to call a function defined within an angular component.ts from another javascript library
The library supports a snapin, where I can define HTML Elements, which will be rendered (they are not part of angular).
Here is my html (not in a component's template)
<button onclick='ping()'>PING Button</button>
<button onclick='pong()'>PONG Button</button>
How can I, from the html above, call the pong component method defined my component.ts
import { Component, OnInit, AfterViewInit, Inject, ChangeDetectionStrategy, ChangeDetectorRef } from '#angular/core';
function ping() {
alert('PING PING');
}
#Component({ ...
})
export class Component implements OnInit, AfterViewInit { ...
pong() { //This is the method I want to call from outside of the component's template
alert('pong pong');
}
}
I tried, but it will not work
<button (click)="pong()">PONG Button</button>
But I have no idea to call the "pong()" function normally
Thanks!
If you really need this, you can make the method available in the window object
component.ts
constructor()
{
window['pong'] = () => this.pong();
}
pong()
{
alert ('pong inside component')
}
Then in your html, you can use old style event handler
<button onclick="pong()">Pong</button>
Here is a stackblitz demo
Note: If you have several instance of the same angular component implementing this solution, you'll only have once instance of the method. You could save them all to an array if needed, but you'll need to know which one to call
I guess you want to use a non-angular library from angular, which has a global callback. Be warned that this can lead to problems, because you have to manage the lifecycle of the non-angular thing from angular.
From the angular template, you can only call methods on the component class, and you can't call a global callback. You can however create a method on the component class, and call the global callback from there.
There's one more thing before that's possible: typescript doesn't know about your global callback, so you have to explictily declare it, see the example. This tells typescript that there's something that is created outside of typescript, so it will let you call it.
import { Component } from '#angular/core';
declare const libMethod: (any) => any;
#Component({
selector: 'my-app',
template: `
<button (click)="myMethod($event)"></button>
`,
styleUrls: []
})
export class AppComponent {
public myMethod(param) {
libMethod(param);
}
}
If you plan to use that library from multiple of your angular components, then you might want to create a service, declare the global callback only in that, and create a method on the service. That way, this somewhat hacky declaration will not be littered all over your code, but contained to a single place. It also makes your life easier, if you upgrade/replace this library.
Answers to questions in the comments:
TBH I don't completely understand the situation. (Who calls the backend, when it returns the HTML? You from Angular, or the lib? Does the lib process the HTML? Or what does it do?)
Some suggestions: create a global singleton service, which puts up one of its methods to the window (dont' forget to bind it if you use this inside the method) to serve as a callback for the lib. When the lib calls it with the data, regardless of who/when actually triggered the lib to do its thing, the service stores the data in a subject, and the service also provides an observable of the data (maybe with a shareReplay(1) so that the consumers always get something).
With that, actually displaying the data is fairly easy, you can just use the async pipe, and not care about how/when the data got there in the first place, and don't have to sync the component's lifecycle with the service.
Also, you probably need to use https://angular.io/api/platform-browser/DomSanitizer#bypasssecuritytrusthtml but I am not sure about that, since I never had to inject HTML. Speaking about which...
Important security notice: if you inject HTML from outside of angular, and that is hijacked, you just opened up your page to all kind of nasty cross site scripting things.
Related
Using Angular 10
There are many questions on SO that are similar to this, but I have yet to find one that answers my situation.
I'm hoping someone can guide me.
I'm using a third party library to display 360° photos. This third party library has a built-in API to display hotspots in the scene. Simply give the library the element you want to be the hotspot, and it takes care of the rest.
I have most of it working as expected, but there are a couple pieces that are not.
So far, I'm dynamically generating my components like so:
this._hotspotFactory = this.resolver.resolveComponentFactory(HotspotComponent);
const component = this._hotspotFactory.create(this.injector);
//Hydrate component with bunch of data
component.instance.id = data.id;
...
// Create the Hotspot with Third Party
// Calling this third party method injects the native element into the DOM.
// Passing the nativeElement in. Looks great at first glance.
const hotspot = this._scene.createHotspot(data, component.location.nativeElement);
this.appRef.attachView(component.hostView);
component.hostView.detectChanges();
if(component.instance.over.observers.length) {
hotspot.on('over', (evt) => {
this.zone.run(() => {
component.instance.over.emit(evt);
});
});
}
if(component.instance.out.observers.length) {
hotspot.on('out', (evt) => {
this.zone.run(() => {
component.instance.out.emit(evt);
});
});
}
if(component.instance.navigate.observers.length) {
hotspot.on('click', (evt) => {
this.zone.run(() => {
component.instance.navigate.emit(evt);
})
});
}
No errors are thrown and I successfully see the hotspot where it should be in the scene. Even Data interpolation in the HotspotComponent template occurs as expected.
BUT, [ngStyle] bindings never result in dynamic styling in HotspotComponent.
I'm 99% sure this is because change detection is not taking place in the component.
I am manually attaching the view with this.appRef.attachView(component.hostView) because the third party is responsible for injecting the element into the DOM, not Angular. Thus Angular needs to know about it so it will perform change detection.
Even with manually calling attachView, I still think Angular doesn't know about this component in the view because the Angular Chrome Extension debugger doesn't register it in its dev tools as a known component in the view....despite be seeing it on screen and in the DOM.
What am I missing?
What change detection strategy does the component have?
When a component is added to a view, it's life cycle hooks will be triggered by angular(ngOninit, ngAfterContentInit etc). Log something in these and see if theme life cycle hooks are being called. Irrespective of the change detection strategy one change detection cycle should happen on the component after it is added to view.
If the life cycle hook invoking is not happening, then it would mean that angular is not involved in adding the element to DOM.
It seems angular has a lifecycle hook precisely for your use-case 'ngDoBootstrap'.
As we can not debug your full source code, from the information you have mentioned it seems the dynamic component you are trying to attach to the view is not available to Angular in NgModule. Every component that angular bootstraps must be in NgModule.
you can although bootstrap it dynamically using 'ngDoBootstrap'.
It is used in the following manner:
ngDoBootstrap(appRef: ApplicationRef) {
this.fetchDataFromApi().then((componentName: string) => {
if (componentName === 'ComponentOne') {
appRef.bootstrap(ComponentOne);
} else {
appRef.bootstrap(ComponentTwo);
}
});
}
In your case, you can do it before attaching the component to the view.
...
appRef.bootstrap(component);
this.appRef.attachView(component.hostView);
component.hostView.detectChanges();
...
Please check the documentation here: https://angular.io/api/core/ApplicationRef
We use resolveComponentFactory method given by ComponentFactoryResolver class present in angular which is used for component level lazy loading. For the confirmation that your component is really breaked in chunks, do ng build --prod or you con and you will see the generated .js chunk for SoftwareListComponent.
app.component.html
<button (click)="loadSoftwareListDynamically()> Load </button>
<div #softwareListContainer></div>
app.component.ts
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) {}
#ViewChild('softwareListContainer', { read: ViewContainerRef })
softwareListContainer: ViewContainerRef;
loadSoftwareListDynamically() {
import('../common-features/software-list/software-list.component').then(
({ SoftwareListComponent }) => {
const componentFactory =
this.componentFactoryResolver.resolveComponentFactory(
SoftwareListComponent
);
this.softwareListContainer.createComponent(componentFactory);
}
);
}
software-list.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
#NgModule({
declarations: [
SoftwareListComponent
],
imports: [
CommonModule,
RouterModule,
SwiperModule <- External Library
],
})
export class SoftwareListModule {}
For more info, you can go over to my complete discussion for Lazy loading of components created using ComponentFactoryResolver. You will get more info here ->
StackOverflow Discussion
Stackblitz Link Here
I am using the npm package ng-dynamic-component to create dynamic components.
I am in a situation where i want to call a specific function on the dynamically created component using this package.
I have experimented with a lot of different ways but so far found no solution.
Does anyone have any idea if it is possible to call a function on a component that is created dynamically using the mentioned package?
Thanks :)
ng-dynamic-component has a "Component Creation Event" ndcDynamicCreated that passes a ComponentRef<any> as a parameter.
From the docs:
#Component({
selector: 'my-component',
template: `<ndc-dynamic [ndcDynamicComponent]="component"
(ndcDynamicCreated)="componentCreated($event)"
></ndc-dynamic>`
})
class MyComponent {
component = MyDynamicComponent1;
componentCreated(compRef: ComponentRef<any>) {
// utilize compRef in some way ...
}
}
Utilizing compRef in some way would be calling a function of the compRef.instance property in your case.
i have noticed that when you use a custom component with a input property and you bind a function to it this function is called lots of times.
E.g.
<some-tag [random-input]="randomFunction()"></some-tag>
and in the class
private randomFunction() {
console.log('Called!');
return true
}
if you run something simple as this you will see in the console a few dozens of 'Called!' logs.
In my project the randomFunction makes a call to the database, so this is pretty anoying.
Does anyone knows why is this happening?
Angular runs this with every cycle, trying to check for updated value, that's why you see so many messages in the log.
For this reason it is not good practice to have ts functions as inputs to component.
You can for example make a call to the server/database in constructor, OnInit or OnChanges, store the result to local variable and make that variable as input to component. Something similar to this:
export class MyComp {
dbResult: any;
constructor(http: HttpClient) {
http.get('/my/api/call').subscribe(result => {
this.dbResult = result;
});
}
....
}
..and in HTML:
<some-tag [random-input]="dbResult"></some-tag>
As a sidenote, having that function marked as private will eventually fail during ng build --prod
Angular needs to check if the value has changes otherwise it can't update the value inside the component.
I'm making a pop-up component that I want to use in several of my other components, so I made a popup.service that enable the component to be loaded through *ngIf inside other components. This is creating a problem for me since the PopupComponent is a separate entity and I'm unsure how to pass data from the child component(PopupComponent) to its respective parents.
Atm the loading looks like this in ParentComponent.ts:
public openPopup(order_id: string, invoice_id: string): void{
this.load_popup=this.popupService.openPopup(order_id, "selected_order", invoice_id, "selected_invoice");
}
And ParentComponent.html:
<app-popup *ngIf="load_popup"></app-popup>
And it loads like a charm, the problem is in closing it. The close button is located on the PopupComponent, is there an efficient way to have the Child Component (PopupComponent) to affect a variable in the Parent Component ie. ParentComponent.load_popup=false?
My other thought was dynamically loading the component, however I have no idea on how to do that. I was fidgeting around with using the PopupService and putting something like this in it:
import { Injectable, ComponentRef } from '#angular/core';
import {PopupComponent} from '../popup/popup.component';
#Injectable({
providedIn: 'root'
})
export class PopupService {
popup_ref: ComponentRef<PopupComponent>
constructor(
) { }
//Implemented in orderoverviewcomponent, invoicecomponent, and placeordercomponent
public openPopup(id1:string, storage_label1:string, id2:string, storage_label2:string): Boolean{
if (id1){
localStorage.setItem(storage_label1, JSON.stringify(id1));
}
if (id2){
localStorage.setItem(storage_label2, JSON.stringify(id2));
}
this.popup_ref.initiate(); //this line is a made up example of loading the component
return true;
}
public closePopup(storage_label1: string, storage_label2:string): Boolean{
if(storage_label1){
localStorage.removeItem(storage_label1);
}
if(storage_label2){
localStorage.removeItem(storage_label2);
}
this.popup_ref.destroy();
return false;
}
}
Where this.popup_ref.destroy(); would ideally destroy PopupComponent, but when I did that I got a "cannot read property of undefined" on the popup_ref, I'm having trouble declaring it, the syntax seems a bit tricky.
The problem also remains that i need a function to load the component, the opposite of .destroy(), if this is possible I would much prefer it over loading and destroying with *ngIf.
Edit: Partially solved it by just using a boolean in the service as the trigger for *ngIf, is there a way to do a function load and destroy on a component still?
You can bind an EventEmitter() to your component to invoke a function in the parent component.
<app-popup [onClose]="load_popup = false" *ngIf="load_popup"></app-popup>
Then inside of your app-popup component:
#Output onClose = new EventEmitter();
public closePopup(/* code emitted for brevity */) {
/* code emitted for brevity */
this.onClose.emit(); //Call the parent function (in this case: 'load_popup = false')
}
It's important to know that you can pass entire functions to the bound function, and you can even pass variables back to the parent from the child:
[onClose]="myFunction($event)"
this.onClose.emit(DATA HERE);
As an aside, since you're using Angular; I would suggest looking into using Modals for popup dialogue boxes. You can see a good example here:
https://ng-bootstrap.github.io/#/components/modal/examples
In my current project, I have a very simple service which sets a string when a request comes from first page and show it in the second page using the same service. Setting the text is working perfectly. But when I called the get function, it returns undefined.
This is my service
import { Injectable } from '#angular/core';
#Injectable()
export class TsService {
constructor() { }
ts: string;
getTs() : string {
return this.ts;
}
setTs(ts) : void {
this.ts = ts;
}
}
In my first component I imported the Service
import { TsService } from './ts.service';
and added it to the providers
providers: [TsService]
and initialized in the contructor
private tsService: TsService
and to the button click, I set a string as well
this.tService.setTs(form.value.member)
In my second component, followed the same steps mentioned above except in the constructor I assigned as follows
this.ts = tsService.getTs();
but it gives me undefined. Is there anything that I missed
As i can make out from your code. You have registered your service as a provider in your component. like
providers: [TsService]
What this line of code will do. Is that it will fetch a new Instance of your service as soon as your component comes into play. So from first component lets say ComponentA you set the service variable as
this.tService.setTs(form.value.member)
But here ComponentA is having suppose Instnace1 of the service. So you have set the value to Instance1 of the Service. Now you navigate to second component say ComponentB As soon as ComponentB comes into play it angular creates a new Instance of the service and same is made available to ComponentB. Now there are two Instances of your service one with ComponentA and one with ComponentB. but you have set the variable using ComponentA so it wont be available to ComponentB hence
this.ts = this.tsService.getTs();
this returns undefined.
In order to check whether you variable is set or not you can try
this.tService.setTs(form.value.member);
console.log(this.tsService.getTs());
in your ComponentA it will log the value set.
The solution for this problem of your is to share the same Instance and that can be achieved by registering the service as provider in the AppModule.
As official docs say
On the one hand, a provider in an NgModule is registered in the root
injector. That means that every provider registered within an NgModule
will be accessible in the entire application.
For more please refer :-
Dependency Injection
NgModule Injectors
Hope it helps :)
Depending on the order in which stuff is executed it may well be that
this.tService.setTs(form.value.member)
is being executed after
this.ts = tsService.getTs();
In which case the behaviour is expected.
As for how to deal with this problem. One way is to add a way for components to subscribe to the service and get notified when ts changes so that they can react by executing some code. Look into RxJS' Subject.
A different reason may be that you are not providing the service correctly.
For example if you provide the service to a parent and a child component (direct or not). In that case the second provider may be shadowing the first due to Angular's hierarchical dependency injection. Which means that one component is setting the value in one instance of the service and the other component is getting it from a different one. Unless you specifically need that kind of behaviour a service should only be provided at the root of the component tree where it's going to be used.
If your components are not related through the parent-child hierarchy then you should be providing the service only once. At the module level.
Without knowing more about your component structure it's not possible to tell what exactly is going on.
You may have use two instances of data object.
use one data service for set and get data.Then you can access to same data object within different components.
And if you set that service as a provider for your components individually those gonna work as different instances. If you need only one instance for your whole app you can set that service as a provider in app.module.ts file.