Reputation: 12191
AngularJS has the & parameters where you could pass a callback to a directive (e.g AngularJS way of callbacks. Is it possible to pass a callback as an @Input
for an Angular Component (something like below)? If not what would be the closest thing to what AngularJS does?
@Component({
selector: 'suggestion-menu',
providers: [SuggestService],
template: `
<div (mousedown)="suggestionWasClicked(suggestion)">
</div>`,
changeDetection: ChangeDetectionStrategy.Default
})
export class SuggestionMenuComponent {
@Input() callback: Function;
suggestionWasClicked(clickedEntry: SomeModel): void {
this.callback(clickedEntry, this.query);
}
}
<suggestion-menu callback="insertSuggestion">
</suggestion-menu>
Upvotes: 341
Views: 383043
Reputation: 31
Using Signals Angular 17.1 and newer
Why should we use signal inputs and not @Input()?
As Angular states:
- Signal inputs are more type safe:
Required inputs do not require initial values, or tricks to tell TypeScript that an input always has a value.
Transforms are automatically checked to match the accepted input values.
Signal inputs, when used in templates, will automatically mark OnPush components as dirty.
Values can be easily derived whenever an input changes using computed.
Easier and more local monitoring of inputs using effect instead of ngOnChanges or setters.
To make use of a signal based approach you may want to receive the function as input signal like:
// child component
@Component({
selector: 'app-button',
templateUrl: './button.component.html',
standalone: true
})
export class ButtonComponent {
onClick = input<() => void>(() => { });
}
// child component template
<button
(click)="onClick()()" //getting value of signal with first () and calling function with second ()
>
<ng-content></ng-content>
</button>
In your parent component you simply pass your method to the child component:
// in parent component template
<app-button [onClick]="myMethod">MyButton</app-button>
// in parent component
myMethod = () => {
console.log('called myMethod');
}
Upvotes: 2
Reputation: 1
There is a way to pass a function to a child component using @Input and it's quite simple.
On your parent component:
myCallback = (args: any): void => {
//your code here
}
On your child component:
@Input() callback: (args: any) => void = () => {};
Pass the function to your child component binding:
<app-child-component [callback]=”myCallback”></app-child-component>
Although it works like this, perhaps as mentioned above, the best solution could be to use @Output instead.
Upvotes: 0
Reputation: 1781
As for me, besides .bind(this)
, I had to put a pair of parentheses behind the the method's name for the method to be executed.
In the Parent component:
In the .ts file:
this.pillTabs = [
{ tabName: 'Subscribers', tabMethod: this.showSubscribers.bind(this) },
{ tabName: 'Exemplars', tabMethod: this.showExemplars.bind(this) }
];
In the .html file:
<pill-tabs [pillTabs]="pillTabs"></pill-tabs>
In the Child component:
In the .ts file:
@Input() pillTabs: PillTab[];
In the .html file:
<div *ngFor="let pillTab of pillTabs; let i = index">
<input type="radio" id="{{'radio-' + i}}" name="tabs" [checked]="pillTab.checked"
(click)="pillTab.tabMethod()" />
<label class="tab" for="{{'radio-' + i}}">{{pillTab.tabName}}</label>
</div>
The code was NOT working when I did NOT have the pair of parentheses behind the method:
(click)="pillTab.tabMethod"
And then when I put the pair of parentheses there, the code started to work.
(click)="pillTab.tabMethod()"
I hope that someone finds it helpful.
Upvotes: 0
Reputation: 1321
An alternative to the answer Max Fahl gave.
You can define callback function as an arrow function in the parent component so that you won't need to bind that.
@Component({
...
// unlike this, template: '<child [myCallback]="theCallback.bind(this)"></child>',
template: '<child [myCallback]="theCallback"></child>',
directives: [ChildComponent]
})
export class ParentComponent {
// unlike this, public theCallback(){
public theCallback = () => {
...
}
}
@Component({...})
export class ChildComponent{
//This will be bound to the ParentComponent.theCallback
@Input()
public myCallback: Function;
...
}
Upvotes: 16
Reputation: 1945
Following works for me in Angular 13 (as of March 2022).
P.S.- This is more or less similar to what others answered. Adding this answer just to let people know it works in Angular 13.
Define the Function as Flat Arrow and not regular function in parent component.
callBackFn= (args: string): void => {
// callback code here
// This will work (Flat Arrow)
}
// callbackFn(args: string): void {
// //This type of definition will not work.
// }
Pass callback function as attribute to child component
<app-child [callBack]=”callBackFn”></app-child>
Receive the callback function as Input in the child component. The definition should match as that defined in the parent.
@Input() callBack: (args: string) => void;
Then call this function in the child component. You can also call this is child component template.
this.callBack('Test');
OR
<button (click)="callBack('Test')"></button>
But not sure whether this approach is good or not. I see a similar approach in ReactJS and it works great but still not sure how it works in angular and what will be its impact.
Any comments on this approach would be appreciated.
Upvotes: 6
Reputation: 1121
Another alternative.
The OP asked a way to use a callback. In this case he was referring specifically to a function that process an event (in his example: a click event), which shall be treated as the accepted answer from @serginho suggests: with @Output
and EventEmitter
.
However, there is a difference between a callback and an event: With a callback your child component can retrieve some feedback or information from the parent, but an event only can inform that something happened without expect any feedback.
There are use cases where a feedback is necessary, ex. get a color, or a list of elements that the component needs to handle. You can use bound functions as some answers have suggested, or you can use interfaces (that's always my preference).
Example
Let's suppose you have a generic component that operates over a list of elements {id, name} that you want to use with all your database tables that have these fields. This component should:
Child Component
Using normal binding we would need 1 @Input()
and 3 @Output()
parameters (but without any feedback from the parent). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>
, but creating an interface we will need only one @Input()
:
import {Component, Input, OnInit} from '@angular/core';
export interface IdName{
id: number;
name: string;
}
export interface IListComponentCallback<T extends IdName> {
getList(page: number, limit: number): Promise< T[] >;
removeItem(item: T): Promise<boolean>;
click(item: T): void;
}
@Component({
selector: 'list-ctrl',
template: `
<button class="item" (click)="loadMore()">Load page {{page+1}}</button>
<div class="item" *ngFor="let item of list">
<button (click)="onDel(item)">DEL</button>
<div (click)="onClick(item)">
Id: {{item.id}}, Name: "{{item.name}}"
</div>
</div>
`,
styles: [`
.item{ margin: -1px .25rem 0; border: 1px solid #888; padding: .5rem; width: 100%; cursor:pointer; }
.item > button{ float: right; }
button.item{margin:.25rem;}
`]
})
export class ListComponent implements OnInit {
@Input() callback: IListComponentCallback<IdName>; // <-- CALLBACK
list: IdName[];
page = -1;
limit = 10;
async ngOnInit() {
this.loadMore();
}
onClick(item: IdName) {
this.callback.click(item);
}
async onDel(item: IdName){
if(await this.callback.removeItem(item)) {
const i = this.list.findIndex(i=>i.id == item.id);
this.list.splice(i, 1);
}
}
async loadMore(){
this.page++;
this.list = await this.callback.getList(this.page, this.limit);
}
}
Parent Component
Now we can use the list component in the parent.
import { Component } from "@angular/core";
import { SuggestionService } from "./suggestion.service";
import { IdName, IListComponentCallback } from "./list.component";
type Suggestion = IdName;
@Component({
selector: "my-app",
template: `
<list-ctrl class="left" [callback]="this"></list-ctrl>
<div class="right" *ngIf="msg">{{ msg }}<br/><pre>{{item|json}}</pre></div>
`,
styles:[`
.left{ width: 50%; }
.left,.right{ color: blue; display: inline-block; vertical-align: top}
.right{max-width:50%;overflow-x:scroll;padding-left:1rem}
`]
})
export class ParentComponent implements IListComponentCallback<Suggestion> {
msg: string;
item: Suggestion;
constructor(private suggApi: SuggestionService) {}
getList(page: number, limit: number): Promise<Suggestion[]> {
return this.suggApi.getSuggestions(page, limit);
}
removeItem(item: Suggestion): Promise<boolean> {
return this.suggApi.removeSuggestion(item.id)
.then(() => {
this.showMessage('removed', item);
return true;
})
.catch(() => false);
}
click(item: Suggestion): void {
this.showMessage('clicked', item);
}
private showMessage(msg: string, item: Suggestion) {
this.item = item;
this.msg = 'last ' + msg;
}
}
Note that the <list-ctrl>
receives this
(parent component) as the callback object.
One additional advantage is that it's not required to send the parent instance, it can be a service or any object that implements the interface if your use case allows it.
The complete example is on this stackblitz.
Upvotes: 4
Reputation: 105
Use Observable pattern. You can put Observable value (not Subject) into Input parameter and manage it from parent component. You do not need callback function.
See example: https://stackoverflow.com/a/49662611/4604351
Upvotes: 0
Reputation: 6114
In some cases, you might need business logic to be performed by a parent component. In the example below we have a child component that renders table row depending on the logic provided by the parent component:
@Component({
...
template: '<table-component [getRowColor]="getColor"></table-component>',
directives: [TableComponent]
})
export class ParentComponent {
// Pay attention on the way this function is declared. Using fat arrow (=>) declaration
// we can 'fixate' the context of `getColor` function
// so that it is bound to ParentComponent as if .bind(this) was used.
getColor = (row: Row) => {
return this.fancyColorService.getUserFavoriteColor(row);
}
}
@Component({...})
export class TableComponent{
// This will be bound to the ParentComponent.getColor.
// I found this way of declaration a bit safer and convenient than just raw Function declaration
@Input('getRowColor') getRowColor: (row: Row) => Color;
renderRow(){
....
// Notice that `getRowColor` function holds parent's context because of a fat arrow function used in the parent
const color = this.getRowColor(row);
renderRow(row, color);
}
}
So, I wanted to demonstrate 2 things here:
Upvotes: 76
Reputation: 851
Passing method with argument, using .bind inside template
@Component({
...
template: '<child [action]="foo.bind(this, 'someArgument')"></child>',
...
})
export class ParentComponent {
public foo(someParameter: string){
...
}
}
@Component({...})
export class ChildComponent{
@Input()
public action: Function;
...
}
Upvotes: 5
Reputation: 888
An alternative to the answer SnareChops gave.
You can use .bind(this) in your template to have the same effect. It may not be as clean but it saves a couple of lines. I'm currently on angular 2.4.0
@Component({
...
template: '<child [myCallback]="theCallback.bind(this)"></child>',
directives: [ChildComponent]
})
export class ParentComponent {
public theCallback(){
...
}
}
@Component({...})
export class ChildComponent{
//This will be bound to the ParentComponent.theCallback
@Input()
public myCallback: Function;
...
}
Upvotes: 36
Reputation: 13347
UPDATE
This answer was submitted when Angular 2 was still in alpha and many of the features were unavailable / undocumented. While the below will still work, this method is now entirely outdated. I strongly recommend the accepted answer over the below.
Original Answer
Yes in fact it is, however you will want to make sure that it is scoped correctly. For this I've used a property to ensure that this
means what I want it to.
@Component({
...
template: '<child [myCallback]="theBoundCallback"></child>',
directives: [ChildComponent]
})
export class ParentComponent{
public theBoundCallback: Function;
public ngOnInit(){
this.theBoundCallback = this.theCallback.bind(this);
}
public theCallback(){
...
}
}
@Component({...})
export class ChildComponent{
//This will be bound to the ParentComponent.theCallback
@Input()
public myCallback: Function;
...
}
Upvotes: 158
Reputation: 7490
I think that is a bad solution. If you want to pass a Function into component with @Input()
, @Output()
decorator is what you are looking for.
export class SuggestionMenuComponent {
@Output() onSuggest: EventEmitter<any> = new EventEmitter();
suggestionWasClicked(clickedEntry: SomeModel): void {
this.onSuggest.emit([clickedEntry, this.query]);
}
}
<suggestion-menu (onSuggest)="insertSuggestion($event[0],$event[1])">
</suggestion-menu>
Upvotes: 413
Reputation: 91
As an example, I am using a login modal window, where the modal window is the parent, the login form is the child and the login button calls back to the modal parent's close function.
The parent modal contains the function to close the modal. This parent passes the close function to the login child component.
import { Component} from '@angular/core';
import { LoginFormComponent } from './login-form.component'
@Component({
selector: 'my-modal',
template: `<modal #modal>
<login-form (onClose)="onClose($event)" ></login-form>
</modal>`
})
export class ParentModalComponent {
modal: {...};
onClose() {
this.modal.close();
}
}
After the child login component submits the login form, it closes the parent modal using the parent's callback function
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'login-form',
template: `<form (ngSubmit)="onSubmit()" #loginForm="ngForm">
<button type="submit">Submit</button>
</form>`
})
export class ChildLoginComponent {
@Output() onClose = new EventEmitter();
submitted = false;
onSubmit() {
this.onClose.emit();
this.submitted = true;
}
}
Upvotes: 8
Reputation: 728
The current answer can be simplified to...
@Component({
...
template: '<child [myCallback]="theCallback"></child>',
directives: [ChildComponent]
})
export class ParentComponent{
public theCallback(){
...
}
}
@Component({...})
export class ChildComponent{
//This will be bound to the ParentComponent.theCallback
@Input()
public myCallback: Function;
...
}
Upvotes: -3