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>