0

Suppose I have a component called ButtonComponent which will be used in various places in the application, so I make is as generic as possible, like so:

button.component.ts

import { Component, ViewEncapsulation, Input, Output, EventEmitter } 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{

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

button.component.html

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

Now my button is completely customizable (in theory). I am now going to import it to a login component which has a function called login(). I want my button instance to run this specific function when I click it:

login.component.ts

//imports

/**
* This component is rendered at the start of application, it provides the UI
* & functionality for the login page.
*/
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})

/**
* This class is used to build a login form along with initialization of validators
* as well as authenticate the user, and reroute upon success
*/
export class LoginComponent implements OnInit, AfterContentInit{

  @ViewChild('login', { read: ViewContainerRef }) login_button;


  /**
  * This property initializes the formGroup element.
  */
  userForm: FormGroup;

  /**
  * The constructor initializes Router, FormBuilder, OauthService, LoggerService, ToastrService
  * & TranslatePipe in the component.
  */
  constructor(//initializations
  ) { }

  /**
  * This is the hook called on the initialization of the component, it initializes
  * the form.
  */
  ngOnInit() {
    this.buildForm();
  }



  /**
   * This method initialized the the formGroup element. Its properties and the validators.
   *
   * @method buildForm
   * @return
   */
  buildForm() { 
    // validations
  });
  }

   /**
   * This method returns the values of the form controls.
   *
   * @return
   */
  get form() { return this.userForm.controls; }

   /**
   * This method is triggered on success, it reroutes the user to main page.
   *
   * @return
   */
  onSuccess() {
    let result = this.translate.transform("pages[login_page][responses][success]");
    this.logger.info(result);
    this.toastr.success(result);
    this.router.navigate(['main']);
  }

   /**
   * This method is triggered when user clicks log-in, it calls the aunthenication method
   * from oauth service.
   *
   * @return
   */
  login() {
    this.oauth.authenticateUser(this.form.username.value, this.form.password.value, this.onSuccess.bind(this));
  }

  ngAfterContentInit() { //here I build my login button instance after init
    this.buildLoginButton();
  }


  /**
  * This function builds the login button, imports the ButtonComponent
  *
  */
  buildLoginButton(){
    let data = {
      type: "button",
      class: "btn btn-primary px-4",
      description: this.translate.transform("pages[login_page][login_form][buttons][login]"),
      function: "login",
      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 = function(){ //I call parent function using a static method
      LoginComponent.executeMethod(data.function);
    }
  }


  static executeMethod(someMethod){ //for my login button this should return this.login()
    eval("this."+someMethod+"()");
  }

}

To make the button instance visible I add the reference into my login template like this:

<div #login></div>

Now my button is visible, great! But now when i click the button:

ERROR TypeError: this.login is not a function at eval (eval at push../src/app/views/login/login.component.ts.LoginComponent.executeMethod (login.component.ts:225), :1:6) at Function.push../src/app/views/login/login.component.ts.LoginComponent.executeMethod (login.component.ts:225) at ButtonComponent.loginButton.instance.callFunction (login.component.ts:179) at Object.eval [as handleEvent] (ButtonComponent.html:2) at handleEvent (core.js:10251) at callWithDebugContext (core.js:11344) at Object.debugHandleEvent [as handleEvent] (core.js:11047) at dispatchEvent (core.js:7710) at core.js:8154 at HTMLButtonElement. (platform-browser.js:988)

How do I make my button run the function in the parent component instead of looking for the function within itself? I don't want to change a lot in the ButtonComponent that would make it less generic as I have to make other buttons as well that would probably run other functions.

There was a solution that stated using EventEmitter for this, but I am unsure how this would work given how I am importing the button into the login component, both the ts and the html

Edit the complete login.component.html:

<div class="app-body">
  <main class="main d-flex align-items-center">
    <div class="container center">
      <div class="row">
        <div class="col-md-8 mx-auto">
          <div class="card-group">
            <div class="card p-4">
              <div class="card-body">
                <form [formGroup]="userForm" (submit)="login()">
                  <h1>{{ 'pages[login_page][login_form][labels][login]' | translate }}</h1>
                  <p class="text-muted">{{ 'pages[login_page][login_form][labels][sign_in]' | translate }}</p>
                  <div class="input-group mb-3">
                    <div class="input-group-prepend">
                      <span class="input-group-text"><i class="icon-user"></i></span>
                    </div>
                    <div #username> </div>
                  </div>
                  <div class="input-group mb-4">
                    <div class="input-group-prepend">
                      <span class="input-group-text"><i class="icon-lock"></i></span>
                    </div>
                    <div #password> </div>
                  </div>
                  <div class="row">
                    <div class="col-6">
                      <div #login></div>
                    <!--  <button type="button" class="btn btn-primary px-4" (click)="login()">{{ 'pages[login_page][login_form][buttons][login]' | translate }}</button> -->
                    </div>
                    <div class="col-6 text-right">
                      <div #forgot></div>
                    <!--  <button type="button" class="btn btn-link px-0">{{ 'pages[login_page][login_form][urls][forgot_password]' | translate }}</button>-->
                    </div>
                  </div>
                </form>
              </div>
            </div>
            <div class="card text-white bg-primary py-5 d-md-down-none" style="width:44%">
              <div class="card-body text-center">
                <div>
                  <h2>{{ 'pages[login_page][sign_up_panel][labels][sign_up]' | translate }}</h2>
                  <p>{{ 'pages[login_page][sign_up_panel][labels][new_account]' | translate }}</p>
                  <div #signUp></div>
                <!--  <button type="button" class="btn btn-primary active mt-3">{{ 'pages[login_page][sign_up_panel][buttons][register]' | translate }}</button> -->
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </main>
</div>
Muhammad Hamza
  • 823
  • 1
  • 17
  • 42

1 Answers1

4

Add this code in button.component.ts

@Output() clickFunctionCalled = new EventEmitter<any>();
callFunction() {
    this.clickFunctionCalled.emit();   
  }

No change in button.template.html

Add this code where you use app-button component in html

<app-button  (clickFunctionCalled)="callCustomClickFunction($event)"></app-button>

Add this in login.component.ts

callCustomClickFunction() {
   console.log("custom click called in login");
   this.login();
}

Basically, emit the click event from the child component. Catch the event in the parent component and call the desired function of the parent component. You can also directly call the parent component's function like this

<app-button  (clickFunctionCalled)="login($event)"></app-button>

As you are using dynamic component creator for creating the button component, you need to do something like this, for binding output event

loginButton.instance.clickFunctionCalled.subscribe(data => {
    console.log(data);
});
Nandita Sharma
  • 13,287
  • 2
  • 22
  • 35
  • Hi! Thanks for the solution. One question, how would I be able to refer to the particular instance? For example, I have 2 buttons, each derived from the same button component, and each having different values in their properties. How would I tell the login template here that **this** specific instance refers to the login button? – Muhammad Hamza Feb 26 '19 at 06:33
  • Please refer this for How to use Output in dynamically created component https://stackoverflow.com/questions/42022229/how-to-use-output-in-dynamically-created-component – Nandita Sharma Feb 26 '19 at 06:50
  • Using dynamic components would allow connection to content faster, since only a portion of the website needs to be downloaded when a user first opens it. – Muhammad Hamza Feb 26 '19 at 06:57
  • Also, the implementation gave me an error, please refer to: https://pasteboard.co/I2UwPSP.png – Muhammad Hamza Feb 26 '19 at 06:58
  • Which implmentation gave you the error? Can you plz create a working sample where i can see it ? Also I am not sure about your query asked in first comment. Can you plz elaborate with an example. – Nandita Sharma Feb 26 '19 at 07:11
  • "Using dynamic components would allow connection to content faster, since only a portion of the website needs to be downloaded when a user first opens it" Have you read it somewhere? Can you send me the link to it? – Nandita Sharma Feb 26 '19 at 07:15
  • Hi, I am not very proficient in stackblitz and since my project is a bit large I am trying to create an example that would help understand my issue better, for which I thank your patience. For the second part, here is the source: https://blog.stackpath.com/glossary/lazy-loading/ – Muhammad Hamza Feb 26 '19 at 07:23
  • In my opinion lazy loading and dynamic componets are 2 different things. https://stackoverflow.com/questions/49254388/dynamic-component-loader-vs-lazy-loading – Nandita Sharma Feb 26 '19 at 07:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/189047/discussion-between-noobdev-and-nandita-sharma). – Muhammad Hamza Feb 26 '19 at 07:44