Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fix: Angular 7 (SmartAdmin template) form validation stops working after navigation

I am trying to perform form validation on an angular 7 app that is using the Smart Admin Template (Theme from wrapbootstrap).

My issue is that it is working as expected the first time the browser refreshes, or even when i navigate to a component that does not include another form. The issue arises when i navigate to a component that also includes a form with validation options of its own.

In addition, the actual "Validity" status of the form still works as expected. It just doesn't shows the bootstrap classes and messages on the form.

I have tried resetting the form, resetting any async / not async validators and anything else i could think of.

Finally, there are no errors or anything during the navigation between the components.

This is my main module that handles the navigation (main.routing.ts):

import { Routes, RouterModule } from '@angular/router';
import { ModuleWithProviders } from "@angular/core";
import { MainLayoutComponent } from '@app/shared/layout/app-layouts/main-layout.component';

import { MainComponent } from "./main.component";
import { SettingsComponent } from '../settings/settings.component';
import { GetAccountsComponent } from '../administration/get-accounts/get-accounts.component';
import { GetUsersComponent } from '../administration/get-users/get-users.component';
import { CreateAccountComponent } from '../administration/create-account/create-account.component';
import { CreateUserComponent } from '../administration/create-user/create-user.component';

import { GetTeamsComponent } from '../user/get-teams/get-teams.component';
import { GetGamesComponent } from '../user/get-games/get-games.component';
import { CreateTeamComponent } from '../game/create-team/create-team.component';
import { CreateRoundComponent } from '../game/create-round/create-round.component';
import { CreateRoundBetComponent } from '../game/create-round-bet/create-round-bet.component';
import { CreateGameComponent } from '../game/create-game/create-game.component';

export const mainRoutes: Routes = [
  {
    path: '',
    component: MainLayoutComponent,
    children: [
      {
        path: "",
        redirectTo: "dashboard",
        pathMatch: "full"
      },
      {
        path: "dashboard",
        component: MainComponent,
        data: { pageTitle: "Dashboard" }
      },
      {
        path: "settings",
        component: SettingsComponent,
        data: { pageTitle: "Settings" }
      },
      {
        path: "administration/getusers",
        component: GetUsersComponent,
        data: { pageTitle: "Get Users" }
      },
      {
        path: "administration/getaccounts",
        component: GetAccountsComponent,
        data: { pageTitle: "Get Accounts" }
      },
      {
        path: "administration/createaccount",
        component: CreateAccountComponent,
        data: { pageTitle: "Create Account" }
      },
      {
        path: "administration/createuser",
        component: CreateUserComponent,
        data: { pageTitle: "Create User" }
      },
      {
        path: "user/getteams",
        component: GetTeamsComponent,
        data: { pageTitle: "Get Teams" }
      },
      {
        path: "user/getgames",
        component: GetGamesComponent,
        data: { pageTitle: "Get Games" }
      },
      {
        path: "game/createteam",
        component: CreateTeamComponent,
        data: { pageTitle: "Create Team" }
      },
      {
        path: "game/createround",
        component: CreateRoundComponent,
        data: { pageTitle: "Create Round" }
      },
      {
        path: "game/createroundbet",
        component: CreateRoundBetComponent,
        data: { pageTitle: "Create Round Bet" }
      },
      {
        path: "game/creategame",
        component: CreateGameComponent,
        data: { pageTitle: "Create Game" }
      }
    ]
  }
];

export const mainRouting: ModuleWithProviders = RouterModule.forChild(mainRoutes);

This is an example of a form (create-team.component.html):

<form id="checkout-form"
        name="createTeamForm"
        class="smart-form"
        [saUiValidate]="validationOptions"
        novalidate="novalidate"
        [formGroup]="createTeamForm"
        (ngSubmit)="onSubmit()">

    <fieldset>
      <div class="row">
        <section class="col col-4">
          <label class="select">
            <select name="firstPerson"
                    formControlName="firstPerson">
              <option value="0"
                      selected=""
                      disabled="">First Person</option>
              <option value="{{user.id}}"
                      *ngFor="let user of users">{{user.email}}</option>
            </select> <i></i> </label>
        </section>
        <section class="col col-4">
          <label class="select">
            <select name="secondPerson"
                    formControlName="secondPerson">
              <option value="0"
                      selected=""
                      disabled="">Second Person</option>
              <option value="{{user.id}}"
                      *ngFor="let user of users">{{user.email}}</option>
            </select> <i></i> </label>
        </section>
      </div>
    </fieldset>

    <footer>
      <button type="submit"
              class="btn btn-primary">
        Create Team
      </button>
    </footer>
  </form>

And the .ts file that includes the validation options:

import { Component, OnInit } from '@angular/core';
import { ApiService } from '@app/core/services';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

@Component({
  selector: 'app-create-team',
  templateUrl: './create-team.component.html',
  styleUrls: ['./create-team.component.css']
})
export class CreateTeamComponent implements OnInit {
  public teamCreateSuccess: boolean;

  public users: any;
  public hasSubmitted: boolean;

  public errorMessage: string;
  public successMessage: string;

  public validationOptions = {
    rules: {
      firstPerson: {
        required: true
      },
      secondPerson: {
        required: true
      }
    },

    // Messages for form validation
    messages: {
      firstPerson: {
        required: 'Please select the first person'
      },
      secondPerson: {
        required: 'Please select the second person'
      },
    },
    submitHandler: this.onSubmit
  };

  createTeamForm: FormGroup

  constructor(
    private apiService: ApiService
  ) { }

  ngOnInit() {
    console.log('Create Team Loaded');

    this.apiService.userGetUsers()
      .subscribe(
        data => {
          this.users = data;
          // console.log(this.roles);
        },
        error => {
          this.onCreateTeamError(error);
        }
      );

    this.teamCreateSuccess = null;
    this.hasSubmitted = null;

    this.createTeamForm = new FormGroup({
      firstPerson: new FormControl('0', Validators.required),
      secondPerson: new FormControl('0', Validators.required),
    });
  }

  onSubmit() {
    this.hasSubmitted = true;

    if (this.createTeamForm) {
      console.log(this.createTeamForm.status);
      console.log(this.createTeamForm.controls);

      if (this.createTeamForm.status == 'VALID') {
        this.apiService.createTeam(this.createTeamForm).pipe(first())
          .subscribe(
            data => {
              this.onCreateTeamSuccess(data);
            },
            error => {
              //console.log("failed identity check");
              this.onCreateTeamError(error);
              //console.log(error);
            }
          );
      }
    }
  }

  onCreateTeamSuccess(data: any) {
    this.teamCreateSuccess = true;
    this.successMessage = "Successfully created team with Id: " + data.id;
    console.log(data);
  }

  onCreateTeamError(error: any) {
    this.teamCreateSuccess = false;

    this.errorMessage = "Failed to create team due to error: " + error.error;
    console.log(error);
  }
}

EDIT: To provide some more insight, here is the custom validation that SmartAdmin template is using:

import { Directive, Input, ElementRef } from "@angular/core";

@Directive({
  selector: "[saUiValidate]"
})
export class UiValidateDirective {
  @Input() saUiValidate: any;

  constructor(private el: ElementRef) {
    Promise.all([
      import("jquery-validation/dist/jquery.validate.js"),
      import("jquery-validation/dist/additional-methods.js")
    ])
    .then(() => {
      this.attach();
    });
  }

  attach() {
    const $form = $(this.el.nativeElement);
    const validateCommonOptions = {
      rules: {},
      messages: {},
      errorElement: "em",
      errorClass: "invalid",
      highlight: (element, errorClass, validClass) => {
        $(element)
          .addClass(errorClass)
          .removeClass(validClass);
        $(element)
          .parent()
          .addClass("state-error")
          .removeClass("state-success");
      },
      unhighlight: (element, errorClass, validClass) => {
        $(element)
          .removeClass(errorClass)
          .addClass(validClass);
        $(element)
          .parent()
          .removeClass("state-error")
          .addClass("state-success");
      },

      errorPlacement: (error, element) => {
        if (element.parent(".input-group").length) {
          error.insertAfter(element.parent());
        } else {
          error.insertAfter(element);
        }
      }
    };

    $form
      .find("[data-smart-validate-input], [smart-validate-input]")
      .each(function() {
        var $input = $(this),
          fieldName = $input.attr("name");

        validateCommonOptions.rules[fieldName] = {};

        if ($input.data("required") != undefined) {
          validateCommonOptions.rules[fieldName].required = true;
        }
        if ($input.data("email") != undefined) {
          validateCommonOptions.rules[fieldName].email = true;
        }

        if ($input.data("maxlength") != undefined) {
          validateCommonOptions.rules[fieldName].maxlength = $input.data(
            "maxlength"
          );
        }

        if ($input.data("minlength") != undefined) {
          validateCommonOptions.rules[fieldName].minlength = $input.data(
            "minlength"
          );
        }

        if ($input.data("message")) {
          validateCommonOptions.messages[fieldName] = $input.data("message");
        } else {
          Object.keys($input.data()).forEach(key => {
            if (key.search(/message/) == 0) {
              if (!validateCommonOptions.messages[fieldName])
                validateCommonOptions.messages[fieldName] = {};

              var messageKey = key.toLowerCase().replace(/^message/, "");
              validateCommonOptions.messages[fieldName][
                messageKey
              ] = $input.data(key);
            }
          });
        }
      });

    $form.validate($.extend(validateCommonOptions, this.saUiValidate));
  }
}

EDIT 2: I have managed to find a workaround for this issue, even though i kind of don't like it. It seems that when the custom UI Validation is called, the component is not yet rendered (I am guessing it has something to do with running asynchronously). The solution was to add in the validation component a 'setTimeout' of 0ms as shown below:

  constructor(private el: ElementRef) {
    Promise.all([
      import("jquery-validation/dist/jquery.validate.js"),
      import("jquery-validation/dist/additional-methods.js")
    ])
      .then(() => {
        setTimeout(_ => {
          this.attach();
        }, 0);
      });
  }

If anyone can think of a better solution, it would be much appreciated :)

Looking forward to hearing your ideas.

like image 486
Panayiotis Alexandrou Avatar asked Nov 06 '22 19:11

Panayiotis Alexandrou


1 Answers

So, I was experiencing the same problem as you, and also tried your method. While it does work, my friend, Scott noticed that by moving the logic to the ngOnInit() method (without your modification), it also works. I believe it has to do with the fact that ngOnInit waits for the component to fully render and initialize before it is called, which in turn makes sure the bindings are available.

Here is the relevant code I used to make it work (everything before the attach() method). This is in ui-validate.directive.ts:

import { Directive, Input, ElementRef, OnInit } from "@angular/core";

@Directive({
  selector: "[saUiValidate]"
})
export class UiValidateDirective implements OnInit {
  @Input() saUiValidate: any;

  constructor(private el: ElementRef) { }

  ngOnInit(){
    Promise.all([
      import("jquery-validation/dist/jquery.validate.js"),
      import("jquery-validation/dist/additional-methods.js")
    ])
    .then(() => {
      this.attach();
    });
  }
like image 184
FranksBrain Avatar answered Nov 15 '22 06:11

FranksBrain