2

I am trying to implement a component factory to dynamically create my login form. Initially I had the issue of the FormGroup not being identified by the form where I was adding my component. Finally when I got that to pass using the solution provided here I ran into another error:

TypeError: Cannot create property 'validator' on string 'userForm'

This error pops up in the console when I hit ng serve and browse my login page, which is accompanied by a very odd layout of the form (my form was working perfectly when I had inserted the input within the form itself instead of trying to add the component) as shown here:

first load

As you can see the component (the first input field) is not even styled like the second input field (directly added to the form) and upon clicking one or two times, the form (almost) turns back to it's original shape with another error in the console:

TypeError: this.form.get is not a function

After

I am unsure as to why this is happening and whether the solution I have used to solve the FormGroup issue may be causing the problem.

Here is my code:

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';

/**
* 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('input', { read: ViewContainerRef }) container;

  /**
  * 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() {
    this.oauth.authenticateUser(this.form.username.value, this.form.password.value, this.onSuccess.bind(this));
  }

  ngAfterContentInit() {
    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: "userForm"
    }
    const inputFactory = this.resolver.resolveComponentFactory(TextFieldComponent);
    const usernameField = this.container.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;
  }

}

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 #input> </div>
                  <!--  <input type="text" class="form-control" placeholder="{{ 'pages[login_page][login_form][placeholders][username]' | translate }}"  id="username" autocomplete="username" formControlName="username"> -->
                  </div>
                  <div class="input-group mb-4">
                    <div class="input-group-prepend">
                      <span class="input-group-text"><i class="icon-lock"></i></span>
                    </div>
                    <input type="password" class="form-control" placeholder="{{ 'pages[login_page][login_form][placeholders][password]' | translate }}" id="password" autocomplete="current-password" formControlName="password" >
                  </div>
                  <div class="row">
                    <div class="col-6">
                      <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">
                      <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>
                  <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>

text-field.component.html

<div [formGroup]="group">
  <input type="{{ type }}" class="{{ class }}" placeholder="{{ placeholder }}" id="{{ id }}" autocomplete="{{ autoComplete }}" formControlName="{{ formControlName }}">  
</div>

text-field.component.ts

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

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

  @Input() group: FormGroup;
  @Input() type: string;
  @Input() placeholder: string;
  @Input() class: string;
  @Input() id: string;
  @Input() autoComplete: string;
  @Input() formControlName: string;


  constructor() {}

  ngOnInit() {}

}

Summary

expected: Form should be properly work with dynamic components

actual: Form giving errors and styling also not working

Abhishek
  • 1,742
  • 2
  • 14
  • 25
Muhammad Hamza
  • 823
  • 1
  • 17
  • 42

1 Answers1

2

You are assigning a string "userForm" to group attribute, where as you are supposed to assign the actual form this.userForm.

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              // see this line
    }
Amit Chigadani
  • 28,482
  • 13
  • 80
  • 98
  • Worked like a charm! A very naive observation on my part. However, the styling for some reason still is a bit off (the username field is somewhat smaller than the password field). Although I know it's not directly part of the question asked, any idea what may be causing this? Thanks – Muhammad Hamza Feb 22 '19 at 05:43
  • You can try using the same class `class="input-group mb-4"` for username aswell. You are using `mb-3` over there. – Amit Chigadani Feb 22 '19 at 05:49