19

In an Angular 2+ component, how do I pass in a callback function that takes parameters? My initial assumption was something like

<app-example [onComplete]="doThing('someParam')"></app-example>

And sometimes I won't need any parameters, like this:

<app-example [onComplete]="doSomeThingElse()"></app-example>

And then in the component I have

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

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
})
export class ExampleComponent {
  @Input() public onComplete: () => void;

  //run `this.onComplete()` somewhere as needed
}

But, what ends up happening is that the doThing('someParam') or doSomeThingElse() is immediately called without any interaction.

How am I supposed to pass callback functions in to a component to be called later?


EDIT:

The actual problem I am trying to solve here is to be able to run any passed in function at a later time. This is for a confirmation component that will ask the user "are you sure you want to continue?" and then if they press the "Yes I'm sure" button, the passed in function will run.

Chris Barr
  • 29,851
  • 23
  • 95
  • 135
  • @yurzui isn't this reinventing the wheel with events and potentially dangerous with memory leaks? (not your solution but intention itself) – dee zg Dec 28 '17 at 18:50
  • 1
    @deezg `bind` method will return new function every time angular calls change detection cycle. So i agree we shouldn't reinvent the wheel) – yurzui Dec 28 '17 at 18:53
  • you should read about @Output. It's what you are looking for. – toskv Dec 28 '17 at 19:07
  • @toskv I know of `@Output` to pass values back to a parent controller, but I'm not sure how I would use that here in this context. Feel free to add an answer to this question with the details, and I'll mark it as correct. Telling me to "read about" something is not very helpful to me right now. – Chris Barr Dec 28 '17 at 19:17
  • @ChrisBarr you might be better off actually describing the problem you want to solve. Problem not being 'passing a callback function as variable' but real problem you want your component(s) to solve. Usual solution for what this looks like is to send parameters to child as `@Input` params and then have event bound with `@Output` and event will be raised with whatever arguments you want. When event is raised, your parent component will call whatever function it wants (your callback in case above) with whatever parameters it got through event. – dee zg Dec 28 '17 at 19:21
  • @deezg ok, good point. I've added that to my question. However, what I actually want is callback function though. Sometimes it will have parameters, sometimes it won't. – Chris Barr Dec 28 '17 at 19:28
  • @ChrisBarr Based on your update, why not have `@Output` event defined like `(confirm)=onConfirmed($event)`? So, when your child component gets confirm click it raises that event, parent gets notified (with whatever params packed within event) and calls whatever function it wants. If there are any parameters you need to pass to your child function, you just bind them separately as usual properties through `@Input` binding. – dee zg Dec 28 '17 at 19:30
  • @deezg I only half-follow what you mean here. Can you please write out an answer with a code example? I'll mark yours as the correct answer if it works – Chris Barr Dec 28 '17 at 19:33
  • Why??? Output events are the way to do this kind of stuff! – Fals Dec 28 '17 at 19:34
  • @ChrisBarr hold your horses. You didn't help with your explanations either since you came in asking how to do wrong thing and not if its wrong itself ;). Check my answer below. – dee zg Dec 28 '17 at 19:43

4 Answers4

26

Here's an example of the @Output syntax @toskv was looking for, Angular pass callback function to child component as @Input

So for your example,

<app-example 
  (onComplete)="doThing()" 
  [completedParam]="'someParam'"></app-example>
@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
})
export class ExampleComponent {
  @Output() public onComplete: EventEmitter<any> = new EventEmitter();
  @Input() completedParam;

  runOnComplete(): void {
    this.onComplete.emit(this.completedParam);
  }
}

Does not feel as good as [onComplete]="doThing.bind(this, 'someParam')".

  • 1
    I was able to remove the `@Input() completedParam;` and just call `this.onComplete.emit()` and it worked with anything I passed in or didn't pass in. thanks! – Chris Barr Dec 28 '17 at 20:01
  • Interesting, so `(onComplete)="doThing('someParam')"`? I guess it's ok because param is a fixed string at compile time? –  Dec 28 '17 at 20:04
  • Yep, that worked, and I'm glad because it would be confusing to have a parent controller that had the function I needed to run, but then have to pass it parameterless and pass params in on some other binding. This is exactly what I was hoping would work! I actually did try this originally, but I ran into another issue and assumed it was related so I backtracked and posted this question. Oh well! – Chris Barr Dec 28 '17 at 20:09
4

Template:

<app-example [someParams]="someParamsObject"
             (complete)="onComplete($event)" >
</app-example>

Component:

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
})
export class ExampleComponent {
  @Input()
  someParams: YourParamsType;

  @Output()
  complete:EventEmitter<any> = new EventEmitter<any>();

  //run `this.complete(this.someParams)` somewhere as needed and 
  //pack in some params if you need
}

In your calling, parent component you need a function named onComplete that receives parameter of type any in this case (that comes from @Output being defined as EventEmitter<any>). If you need, you can also have event parameters of any particular type you like EventEmitter<YourParticularType>.

dee zg
  • 13,793
  • 10
  • 42
  • 82
4

The said solution will work only if you have a method that needs to invoke without taking action on the component itself. However, in my case, I need to execute an observable method inside of the app-example component and wait for the response to do some action inside of that component.

If anyone has the same issue. Here is the solution for it.

  1. create an interface.

    export interface Delegate<T> {
      (...args: any[]): T;
    }
    
  2. On your angular component, create a @Input variable

    @Component({
        selector: 'app-example',
        templateUrl: './example.component.html',
    })
    export class AppExampleComponent {
      @Input() executeWhen: Delegate<Observable<any>>;
    
      runOnComplete(): void {
        this.executeWhen().subscribe(() => // do some action);
      }
    }
    
Yhan
  • 51
  • 5
  • creation of Interface is the solution to utilize `@Input` and the callback function can be called later, instead of setting it to `@Output` Parameter. – Yhan Sep 10 '18 at 03:08
1

You can have a private method in your component:

private doThingFactory(param) {
  return () => this.doThing(param);
}

and then use it like that:

<app-example [onComplete]="doThingFactory('someParam')"></app-example>
Sagi
  • 8,009
  • 3
  • 26
  • 25
  • I thought that might work, but I'd like to avoid this if possible. It seems like Angular should just have a way to pass a function and invoke it later – Chris Barr Dec 28 '17 at 18:44
  • 4
    Actually, it is not a best practice. Angular encourages you to use Input and Output to solve your problems, and not use callback functions if possible. You should send someParam as input and receive the result as output. – Sagi Dec 28 '17 at 18:47
  • 1
    I think the @Output is what he's looking for. Maybe add an example of that too? – toskv Dec 28 '17 at 19:06
  • i think it is better to use public method. as the method would be involved in html outside class. It is sometimes a checking for this which may cause compile error although there are not public / private method in javascript actaully. – Ben Cheng Aug 19 '20 at 04:37