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:
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
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