8

I am creating dynamic components for my application, which can be re-used in various parts of the program. So far I have been able to create text inputs that are dynamically added and customized using the componentFactory into a form and they work perfectly.

The next part is creating the fully dynamic buttons which can be customized when placing in the targeted view (just like the text inputs with the form). I have tried to make most of the things generic and they work ok, but the problem I seem to be having is making the (click) function dynamic. I want to add the function needing to be triggered using the componentFactory as well, but for some reason I am not able to do so.

I can't find any resource that would give me details of this specific problem I am having. Here is the component I have made so far:

button.component.ts

import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-button',
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class ButtonComponent implements OnInit {

  @Input() group: FormGroup;
  @Input() type: string;
  @Input() description: string;
  @Input() class: string;
  @Input() data: string;
  @Input() callFunction: string;


  constructor() { }

  ngOnInit() {
  }

}

button.component.html

<div [formGroup]="group">
  <button type="{{ type }}" class="{{ class }}" (click)="{{callFunction}}">{{ description }}</button>
</div>

The (click) is not working in the button.component.html, it gives me the following error:

Parser Error: Got interpolation ({{}}) where expression was expected

everything else works, but I can't make the button fully dynamic unless this is catered for, and I can't find the resource that would fulfill my requirements.

EDIT I have added the function using which I am importing the component into my view:

  buildLoginButton(){
    let data = {
      type: "button",
      class: "btn btn-primary px-4",
      description: this.translate.transform("pages[login_page][login_form][buttons][login]"),
      callFunction: "login()", //I have tried this.login() as well
      group: this.userForm
      }
    const inputFactory = this.resolver.resolveComponentFactory(ButtonComponent);
    const loginButton = this.login_button.createComponent(inputFactory);
    loginButton.instance.group = data.group;
    loginButton.instance.type = data.type;
    loginButton.instance.class = data.class;
    loginButton.instance.description = data.description;
    loginButton.instance.callFunction = data.callFunction;
  }
Muhammad Hamza
  • 823
  • 1
  • 17
  • 42
  • have you tried without {{}} interpolation binding – TheParam Feb 25 '19 at 06:09
  • Hi @TheParam, I'm not sure what you mean, could you please elaborate? – Muhammad Hamza Feb 25 '19 at 06:11
  • Try defining `callFunction` in your `@Input` property as an actual function instead of a string name of a function. – DeborahK Feb 25 '19 at 06:32
  • Hi @DeborahK, I have changed the property as you requested and used `this.login()` in my import function, but when I run the server it showed an error **on load** (which should be produced when I hit login), and the login button itself is unresponsive (nothing in the console as well when I click it). Here is a screenshot: https://pasteboard.co/I2L2EWv.png – Muhammad Hamza Feb 25 '19 at 06:46
  • Hi Updated answer and solution with fully working example please check https://stackblitz.com/edit/angular-oqkmee?file=src%2Fapp%2Fbutton%2Fbutton.component.ts – TheParam Feb 25 '19 at 07:06
  • Hi @TheParam, thanks for the update. Here we are only console logging, but could you please edit the example to show how I would be able to call the function that is located in the parent component onClick using the format in the **edit** section of my question? – Muhammad Hamza Feb 25 '19 at 07:51
  • Hi @Meh, hey are you able to handle insertion of images in the button with this code like a shopping_cart icon in the button? – Tejaswi Pandava Jul 09 '20 at 17:15

2 Answers2

4

Here you need to property binding and event binding like below.

app.component.html

<app-button description="Dyanmic Button" 
  class="button" (callFunction)="onButtonClicked($event)" >
</app-button>

app.component.ts

export class AppComponent  {
  name = 'Angular';

  onButtonClicked(event) {
   console.log(event); // handle button clicked here.
  }
}

button.component.html

<div [formGroup]="group">
  <button [type]="type" [class]="class" (click)="onClick($event)">
   {{ description }}
  </button>
</div>

button.component.ts

import { Component, OnInit, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-button',
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.css'],
  encapsulation: ViewEncapsulation.None

})
export class ButtonComponent implements OnInit {

  @Input() group: FormGroup;
  @Input() type: string;
  @Input() description: string;
  @Input() class: string;
  @Input() data: string;
  @Output() callFunction = new EventEmitter();



  constructor() { }

  ngOnInit() {

    this.group = new FormGroup({
      firstName: new FormControl()
    });
  }


  onClick(event) {
    this.callFunction.emit('I am button');
  }


}

Here is solution on stackblitz

TheParam
  • 10,113
  • 4
  • 40
  • 51
  • Hi, thanks for the solution. The rest of the properties were being interpolated fine, the only problem I was having was with the `(click)` attribute. I tried removing the interpolation as you asked with the `(click)` but now the property is not displayed altogether when i inspect in the `console`. The same is happening in your example as well when I inspect the button. – Muhammad Hamza Feb 25 '19 at 06:26
3

I was able to make this example work:

Child Component

export class HelloComponent implements OnChanges {
  @Input() name: string;
  @Input() callFunction: Function;  // NOTE: The data type here

  // Just some code to call the function
  ngOnChanges() {
    // Ensure the @Input property has been set before trying to call it.
    if (this.callFunction) {
      this.callFunction();
    }
  }
}

Parent Template

<hello 
  name="{{ name }}"
  [callFunction]="myCallBackFunction"></hello>

Notice that is it using property binding with [] and referencing the function in the parent's component code.

Parent Component

export class AppComponent  {
  name = 'Angular';

  myCallBackFunction() {
    console.log('called the function')
  }
}

I have a working stackblitz here: https://stackblitz.com/edit/angular-function-input-property-deborahk

When you run the code you will see "called the function" in the console.

I have not used dynamically loaded components, so not sure how that impacts how this works. But wanted to provide the basic syntax that does work in a "normal" component.

UPDATE

Setting an @Input property like this:

@Input() callFunction: Function;

Cannot be bound like this:

<button type="{{ type }}" class="{{ class }}" (click)="{{callFunction}}">

Input properties are set using property binding:

<button type="{{ type }}" class="{{ class }}" [callFunction]="someFunction">

If you want to use event binding, you'll need to define an @Output property.

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • Hi, Thanks for the update, could you show how I would use the `@Output` property in my case (taking into account how I am importing the content in the **edit** section) ? Would greatly appreciate it. – Muhammad Hamza Feb 25 '19 at 08:29
  • 1
    Trying to call a function by its string name is beyond my experience. You could check out this: https://stackoverflow.com/questions/359788/how-to-execute-a-javascript-function-when-i-have-its-name-as-a-string And see if anything in there helps. – DeborahK Feb 25 '19 at 16:47