0

I have created a generic button component in my application that would execute a particular function from the parent component it is imported into. In this specific example I have a login form which will call a login() function upon clicking of that button.

The problem I seem to be having is that every time I click the login button it executes the method twice instead of once. I am not sure why this is happening. There is a forgot password button right next to it which has also been derived from the same template but should have different functionality since I have bound a different function to the click directive but when I click that link it runs the login() function again but this time once.

Here is a screenshot of my login form to get an idea of the layout:

login form

Here is my code:

button.component.ts

import { Component, ViewEncapsulation, Input, Output, OnInit, EventEmitter } from '@angular/core';
import { FormGroup, FormControl } 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;
  @Output() callFunction = new EventEmitter();

  constructor(){ }

  ngOnInit(){
    this.group = new FormGroup({});
  }

  onClick(event){
    this.callFunction.emit(event);
  }

}

button.component.html

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

login.component.ts

import { Component, OnInit, ViewChild, ViewContainerRef, AfterContentInit, ComponentFactoryResolver } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { OauthService } from 'src/app/services/oauth/oauth.service';
import { LoggerService } from 'src/app/services/logger/logger.service';
import { ToastrService } from 'ngx-toastr';
import { TranslatePipe } from 'src/app/pipes/translate/translate.pipe';
import { TextFieldComponent } from 'src/app/components/core/text-field/text-field.component';
import { ButtonComponent } from 'src/app/components/core/button/button.component';

/**
* 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('username', { read: ViewContainerRef }) username;
  @ViewChild('password', { read: ViewContainerRef }) password;



  /**
  * This property initializes the formGroup element.
  */
  userForm: FormGroup;
  /**
  * The constructor initializes Router, FormBuilder, OauthService, LoggerService, ToastrService
  * & TranslatePipe in the component.
  */
  constructor(private router: Router,
    private fb: FormBuilder,
    private oauth: OauthService,
    private logger: LoggerService,
    private toastr: ToastrService,
    private translate: TranslatePipe,
    private resolver: ComponentFactoryResolver) { }
  /**
  * 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() {
    this.userForm = this.fb.group({
      'username': ['', Validators.required],
      'password': ['', Validators.required],
    });
  }
  /**
   * 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() { console.log("i am called00");
    this.oauth.authenticateUser(this.form.username.value, this.form.password.value, this.onSuccess.bind(this));
  }

  ngAfterContentInit() {
    this.buildUserField();
    this.buildPasswordField();
  }




  /**
  * This function builds the username field, imports the TextFieldComponent
  *
  */
  buildUserField(){
    let data = {
      type: "text",
      class: "form-control",
      placeholder: this.translate.transform("pages[login_page][login_form][placeholders][username]"),
      id: "username",
      autoComplete: "username",
      formControlName: "username",
      group: this.userForm
      }
    const inputFactory = this.resolver.resolveComponentFactory(TextFieldComponent);
    const usernameField = this.username.createComponent(inputFactory);
    usernameField.instance.group = data.group;
    usernameField.instance.type = data.type;
    usernameField.instance.class = data.class;
    usernameField.instance.placeholder = data.placeholder;
    usernameField.instance.id = data.id;
    usernameField.instance.autoComplete = data.autoComplete;
    usernameField.instance.formControlName = data.formControlName;
  }

  /**
  * This function builds the password field, imports the TextFieldComponent
  *
  */
  buildPasswordField(){
    let data = {
      type: "password",
      class: "form-control",
      placeholder: this.translate.transform("pages[login_page][login_form][placeholders][password]"),
      id: "password",
      autoComplete: "password",
      formControlName: "password",
      group: this.userForm
      }
    const inputFactory = this.resolver.resolveComponentFactory(TextFieldComponent);
    const passwordField = this.password.createComponent(inputFactory);
    passwordField.instance.group = data.group;
    passwordField.instance.type = data.type;
    passwordField.instance.class = data.class;
    passwordField.instance.placeholder = data.placeholder;
    passwordField.instance.id = data.id;
    passwordField.instance.autoComplete = data.autoComplete;
    passwordField.instance.formControlName = data.formControlName;
  }


  /**
  * This function builds the login button functionality, imports the ButtonComponent
  *
  */
  buildLoginButton(){
    this.login();
  }


  /**
  * This function builds the forgot password link functionality, imports the ButtonComponent
  *
  */
  buildForgotPasswordButton(){
  }

  /**
  * This function builds the sign up button functionality, imports the ButtonComponent
  *
  */
  buildSignUpButton(){
  }



}

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">
                    <app-button description="{{ 'pages[login_page][login_form][buttons][login]' | translate }}" class="btn btn-primary px-4" (callFunction)="buildLoginButton()"></app-button>
                    <!--  <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">
                      <app-button description="{{ 'pages[login_page][login_form][urls][forgot_password]' | translate }}" class="btn btn-link px-0" (callFunction)="buildForgotPasswordButton()"></app-button>
                    <!--  <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>
                      <app-button description="{{ 'pages[login_page][sign_up_panel][buttons][register]' | translate }}" class="btn btn-primary active" (callFunction)="buildSignUpButton()"></app-button>
                <!--  <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>

SIDE NOTE: (Although this is not technically part of the question title) I am using CoreUI for styling the form, which is imported into my global styling file and worked fine when I was not using reusable components for text fields and buttons, but now when I have started using those the child components somehow are not inheriting the CoreUI styles, just plain bootstrap, if someone could pretty please help me with that I would really appreciate it :)

5
  • 2
    excuse me for asking, but apart from it not working, what is the benefit of such complex construction over a simple variant for such simple task. Commented Mar 12, 2019 at 9:39
  • The login form is just a part of the application, this would become a complete portal that includes multiple forms, designs and views requiring many reusable components. I could later reuse those components for multiple purposes. The current implementation of building these fields is only for testing purposes, I would be creating a service to remove this dirty work. Commented Mar 12, 2019 at 9:43
  • Ok understood, but in the end you use a click handler to emit a value to a variable function you provide. so the only thing you are doing in move the function from the code to the markup... If you need more complex logic later, it will become a mess IMHO Commented Mar 12, 2019 at 9:46
  • Could you please present some specific use case for this button that may cause the implementation go haywire? Just a simple use case would do fine, it would really help me understand what I am dealing with here. Commented Mar 12, 2019 at 9:55
  • I need to wrap my head around it myself to find one. maybe constructions with rxjs / async / Observables / Subjects / multiple function calls (one will be visible the other hidden in the function). I don't say it's impossible, or it cannot be hidden somehow. it just seems an extra step, which doesn't add much value Commented Mar 12, 2019 at 10:05

2 Answers 2

3

in your form you have a submit callback for login()

<form [formGroup]="userForm" (submit)="login()">

and in your submit button which has a callback buildLoginButton which is calling login() is the reason why your issue occurred.

doing your form like this should resolve your problem

<form [formGroup]="userForm">

Sign up to request clarification or add additional context in comments.

3 Comments

This will remove the possibility to login by hitting "Enter" after filling the fields. The type of the button should be changed instead.
I've been slaving over this problem for so long and never noticed this naive error. Thanks!
np, glad to help :)
1

Your button have type "text". That is not a valid value. It should be "button|submit|reset" (see doc for "type" attribute)

Here, it default to "submit", hence calling the submit callback of your form (login()).

You should change the type of the button to button.

1 Comment

Hi, thanks for the answer, although if I change the type the styling goes haywire (which for some reason I do not understand why the child component is not inheriting from the global file)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.