الزاوي: إنشاء عنصر نموذج مخصص وتمرير حالة النموذج إليه

غالبًا ما يتم بناء الأشكال المختلفة في تطبيقات الويب لدينا من نفس عناصر الطوب. تساعدنا أطر المكونات في التخلص من التعليمات البرمجية القابلة للتكرار ، والآن أريد التفكير في أحد هذه الأساليب. لذا ، كما هي العادة في الزاوي.

مهمة فنية:


  • أنت بحاجة إلى إنشاء مكون "عنصر النموذج لإدخال SNILS" ؛
  • يجب على المكون تنسيق القيم المدخلة حسب القناع ؛
  • يجب أن يتحقق المكون من صحة الإدخال.
  • يجب أن يعمل المكون كجزء من الشكل التفاعلي ؛
  • يجب أن يحافظ النموذج غير المكتمل على حالته بين عمليات إعادة التشغيل ؛
  • عند تحميل الصفحة ، بمجرد أن يظهر النموذج المحرر أخطاء على الفور ؛
  • تستخدم من قبل Angular Material.

إنشاء مشروع وتثبيت تبعيات


إنشاء مشروع تجريبي


ng new input-snils

قم بتثبيت مكتبات الطرف الثالث


بادئ ذي بدء ، مادة الزاوي نفسها

ng add @angular/material

ثم نحتاج إلى إخفاء وفحص SNILS بأنفسهم وفقًا لقواعد حساب المجموع الاختباري .

تثبيت المكتبات:

npm install ngx-mask ru-validation-codes

حفظ بيانات النموذج


تحضير خدمة للعمل مع التخزين المحلي


سيتم تخزين بيانات النموذج في localStorage.

يمكنك على الفور اتباع طرق المتصفح للعمل مع LS ، ولكن من المعتاد في Angular محاولة كتابة رمز عالمي ، والحفاظ على جميع التبعيات الخارجية تحت السيطرة. كما يبسط الاختبار.

لذلك ، سيكون صحيحًا عندما يحصل الفصل على جميع تبعياته من حاوية DI. تذكر القطة Matroskin - من أجل شراء شيء غير ضروري من الحاقن ، يجب عليك أولاً بيع شيء غير ضروري للحاقن.

قم بإنشاء موفر

window.provider.ts

import { InjectionToken } from '@angular/core';

export function getWindow() {
  return window;
}

export const WINDOW = new InjectionToken('Window', {
  providedIn: 'root',
  factory: getWindow,
});

ماذا يجري هنا؟ يتذكر Angular's DI Injector الرموز المميزة ويمنح الكيانات المرتبطة بها. الرمز المميز - يمكن أن يكون كائن InjectionToken أو سلسلة أو فئة. يؤدي هذا إلى إنشاء ميزة InjectionToken جديدة على مستوى الجذر ويتواصل مع المصنع الذي يعيد المتصفح window.

الآن بعد أن أصبح لدينا نافذة ، دعنا ننشئ خدمة بسيطة للعمل مع LocalStorage
storage.service.ts

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  readonly prefix = 'snils-input__';

  constructor(
    @Inject(WINDOW) private window: Window,
  ) {}

  public set<T>(key: string, data: T): void {
    this.window.localStorage.setItem(this.prefix + key, JSON.stringify(data));
  }

  public get<T>(key: string): T {
    try {
      return JSON.parse(this.window.localStorage.getItem(this.prefix + key));
    } catch (e) { }
  }

  public remove(key: string): void {
    this.window.localStorage.removeItem(this.prefix + key);
  }
}

تأخذ StorageService نافذة من حاقن وتوفر أغلفة خاصة بها لحفظ البيانات وقراءتها. لم أجعل البادئة قابلة للتهيئة حتى لا تفرط في المقالة مع وصف كيفية إنشاء وحدات مع التكوين.

FormPersistModule


نقوم بإنشاء خدمة بسيطة لحفظ بيانات النموذج.

شكل-استمرار. خدمة. tts

@Injectable({
  providedIn: 'root'
})
export class FormPersistService {
  private subscriptions: Record<string, Subscription> = {};

  constructor(
    private storageService: StorageService,
  ) { }

  /**
   * @returns restored data if exists
   */
  public registerForm<T>(formName: string, form: AbstractControl): T {
    this.subscriptions[formName]?.unsubscribe();
    this.subscriptions[formName] = this.createFormSubscription(formName, form);

    return this.restoreData(formName, form);
  }

  public unregisterForm(formName: string): void {
    this.storageService.remove(formName);

    this.subscriptions[formName]?.unsubscribe();
    delete this.subscriptions[formName];
  }

  public restoreData<T>(formName: string, form: AbstractControl): T {
    const data = this.storageService.get(formName) as T;
    if (data) {
      form.patchValue(data, { emitEvent: false });
    }

    return data;
  }

  private createFormSubscription(formName: string, form: AbstractControl): Subscription {
    return form.valueChanges.pipe(
      debounceTime(500),
    )
      .subscribe(value => {
        this.storageService.set(formName, value);
      });
  }
}

FormPersistService قادر على تسجيل النماذج باستخدام مفتاح السلسلة التي تم تمريرها. التسجيل يعني أنه سيتم حفظ بيانات النموذج في LS مع كل تغيير.
عند التسجيل ، يتم أيضًا إرجاع القيمة المستخرجة من LS بحيث يمكن فهم أن النموذج قد تم حفظه مسبقًا.

يقوم Unregister ( unregisterForm) بإنهاء عملية الحذف وحذف الإدخال في LS.

أود أن أصف وظيفة التخزين بشكل معلن ، ولا أفعل ذلك في كل مرة في كود المكون. يتيح لك Angular القيام بمعجزات بمساعدة التوجيهات ، وهذه هي الحال الآن.

قم بإنشاء التوجيه
form-persist.directive.ts

@Directive({
  selector: 'form[formPersist]', // tslint:disable-line: directive-selector
})
export class FormPersistDirective implements OnInit {
  @Input() formPersist: string;

  constructor(
    private formPersistService: FormPersistService,
    @Self() private formGroup: FormGroupDirective,
  ) { }

  @HostListener('submit')
  onSubmit() {
    this.formPersistService.unregisterForm(this.formPersist);
  }

  ngOnInit() {
    const savedValue = this.formPersistService.registerForm(this.formPersist, this.formGroup.control);
    if (savedValue) {
      this.formGroup.control.markAllAsTouched();
    }
  }
}

عند فرضه على النموذج ، يسحب FormPersistDirective توجيهًا آخر من الحاقن المحلي - FormGroupDirective ويأخذ كائن النموذج التفاعلي من هناك للتسجيل مع FormPersistService.

يجب أخذ مفتاح التسجيل من النموذج ، ولا يحتوي النموذج نفسه على أي معرف فريد متأصل فيه.

عند تقديم نموذج ، يجب إلغاء التسجيل. للقيام بذلك ، استمع للحدث المرسل باستخدام HostListener.

يجب أيضًا تسليم التوجيه إلى المكونات حيث يمكن استخدامه. من الممارسات الجيدة إنشاء وحدات صغيرة منفصلة لكل كيان معاد استخدامه.

نموذج- persist.module.ts

@NgModule({
  declarations: [FormPersistDirective],
  exports: [FormPersistDirective]
})
export class FormPersistModule { }

عنصر نموذج SNILS


ما المهام الموكلة إليها؟

بادئ ذي بدء ، يجب التحقق من صحة البيانات.

snilsValidator


يتيح لك Angular إرفاق أدوات التحقق الخاصة بك لتشكيل عناصر التحكم ، وحان الوقت لإنشاء أدوات التحكم الخاصة بك. للتحقق من SNILS ، أستخدم مكتبة رموز التحقق من صحة ru الخارجية وسيكون المدقق بسيطًا جدًا.

snils.validator.ts

import { checkSnils } from 'ru-validation-codes';

export function snilsValidator(control: AbstractControl): ValidationErrors | null {
  if (control.value === ''  || control.value === null) {
    return null;
  }

  return checkSnils(control.value)
    ? null
    : { snils: 'error' };
}

مكون InputSnilsComponent


يتكون قالب المكون من حقل إدخال ملفوف ، إصدار كلاسيكي من مكتبة المواد الزاويّة.

مع إضافة صغيرة واحدة ، سيتم فرض قناع إدخال على الإدخال باستخدام قناع ngx الخاص بالمكتبة الخارجية ، ومن هنا يقوم قناع معلمات الإدخال - تعيين القناع وإسقاط خاص - بإيقاف إزالة أحرف القناع الخاصة من القيمة.

راجع وثائق ngx-mask للحصول على مزيد من التفاصيل.

إليك قالب مكون
input-snils.component.html

<mat-form-field appearance="outline">
  <input
    matInput
    autocomplete="snils"
    [formControl]="formControl"
    [mask]="mask"
    [dropSpecialCharacters]="false"
    [placeholder]="placeholder"
    [readonly]="readonly"
    [required]="required"
    [tabIndex]="tabIndex"
  >
  <mat-error [hidden]="formControl | snilsErrors: 'required'">  </mat-error>
  <mat-error [hidden]="formControl | snilsErrors: 'format'">   </mat-error>
  <mat-error [hidden]="formControl | snilsErrors: 'snils'"> </mat-error>
</mat-form-field>

السؤال هو ، ما هذا النموذج؟ أخطاء؟ هذا أنبوب مخصص لعرض الأخطاء ، الآن سنقوم بإنشائه.

snils-trouble.pipe.ts

type ErrorType = 'required' | 'format' | 'snils';

@Pipe({
  name: 'snilsErrors',
  pure: false,
})
export class SnilsErrorsPipe implements PipeTransform {

  transform(control: AbstractControl, errorrType: ErrorType): boolean {
    switch (errorrType) {
      case 'required': return !control.hasError('required');
      case 'format': return !control.hasError('Mask error');
      case 'snils': return control.hasError('Mask error') || !control.hasError('snils');
      default: return false;
    }
  }
}

الأنبوب غير نظيف ، مما يعني أنه سيتم تنفيذه في كل مرة يتم فيها اكتشاف التغييرات.

يقبل Pipe معلمة نوع الخطأ ويكشف الأخطاء من ثلاثة أنواع:

  • "مطلوب" - هذا الخطأ مأخوذ من التوجيه الزاوي RequiredValidator المدمج
  • "Snils" - هذا الخطأ من snilsValidator
  • "خطأ قناع" - هذا الخطأ من توجيه MaskDirective من مكتبة ngx-mask

وترجع قيمة منطقية - هل هناك مثل هذا الخطأ أم لا.

الآن ، دعونا نلقي نظرة على رمز مكون
input-snils.component.ts نفسه

@Component({
  selector: 'app-input-snils',
  templateUrl: './input-snils.component.html',
  styleUrls: ['./input-snils.component.css'],
  encapsulation: ViewEncapsulation.None,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputSnilsComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => InputSnilsComponent),
      multi: true,
    },
    {
      provide: STATE_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputSnilsComponent),
    },
  ]
})
export class InputSnilsComponent implements OnInit, ControlValueAccessor, StateValueAccessor, OnDestroy {
  public mask = '000-000-000 00';
  public formControl = new FormControl('', [snilsValidator]);
  private sub = new Subscription();

  @Input() readonly: boolean;
  @Input() placeholder = '';
  @Input() tabIndex = 0;
  @Input() required: boolean;

  private onChange = (value: any) => { };
  private onTouched = () => { };
  registerOnChange = (fn: (value: any) => {}) => this.onChange = fn;
  registerOnTouched = (fn: () => {}) => this.onTouched = fn;


  ngOnInit() {
    this.sub = this.linkForm();
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

  private linkForm(): Subscription {
    return this.formControl.valueChanges.subscribe(value => {
      this.onTouched();
      this.onChange(value);
    });
  }

  writeValue(outsideValue: string): void {
    if (outsideValue) {
      this.onTouched();
    }
    this.formControl.setValue(outsideValue, { emitEvent: false });
  }

  setDisabledState(disabled: boolean) {
    disabled
      ? this.formControl.disable()
      : this.formControl.enable();
  }

  validate(): ValidationErrors | null {
    return this.formControl.errors;
  }

  setPristineState(pristine: boolean) {
    pristine
      ? this.formControl.markAsPristine()
      : this.formControl.markAsDirty();

    this.formControl.updateValueAndValidity({ emitEvent: false });
  }

  setTouchedState(touched: boolean) {
    touched
      ? this.formControl.markAsTouched()
      : this.formControl.markAsUntouched();

    this.formControl.updateValueAndValidity({ emitEvent: false });
  }
}

هناك الكثير من الأشياء ولن أصف كيفية العمل مع ControlValueAccessor ، يمكنك القراءة عن ذلك في وثائق Angular ، أو على سبيل المثال هنا tyapk.ru/blog/post/angular-custom-form-field-control

ما الذي يحتاج إلى شرح هنا؟

أولاً ، نستخدم عنصر التحكم الداخلي في النموذج formControl ، ونعلق تغييراته لإرسال تغيير القيمة إلى الأعلى ، عبر onChange وطرق onTouched.
والعكس بالعكس ، تأتي التغييرات في النموذج الخارجي إلينا من خلال طرق writeValue و setDisabledState وتنعكس في formControl.

ثانيًا ، هناك رمز STATE_VALUE_ACCESSOR غير معروف ، وواجهة StateValueAccessor غير معروفة وطريقتين إضافيتين setPristineState و setTouchedState. سيتم شرحها لاحقًا.

في هذه الأثناء ، قم بإنشاء وحدة شخصية للمكون
الإدخال snils.module.ts

@NgModule({
  declarations: [InputSnilsComponent, SnilsErrorsPipe],
  imports: [
    CommonModule,
    MatFormFieldModule,
    MatInputModule,
    NgxMaskModule.forChild(),
    ReactiveFormsModule,
  ],
  exports: [InputSnilsComponent],
})
export class InputSnilsModule { }

تمرير الحالات إلى عنصر


عند استخدام ControlValueAccessor ، هناك الفروق الدقيقة التالية:

لقد لمست الشكل التفاعلي الحالات البكر (يشار إليها فيما يلي ببساطة بـ "الحالات").

  • البكر صحيح في البداية ويتغير إلى خطأ عند تغيير قيمة التحكم من القالب
  • لمست في البداية كاذبة ويتغير إلى صحيح عندما فقدت السيطرة التركيز

يمكن أيضًا تعيينها بالقوة ، ولكن هذا لن يؤثر على عنصر التحكم داخل ControlValueAccessor ، بالنسبة للمكون الخاص بنا فهو formControl.

ولا تظهر أخطاء أخطاء mat إلا عند لمس عنصر التحكم الحالي. لدينا مطلب بأن يعرض النموذج الذي تمت استعادته على الفور أخطاء التحقق من الصحة ، لذلك ينفذ FormPersistDirective العلامة "كل" المس إذا تم قراءة قيمة النموذج من localStorage. ولكن لن يتم عرض أخطاء mat ، نظرًا لأنها موجودة داخل مكون ControlValueAccessor الخاص بنا ، فهي تعتمد على عنصر التحكم formControl ، وعلى هذا التحكم المستقل ، لا تزال الحالة التي تم لمسها خاطئة.

نحن بحاجة إلى آلية لرمي هذه الدول. للقيام بذلك ، يمكنك إنشاء التناظرية الخاصة بك من ControlValueAccessor ، دعنا نسميها StateValueAccessor.

تحتاج أولاً إلى إنشاء رمز مميز وواجهة.

الدولة-القيمة-accessor.token.ts

export const STATE_VALUE_ACCESSOR = new InjectionToken<StateValueAccessor>('STATE_VALUE_ACCESSOR');


accessor.interface.ts
export interface StateValueAccessor {
  setTouchedState?(touched: boolean): void;
  setPristineState?(pristine: boolean): void;
}

تصف الواجهة متطلبات الفئة التي تنفذها للحصول على طريقتين محددتين (اختياريًا). يتم تنفيذ هذه الطرق في InputSnilsComponent وتفرض هذه الحالات على نموذج الرقابة الداخلية.

ثم تحتاج إلى توجيه لربط NgControl ومكوننا الذي يقوم بتنفيذ StateValueAccessor. لا يمكن تحديد اللحظة التي تتغير فيها حالة النموذج ، ولكننا نعلم أنه كلما تغير النموذج ، تحدد Angular المكون على أنه ينتظر دورة الكشف عن التغيير. يقوم المكون المختبر ونسله بتنفيذ خطاف دورة حياة ngDoCheck ، والذي سيستخدمه توجيهنا.

FormStatusesDirective


قم بإنشاء نموذج حالة التوجيه. directive.ts


const noop: (v?: boolean) => void = () => { };

@Directive({
  selector: '[formControlName],[ngModel],[formControl]' // tslint:disable-line: directive-selector
})
export class FormStatusesDirective implements DoCheck, OnInit {
  private setSVATouched = noop;
  private setSVAPristine = noop;

  constructor(
    @Self() private control: NgControl,
    @Self() @Optional()  @Inject(STATE_VALUE_ACCESSOR) private stateValueAccessor: StateValueAccessor,
  ) { }

  ngOnInit() {
    if (this.stateValueAccessor?.setTouchedState) {
      this.setSVATouched = wrapIfChanges(touched => this.stateValueAccessor.setTouchedState(touched));
    }

    if (this.stateValueAccessor?.setPristineState) {
      this.setSVAPristine = wrapIfChanges(pristine => this.stateValueAccessor.setPristineState(pristine));
    }
  }

  ngDoCheck() {
    this.setSVAPristine(this.control.pristine);
    this.setSVATouched(this.control.touched);
  }
}

يتم فرض FormStatusesDirective على جميع عناصر التحكم والفحوصات الممكنة لوجود StateValueAccessor. للقيام بذلك ، مطلوب تبعية اختيارية على الرمز المميز STATE_VALUE_ACCESSOR من الحاقن ، والذي كان يجب على المكون الذي يقوم بتطبيق StateValueAccessor التحقق منه.

إذا لم يتم العثور على أي شيء من خلال الرمز المميز ، فلن يحدث شيء ، وستكون الأساليب setSVATouched و setSVAPristine مجرد وظائف فارغة.

إذا تم العثور على StateValueAccessor ، فسيتم استدعاء طريقتين setTouchedState و setPristineState لكل تغيير في الحالة المكتشفة.

يبقى تزويد التوجيه بوحدة نمطية لتصدير
form-statuses.module.ts

@NgModule({
  declarations: [FormStatusesDirective],
  exports: [FormStatusesDirective]
})
export class FormStatusesModule { }

الصفحة الرئيسية


الآن تحتاج إلى إنشاء النموذج نفسه. لكي لا تثير ضجة ، ضعها على الصفحة الرئيسية لـ AppComponent. بالطبع ، في التطبيق الحقيقي ، من الأفضل عمل مكون منفصل للنموذج.
قالب
App.component.html

<section class="form-wrapper">
  <form
    class="form"
    [formGroup]="form"
    formPersist="inputSnils"
  >
    <app-input-snils
      class="input-snils"
      formControlName="snils"
      [required]="true"
    ></app-input-snils>

    <button
      class="ready-button"
      mat-raised-button
      [disabled]="form.invalid"
      type="submit"
    >
      Submit
    </button>
  </form>
</section>

يتم تعليق توجيه FormPersistDirective على النموذج ، وتتعلم Angular عنه من خلال محدد النموذج [formPersist].

يجب تزويد القالب بمتغيرات ، لنقم بذلك
app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public form = new FormGroup({
    snils: new FormControl('', [Validators.required])
  });
}

جاء رمز المكون مع النموذج بسيطًا للغاية ولا يحتوي على أي شيء غير ضروري.

يبدو أن هذا:


يمكن أخذ شفرة المصدر على GitHub
العرض التوضيحي على stackblitz
يختلف الرمز على stackblitz قليلاً ، نظرًا لأن إصدار النسخة المطبوعة هناك لا يدعم عامل الفيس بعد.
بالإضافة إلى ذلك ، لا تزال هناك فروق دقيقة لا تنعكس في المقالة ، إذا احتاجها شخص ما ، فسأكملها.

All Articles