79

I am using a javascript Object that has a callback. Once the callback is fired I want to call a function inside an Angular2 component.

example HTML file.

    var run = new Hello('callbackfunction');

    function callbackfunction(){   
     // how to call the function **runThisFunctionFromOutside**
   }
   <script>
      System.config({
        transpiler: 'typescript', 
        typescriptOptions: { emitDecoratorMetadata: true }, 
        packages: {'js/app': {defaultExtension: 'ts'}} 
      });
      System.import('js/app/main')
            .then(null, console.error.bind(console));
    </script>

My App.component.ts

import {Component NgZone} from 'angular2/core';
import {GameButtonsComponent} from './buttons/game-buttons.component';
@Component({
  selector: 'my-app',
  template: ' blblb'
})
export class AppComponent {

constructor(private _ngZone: NgZone){}

ngOnInit(){
    calledFromOutside() {
        this._ngZone.run(() => {
          this.runThisFunctionFromOutside();
    });
  }
  }
runThisFunctionFromOutside(){
   console.log("run");
}

How can i call the function runThisFunctionFromOutside which is inside App.component.ts

Dean Kuga
  • 11,878
  • 8
  • 54
  • 108
Roninio
  • 1,761
  • 1
  • 17
  • 24

6 Answers6

78

I basically followed this answer, but I didn't want my "outside" code to know anything about NgZone. This is app.component.ts:

import {Component, NgZone, OnInit, OnDestroy} from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    window.my = window.my || {};
    window.my.namespace = window.my.namespace || {};
    window.my.namespace.publicFunc = this.publicFunc.bind(this);
  }

  ngOnDestroy() {
    window.my.namespace.publicFunc = null;
  }

  publicFunc() {
    this.ngZone.run(() => this.privateFunc());
  }

  privateFunc() {
    // do private stuff
  }
}

I also had to add a definition for TypeScript to extend the window object. I put this in typings.d.ts:

interface Window { my: any; }

Calling the function from the console is now as simple as:

my.namespace.publicFunc()
Community
  • 1
  • 1
Big McLargeHuge
  • 14,841
  • 10
  • 80
  • 108
65

See also How do expose angular 2 methods publicly?

When the component is constucted make it assign itself to a global variable. Then you can reference it from there and call methods. Don't forget to use zone.run(() => { ... }) so Angular gets notified about required change detection runs.

 function callbackfunction(){   
   // window['angularComponentRef'] might not yet be set here though
   window['angularComponent'].zone.run(() => {
     runThisFunctionFromOutside(); 
   });
 }

constructor(private _ngZone: NgZone){
  window['angularComponentRef'] = {component: this, zone: _ngZone};
}

ngOnDestroy() {
  window.angularComponent = null;
}

Plunker example1

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');})

An alternative approach

would be to dispatch events outside Angular and listen to them in Angular like explained in Angular 2 - communication of typescript functions with external js libraries

Plunker example2 (from the comments)

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Out of interest, wouldn't it be better to inject the `Window` provider rather than attaching it directly to the `window` object? – Joseph Woodward Feb 10 '16 at 09:06
  • Probably. But at first it should work at all I guess. – Günter Zöchbauer Feb 10 '16 at 09:07
  • 2
    If minification changes the names it might be better to assign a function reference instead of a class reference (`window.angularComponentRef = {zone: zone, /*component: this*/ componentFn: this.callFromOutside};`). – Günter Zöchbauer Feb 10 '16 at 10:00
  • @GünterZöchbauer did you look at question. I have the same issue. When i call a function, callFromOutside(){ this.edit(); } i get undefined – Roninio May 22 '16 at 15:28
  • @Roninio not sure what you mean. Can you demonstrate it with a Plunker? – Günter Zöchbauer May 22 '16 at 15:32
  • @GünterZöchbauer https://plnkr.co/edit/rKZaWsjxZusitnDq1d3Z?p=preview in the console. window.angularComponentRef.zone.run(() => { window.angularComponentRef.component.callFromOutside('1');} ) i placed, version 16 – Roninio May 22 '16 at 15:57
  • 1
    check the `outside` button in `index.html` https://plnkr.co/edit/e77JkHmO7n5FYoKSXnIL?p=preview – Günter Zöchbauer May 22 '16 at 16:50
  • @GünterZöchbauer I manage to make it work using an extra `.bind(this)` So it will look like this: `window.ServiceName = { fname: this.fname.bind(this), zone: _ngZone };` – Daniel Dudas May 23 '16 at 11:55
  • 1
    @GünterZöchbauer thanks for the response. The issue I had, was calling a function inside a child. I had to make in each constructor the NgZone event. – Roninio May 24 '16 at 11:43
  • 1
    Once upgraded to RC4, window.angularComponentRef is throwing error, `Property 'angularComponentRef' does not exist on type 'Window'.`while starting the npm. Is there any alternative method to replace window.angularComponentRef – Nidhin T T Aug 04 '16 at 11:56
  • Please provide a Plunker. – Günter Zöchbauer Aug 04 '16 at 12:00
  • 1
    @GünterZöchbauer I followed this steps but getting this problems: http://stackoverflow.com/questions/39849369/linkedin-with-angular-2 – Dheeraj Agrawal Oct 04 '16 at 10:00
  • 1
    I can confirm that setting `window.angularComponent[Ref]` inside the component will not compile with latest setup. However using `window['angularComponentRef']` will do just fine. This answer is excellent, thanks for your contributions günter – phil294 Mar 04 '17 at 15:01
  • Works great, but the app's performance is hurt. Something familiar with the problem? – Harry Dec 24 '17 at 12:03
  • @Harry I doubt this is related to performance. Perhaps you niid to invoke change detection to make Angular update bindings. – Günter Zöchbauer Dec 24 '17 at 12:06
6

Below is a solution.

function callbackfunction(){   
   window.angularComponent.runThisFunctionFromOutside();
}
       <script>
          System.config({
            transpiler: 'typescript', 
            typescriptOptions: { emitDecoratorMetadata: true }, 
            packages: {'js/app': {defaultExtension: 'ts'}} 
          });
          System.import('js/app/main')
                .then(null, console.error.bind(console));
        </script>

My App.component.ts

import {Component NgZone} from 'angular2/core';
import {GameButtonsComponent} from './buttons/game-buttons.component';
@Component({
    selector: 'my-app',
       template: ' blblb'
})
export class AppComponent {

  constructor(private _ngZone: NgZone){
  window.angularComponent = {runThisFunctionFromOutside: this.runThisFunctionFromOutside, zone: _ngZone};
}


    runThisFunctionFromOutside(){
      console.log("run");
    }
}
Roninio
  • 1,761
  • 1
  • 17
  • 24
3

An other approach without using global variables is to use pass a control object and bind its properties to the variables and methods to expose.

export class MyComponentToControlFromOutside implements OnChanges {

  @Input() // object to bind to internal methods
  control: {
    openDialog,
    closeDialog
  };

  ngOnChanges() {
    if (this.control) {
      // bind control methods to internal methods
      this.control.openDialog = this.internalOpenDialog.bind(this);
      this.control.closeDialog = this.internalCloseDialog;
    }
  }

  internalOpenDialog(): Observable<boolean> {
    // ...
  }

  internalCloseDialog(result: boolean) {
    // ...
  }
}
export class MyHostComponent {
   controlObject= {};
}
<my-component-to-control [control]="controlObject"></my-component-to-control>

<a (click)="controlObject.open()">Call open method</a>
Nico Toub
  • 1,528
  • 15
  • 17
1

I had a similar situation when using the callback 'eventClick' of the fullCalendar library, whose callbacks are returning from outside the angular zone, causing my application to have partial and unreliable effects. I was able to combine the zone approach and a closure reference to the component as seen below in order to raise an output event. Once I started executing the event inside of the zone.run() method the event and it's effects were once again predictable and picked up by angular change detection. Hope this helps someone.

constructor(public zone: NgZone) { // code removed for clarity
}

ngOnInit() {
    this.configureCalendar();
}

private configureCalendar() {
    // FullCalendar settings
    this.uiConfig = {
        calendar: { // code removed for clarity

        }
    };

    this.uiConfig.calendar.eventClick = this.onEventClick();

}

private onEventClick() {
    const vm = this;

    return function (event, element, view) {
        vm.zone.run(() => {
            vm.onSequenceSelected.emit(event.sequenceSource);                    
        });

        return false;

    };
}
Brandon Culley
  • 5,219
  • 1
  • 28
  • 28
0

Just adding to @Dave Kennedy:

Calling the function from the console is now as simple as:

my.namespace.publicFunc()

1) If we try to access our component's public method from a different domain you will get caught into CORS issue (the cross origin problem, can be solved if both server and client code resides in same machine).

2) if you were to call this method from server using javascript, you will have to use window.opener.my.namespace.publicFunc() instead of window.my.namespace.publicFunc():

window.opener.my.namespace.publicFunc();

Kailas
  • 7,350
  • 3
  • 47
  • 63
  • what is `my`? Is that the literal value? Or some special key? – redOctober13 May 08 '18 at 19:58
  • Any variable name you can choose instead of my ""ngOnInit() { window.my = window.my || {}; window.my.namespace = window.my.namespace || {}; window.my.namespace.publicFunc = this.publicFunc.bind(this); } "" – Kailas May 10 '18 at 06:55