Angular template-driven form, *ngFor with validation.

Objetivo: construir una aplicación que incluya un template-driven form con el uso de ngFor directive y validation.

El form va a tener tres campos: username, password y location. Para la construcción del form se utiliza un component y el template (component view) correspondiente. Los pasos iniciales son:
1ro – Generate the application scaffold.
2do – Create the login component (si se crea con el comando de angular-cli, el component es registrado automáticamente en el root module, app.module.ts)

File estructure:
angular-form/
  |__app/
    |__app.module.ts
    |__app.component.ts
    |__main.ts
    |__no-special-chars.directive.ts
    |__login.ts
    |__views/
      |__app.component.html
    |__login-form/
      |__login.component.ts
      |__login.component.html

Creamos un objeto que represente los campos del formulario.
file login.ts:

export class LoginInfo {
    constructor(
        public username: string,
        public password: string,
        public location?: string
    ) {}
}

Importamos la clase dentro del component login-component.ts.

import { LoginInfo } from '../login'

Luego instanciamos la clase con la variable login, lo que da como resultado un objeto y creamos el array loginKeys con las llaves de login.

@Component({
  selector: 'login',
  templateUrl: 'app/login-form/login.component.html'
})
export class LoginComponent {
    login = new LoginInfo('', '')
    loginKeys = Object.keys(this.login)
    constructor() {}

Dentro del template login.component.html creamos el form que permite visualizar los campos donde introducir la información.

<form/>
    <div *ngFor="let key of loginKeys">
        {{key}}
        <input name={{key}} placeholder="Enter {{key}} Here" [(ngModel)]="login[key]" />
    </div>
    <button (click)="submit()" [disabled]="!myForm.valid">Submit</button>
</form>

La imagen muestra el form creado.

Para añadir la validación con template-diven form se utiliza validación de atributos. Si la validación es diferente para cada campo entonces se requieren varios elementos input. Cada elemento input utilizará la directiva *ngIf para evaluar la llave en cada iteración del ciclo for y determinar si el elemento input será el correspondiente. La validación para cada campo será:
– Username: required y no special characters.
– Password: no special characters.
– Location minlength y no special characters.
La validación no special characters será una custom directive.

El nuevo código html será:


<form>
    <div *ngFor="let key of loginKeys">
        {{key}}
        <input name={{key}} placeholder="Enter {{key}} Here" required *ngIf="key === 'username'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} type="password" placeholder="Enter {{key}} Here" *ngIf="key === 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} placeholder="Enter {{key}} Here" minlength="4" *ngIf="key !== 'username' && key !== 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
    <button (click)="submit()">Submit</button>
</form>

El primer input es creado solo si la variable key se corresponde con “username”. También se puede deshabilitar el botón en caso de que uno de los campos del form no sea válido. Para ello se crea la variable myForm del tipo ngForm que lo representa y se referencia dentro del elemento button.

<form #myForm="ngForm">
    <div *ngFor="let key of loginKeys">
        {{key}}
        <input name={{key}} placeholder="Enter {{key}} Here" required *ngIf="key === 'username'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} type="password" placeholder="Enter {{key}} Here" *ngIf="key === 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} placeholder="Enter {{key}} Here" minlength="4" *ngIf="key !== 'username' && key !== 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
    </div>
    <button (click)="submit()" [disabled]="!myForm.valid">Submit</button>
</form>

Para mostrar mensajes de error en caso de fallos de validación se pueden utilizar dos opciones. La primera opción es directamente en el template a través del objeto myForm. La segunda es utilizando angular lifecicle hooks que detecten cambios en el objeto myForm y que cambien el valor de una variable en el component que se refleje en el template.

Para la primera opción se utiliza el siguiente código:

<form #myForm="ngForm">
    <div *ngFor="let key of loginKeys">
        {{key}}
        <input name={{key}} placeholder="Enter {{key}} Here" required *ngIf="key === 'username'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} type="password" placeholder="Enter {{key}} Here" *ngIf="key === 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} placeholder="Enter {{key}} Here" minlength="4" *ngIf="key !== 'username' && key !== 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
        <!-- solution1 -->
        <div *ngIf="myForm.controls[key]?.errors">
            <div *ngIf="myForm.controls[key]?.errors.required">
                You must enter your username.
            </div>
            <div *ngIf="myForm.controls[key]?.errors.hasSpecialChars">
                You must not use special chars.
            </div>
            <div *ngIf="myForm.controls[key]?.errors.minlength">
                The minimal length is 4 chars.
            </div>
        </div>
    </div>
    <button (click)="submit()" [disabled]="!myForm.valid">Submit</button>
</form>

El primer div element verifica si hay error en el control relacionado con la key actual. En caso positivo entra a valorar cual es el error (required, hasSpecialChars o minlength). Si uno de estos errores existe entonces el div element será creado y el texto mostrado. La siguiente figura muestra el resultado.

En la segunda opción se utiliza AfterViewChecked, un angular lifecicle hook. Primero creamos el objeto donde buscaremos los mensajes a mostrar. Para ello creamos el fichero error-messages.ts.

interface Model {
    [key: string]: {[key: string]: string}
}

export class ErrorMessagesValidation {
    static errorMessages: Model = {
        username: {
            required: 'Username is required.',
            hasSpecialChars: 'No special chars allowed.'
        },
        password: {
            hasSpecialChars: 'No special chars allowed.'
        },
        location: {
            hasSpecialChars: 'No special chars allowed.',
            minlength: 'The minimal length is 4 chars'
        }
    }
}

En el component importamos AfterViewChecked y ErrorMessagesValidation

import { AfterViewChecked } from '@angular/core'
import { ErrorMessagesValidation } from './error-messages' 

Lo implementamos

export class LoginComponent implements AfterViewChecked {
...
    ngAfterViewChecked() {}

Para acceder a myForm en el component utilizamos el siguiente código:

@ViewChild('myForm') public currentForm: NgForm
    public myForm: NgForm

La variable currentForm contiene la referencia a myForm en el template. Adicionalmente creamos otra variable myForm. Llenamos el método AfterViewChecked con el siguiente código:

ngAfterViewChecked() {
        if(this.currentForm === this.myForm) {
            return
        }
        this.myForm = this.currentForm
        if (this.myForm) {
            this.myForm.valueChanges.subscribe(data => { this.onValueChange() })
        }
    }

    onValueChange() {
        if (!this.myForm) { return }
                let form = this.myForm.form
                // console.log(form.get('username'))
                this.loginKeys.forEach((key: string) => {
                    let control = form.get(key)
                    this.formErrors[key] = ''
                    
                    if (control && control.dirty && !control.valid) {
                        let messages = ErrorMessagesValidation.errorMessages[key]
                        let errors = Object.keys(control.errors)
                        
                        errors.forEach((error) => {
                            this.formErrors[key] = messages[error]
                        })
                    }
                })
    }

El método se ejecuta cada vez que ocurra un cambio en el template. myForm se mantiene actualizado, entonces nos suscribimos a su propiedad ValueChanges que devuelve un Observable si existe algún cambio en la misma, que ejecutaría el método onValueChange(), encargado de encontrar el error de acuerdo al control correspondiente a cada input. Encontrado el error, se busca el mensaje correspondiente en ErrorMessagesValidation y guardar el valor en el objeto formErrors, que tiene la siguiente estructura:

formErrors: Model = { 'username': '', 'password': '', 'location': ''}

El código en el template es:

<form #myForm="ngForm">
    <div *ngFor="let key of loginKeys">
        {{key}}
        <input name={{key}} placeholder="Enter {{key}} Here" required *ngIf="key === 'username'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} type="password" placeholder="Enter {{key}} Here" *ngIf="key === 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
        <input name={{key}} placeholder="Enter {{key}} Here" minlength="4" *ngIf="key !== 'username' && key !== 'password'" [(ngModel)]="login[key]" appNoSpecialChars />
        <div>
            {{formErrors[key]}}
        </div>
    </div>
    <button (click)="submit()" [disabled]="!myForm.valid">Submit</button>
</form>

La segunda opción permite obtener el mismo resultado. La primera opción también es válida para angular reactive forms.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.