markdown 使用Bootstrap样式验证角度反应形式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了markdown 使用Bootstrap样式验证角度反应形式相关的知识,希望对你有一定的参考价值。

# Validation Angular Reactive Form with Bootstrap styling

[SOURCE](http://jasonwatmore.com/post/2018/05/10/angular-6-reactive-forms-validation-example), [SOURCE](https://github.com/angular/vscode-ng-language-service/issues/126)

This works for `Angular 6.0.3` and `Bootstrap 4.1.1`

This form validate on submit rather than as soon as each field is changed, this is implemented with a `submitted` field in the app component that is set to true when the form is submitted for the first time.

Styling of the example is all done with Bootstrap 4 CSS.

The component:

```typescript
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 
@Component({
    selector: 'app',
    templateUrl: 'app.component.html'
})
 
export class AppComponent implements OnInit {
    registerForm: FormGroup;
    submitted = false;
 
    constructor(private formBuilder: FormBuilder) { }
 
    ngOnInit() {
        this.registerForm = this.formBuilder.group({
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            email: ['', [Validators.required, Validators.email]],
            password: ['', [Validators.required, Validators.minLength(6)]]
        });
    }
 
    // convenience getter for easy access to form fields
    // you can access the email field in the template using f.email instead of registerForm.controls.email
    get f() { return this.registerForm.controls; }
 
    onSubmit() {
        this.submitted = true;
 
        // stop here if form is invalid
        if (this.registerForm.invalid) {
            return;
        }
 
        alert('SUCCESS!! :-)')
    }
}
```

The form binds the form submit event to the `onSubmit()` handler in the app component using the Angular event binding `(ngSubmit)="onSubmit()"`. Validation messages are displayed only after the user attempts to submit the form for the first time, this is controlled with the submitted property of the app component.

The template:

```html
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label>First Name</label>
    <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
    <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
      <div *ngIf="f.firstName.errors.required">First Name is required</div>
    </div>
  </div>
  <div class="form-group">
    <label>Last Name</label>
    <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
    <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
        <div *ngIf="f.lastName.errors.required">Last Name is required</div>
    </div>
  </div>
  <div class="form-group">
      <label>Email</label>
      <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
      <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
          <div *ngIf="f.email.errors.required">Email is required</div>
          <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
      </div>
  </div>
  <div class="form-group">
      <label>Password</label>
      <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
      <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
          <div *ngIf="f.password.errors.required">Password is required</div>
          <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
      </div>
  </div>
  <div class="form-group">
      <button [disabled]="loading" class="btn btn-primary">Register</button>
  </div>
</form>
```

**NOTE:** 
  If tsLint complain about `*ngIf="submitted && f.firstName.errors"` you can bypass it by using either:
  
```html
  *ngIf="submitted && f['firstName'].errors"
  
  or
  
  *ngIf="submitted && !!f.firstName.errors"
```

or just ignore it, it can run just fine. See ([SOURCE](https://github.com/angular/vscode-ng-language-service/issues/126)) for more details.

## Another Example

Using a util class to prevent repeating condition check. Also, this one will check for validation as long as the fields is touched. Notice the `.custom-select` for select is from [Bootstrap](https://getbootstrap.com/docs/4.0/components/forms/#select-menu).

The component:

```typescript
@Component({
  selector: 'app-checkout',
  templateUrl: './checkout.component.html',
  styleUrls: ['./checkout.component.scss']
})
export class CheckoutComponent implements OnInit {
  private readonly PHONE_NUMBER_REGEX = '^[+]?[0-9]{8,20}$'; // https://stackoverflow.com/a/6358825/1602807
  private readonly EMAIL_REGEX = '[a-zA-Z0-9.-_]{1,}@[a-zA-Z.-]{2,}[.]{1}[a-zA-Z]{2,}'; // https://stackoverflow.com/a/46440357/1602807
  
  form: FormGroup;

  readonly address = 'address';
  readonly district = 'district';
  readonly city = 'city';
  readonly name = 'name';
  readonly email = 'email';
  readonly phoneNumber = 'phoneNumber';
  
  private readonly phoneNumberValidators = Validators.compose([
    Validators.required,
    Validators.pattern(this.PHONE_NUMBER_REGEX)
  ]);

  private readonly emailValidators = Validators.compose([
    Validators.required,
    Validators.pattern(this.EMAIL_REGEX)
  ]);

  constructor(formBuilder: FormBuilder) {
    this.createForm(formBuilder);
  }

  private createForm(formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      [this.address]: ['', Validators.required],
      [this.district]: ['', Validators.required],
      [this.city]: [''],
      [this.name]: ['', Validators.required],
      [this.email]: ['', this.emailValidators],
      [this.phoneNumber]: ['', this.phoneNumberValidators],
    });
  }
  
  private restoreFormIfPossible() {
    const info = this.checkoutInfoDao.get();
    if (info) {
      this.form.get(this.address).setValue(info.address);
      this.form.get(this.district).setValue(info.district.value.toString());
      //this.form.get(this.city).setValue(info.city.value);
      this.form.get(this.name).setValue(info.name);
      this.form.get(this.email).setValue(info.email);
      this.form.get(this.phoneNumber).setValue(info.phoneNumber);
    }
  }

  isFieldInvalid(field: string) {
    return FormUtil.isFieldInvalid(this.form, field);
  }

  addInvalidCSSClass(field: string) {
    return FormUtil.addInvalidCSSClass(this.form, field);
  }

  addInvalidCSSClassForSelect(field: string) {
    return FormUtil.addInvalidCSSClassForSelect(this.form, field);
  }

  isFieldRequired(field: string) {
    return FormUtil.isFieldRequired(this.form, field);
  }

  isFieldEmpty(field: string) {
    return FormUtil.isFieldEmpty(this.form, field);
  }

  isSelectedOptionInvalid(field: string) {
    return FormUtil.isSelectedOptionInvalid(this.form, field);
  }

  submit() {
    // stop here if form is invalid
    if (this.form.invalid) {
      return;
    }

    alert('SUCCESS!! :-)');
  }
}
```

The Util:

```typescript
export class FormUtil {
  static isFieldInvalid(form: FormGroup, field: string) {
    return form.get(field).invalid && form.get(field).touched;
  }

  static addInvalidCSSClass(form: FormGroup, field: string) {
    return {
      'is-invalid': this.isFieldInvalid(form, field),
    };
  }

  static isSelectedOptionInvalid(form: FormGroup, selectName: string) {
    const value = Number(form.get(selectName).value);
    return (value === 0 || isNaN(value)) && form.get(selectName).touched;
  }

  static addInvalidCSSClassForSelect(form: FormGroup, selectName: string) {
    return {
      'is-invalid': this.isSelectedOptionInvalid(form, selectName),
    };
  }

  static isFieldRequired(form: FormGroup, field: string) {
    return form.get(field).errors.required;
  }

  static isFieldEmpty(form: FormGroup, field: string) {
    return !form.get(field).value;
  }
}
```

The template:

```html
<!--Delivery Address Section-->
  <hr class="separator">
  <span class="section-title-number">01</span><span class="section-title">{{ 'check_out.delivery_title' | translate }}</span>

  <form class="section-form" [formGroup]="form" (ngSubmit)="onSubmit()">
    <div class="form-group">
      <input type="text" class="form-control"
             [autofocus] //custom directive to auto focus on this field
             [placeholder]="'check_out.address' | translate"
             [formControlName]="address"
             [ngClass]="addInvalidCSSClass(address)">
      <div *ngIf="isFieldInvalid(address)" class="invalid-feedback">
        <div *ngIf="isFieldRequired(address)">{{ 'check_out.error_field_address' | translate }}</div>
      </div>
    </div>
    <div class="form-row">
      <div class="col">
        <select class="custom-select custom-select-lg"
                [formControlName]="district"
                [ngClass]="addInvalidCSSClassForSelect(district)"
                [class.semi-transparent]="isFieldEmpty(district)">
          <option hidden>{{ 'check_out.district' | translate }}</option>
          <option *ngFor="let dist of districtList" [value]="dist.value">{{ dist.text | translate }}</option>
        </select>
        <div *ngIf="isSelectedOptionInvalid(district)" class="invalid-feedback">
          <div *ngIf="isSelectedOptionInvalid(district)">{{ 'check_out.error_field_district' | translate }}</div>
        </div>
      </div>
      <div class="col">
        <select class="custom-select custom-select-lg"
                [formControlName]="city">
          <option selected>{{ selectedCity.text | translate }}</option>
        </select>
      </div>
    </div>
  </form>
  <!--End Delivery Address Section-->
  
  <!--Contact Information Section-->
  <hr class="separator">
  <span class="section-title-number">03</span><span class="section-title">{{ 'check_out.contact_info_title' | translate }}</span>

  <form class="section-form" [formGroup]="form" (ngSubmit)="onSubmit()">
    <div class="form-group">
      <input type="text" class="form-control"
             [placeholder]="'check_out.name' | translate"
             [formControlName]="name"
             [ngClass]="addInvalidCSSClass(name)">
      <div *ngIf="isFieldInvalid(name)" class="invalid-feedback">
        <div *ngIf="isFieldRequired(name)">{{ 'check_out.error_field_name' | translate }}</div>
      </div>
    </div>
    <div class="form-group">
      <input type="text" class="form-control"
             [placeholder]="'check_out.email' | translate"
             [formControlName]="email"
             [ngClass]="addInvalidCSSClass(email)">
      <div *ngIf="isFieldInvalid(email)" class="invalid-feedback">
        <div *ngIf="isFieldRequired(email)">{{ 'check_out.error_field_email' | translate }}</div>
        <div *ngIf="!isFieldEmpty(email)">{{ 'check_out.error_field_email_invalid' | translate }}</div>
      </div>
    </div>
    <div class="form-group">
      <input type="text" class="form-control"
             [placeholder]="'check_out.phone_number' | translate"
             [formControlName]="phoneNumber"
             [ngClass]="addInvalidCSSClass(phoneNumber)">
      <div *ngIf="isFieldInvalid(phoneNumber)" class="invalid-feedback">
        <div *ngIf="isFieldRequired(phoneNumber)">{{ 'check_out.error_field_phone_number' | translate }}</div>
        <div *ngIf="!isFieldEmpty(phoneNumber)">{{ 'check_out.error_field_phone_number_invalid' | translate }}</div>
      </div>
    </div>
  </form>
  <!--End Contact Information Section-->
  <button type="submit" class="btn btn-primary" id="submit-button" (click)="submit()"
          [disabled]="!form.valid">
    {{ 'check_out.submit_button_text' | translate }}
  </button>
```

The SCSS:

```css
@import "color";
@import "typo";

#container {
  padding-top: 70px;
  width: 650px;
  height: auto;
  display: block;
  margin-left: auto;
  margin-right: auto;

  #title {
    font-size: 36px;
  }

  #container-with-padding {
    padding: 34px 25px;
    margin-top: 14px;
    background-color: $color-light-gray;

    #hr-subtotal {
      margin: 1px 0 21px;
    }

    #subtotal {
      @extend .font-bold;
      color: $color-text-lighter-black;
      line-height: 2;
      margin-bottom: -10px;
    }

    #total-price {
      @extend .font-bold;
      color: $color-text-lighter-black;
      text-align: right;
      margin-bottom: -10px;
    }
  }

  .separator {
    margin: 40px 0 30px;
  }

  .section-base-style {
    @extend .font-bold;
    font-size: 24px;
  }

  .section-title-number {
    @extend .section-base-style;
    @extend .text-green;
  }

  .section-title {
    @extend .section-base-style;
    @extend .text-primary-black;
    padding-left: 15px;
  }

  .section-form {
    margin-top: 30px;
  }

  //region Form Custom Styling
  $form-font-size: 16px;
  $form-font-opacity: 0.5;

  .form-control {
    @extend .font-bold;
    color: $color-text-lighter-black;
    font-size: $form-font-size;
  }

  .form-control::-webkit-input-placeholder {
    font-family: AvenirNext-Bold;
    color: $color-text-lighter-black;
    opacity: $form-font-opacity;
    font-size: $form-font-size;
  }  /* WebKit, Blink, Edge */
  .form-control:-moz-placeholder {
    font-family: AvenirNext-Bold;
    color: $color-text-lighter-black;
    opacity: $form-font-opacity;
    font-size: $form-font-size;
  }  /* Mozilla Firefox 4 to 18 */
  .form-control::-moz-placeholder {
    font-family: AvenirNext-Bold;
    color: $color-text-lighter-black;
    opacity: $form-font-opacity;
    font-size: $form-font-size;
  }  /* Mozilla Firefox 19+ */
  .form-control:-ms-input-placeholder {
    font-family: AvenirNext-Bold;
    color: $color-text-lighter-black;
    opacity: $form-font-opacity;
    font-size: $form-font-size;
  }  /* Internet Explorer 10-11 */
  .form-control::-ms-input-placeholder {
    font-family: AvenirNext-Bold;
    color: $color-text-lighter-black;
    opacity: $form-font-opacity;
    font-size: $form-font-size;
  }  /* Microsoft Edge */

  .is-invalid {
    color: #E53A40;
  }
  //endregion Form Custom Styling

  #payment-type-container{
    margin-top: 19px;
  }

  .custom-select {
    @extend .font-bold;
    color: $color-text-lighter-black;
    font-size: $form-font-size;
    border-radius: 10px;
  }

  .semi-transparent {
    opacity: $form-font-opacity;
  }

  #submit-button {
    width: 210px;
    height: 64px;
    @extend .font-bold;
    font-size: 20px;
    margin-top: 40px;
    margin-bottom: 138px;
  }
}
```

以上是关于markdown 使用Bootstrap样式验证角度反应形式的主要内容,如果未能解决你的问题,请参考以下文章

协调 Angular.js 和 Bootstrap 表单验证样式

Bootstrap Blazor 实战 Markdown 编辑器使用

Angular 表单验证和引导样式

markdown [1000hz-bootstrap-validator]用于抛光验证的有用代码:NIP,PESEL,REGON,IDENTITY CARD NUMBER

FormValidation / formvalidation.io Bootstrap 插件在样式更改时放错了验证图标

关于bootstrap--表单控件(disabled表单禁用显示表单验证的样式)