कोणीय: एक कस्टम फार्म तत्व बनाने और इसे करने के लिए फार्म राज्य गुजर रहा है

हमारे वेब अनुप्रयोगों में विभिन्न रूप अक्सर एक ही ईंट तत्वों से निर्मित होते हैं। घटक रूपरेखा हमें दोहराने योग्य कोड से छुटकारा पाने में मदद करती है, और अब मैं इनमें से एक दृष्टिकोण पर विचार करना चाहता हूं। तो, जैसा कि कोणीय में प्रथागत है।

तकनीकी कार्य:


  • आपको "एसएनआईएलएस में प्रवेश करने के लिए घटक तत्व" बनाने की आवश्यकता है;
  • घटक को मास्क द्वारा दर्ज किए गए मूल्यों को प्रारूपित करना चाहिए;
  • घटक को इनपुट को मान्य करना चाहिए।
  • घटक को प्रतिक्रियाशील रूप के भाग के रूप में काम करना चाहिए;
  • एक अपूर्ण रूप रिबूट के बीच अपनी स्थिति बनाए रखना चाहिए;
  • पृष्ठ लोड करते समय, एक बार संपादित किए गए फॉर्म में तुरंत त्रुटियां दिखनी चाहिए;
  • कोणीय सामग्री द्वारा उपयोग किया जाता है।

एक परियोजना बनाना और निर्भरता स्थापित करना


एक परीक्षण परियोजना बनाएँ


ng new input-snils

तृतीय-पक्ष लाइब्रेरी स्थापित करें


सबसे पहले, स्वयं कोणीय सामग्री

ng add @angular/material

फिर हमें चेकसम की गणना के लिए नियमों के अनुसार स्वयं एसएनआईएलएस को मास्क करना और जांचना होगा

लाइब्रेरी स्थापित करें:

npm install ngx-mask ru-validation-codes

प्रपत्र डेटा सहेजना


लोकलस्टोरेज के साथ काम करने के लिए एक सेवा तैयार करना


प्रपत्र डेटा स्थानीयस्टोरेज में संग्रहीत किया जाएगा।

आप तुरंत एलएस के साथ काम करने के लिए ब्राउज़र विधियों को ले सकते हैं, लेकिन कोणीय में सार्वभौमिक कोड लिखने की कोशिश करने और सभी बाहरी निर्भरता को नियंत्रण में रखने के लिए प्रथागत है। यह परीक्षण को सरल भी करता है।

इसलिए, यह सही होगा जब वर्ग डीआई कंटेनर से अपनी सभी निर्भरता प्राप्त करता है। बिल्ली Matroskin को याद रखना - इंजेक्टर से कुछ अनावश्यक खरीदने के लिए, आपको पहले इंजेक्टर को कुछ अनावश्यक बेचना होगा।

एक window.provider.ts प्रदाता बनाएँ



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

export function getWindow() {
  return window;
}

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

यहाँ क्या चल रहा है? कोणीय के DI इंजेक्टर टोकन को याद करते हैं और उन संस्थाओं को दूर करते हैं जो उनके साथ जुड़े हुए हैं। टोकन - यह एक इंजेक्शनटोकन ऑब्जेक्ट, एक स्ट्रिंग या एक वर्ग हो सकता है। यह एक नया रूट-लेवल इंजेक्शनटॉकेन बनाता है और फ़ैक्टरी के साथ संचार करता है, जो ब्राउज़र को वापस करता है window

अब जब हमारे पास एक विंडो है, तो स्थानीय स्तर पर भंडारण के साथ काम करने के लिए एक सरल सेवा बनाएं


@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


हम प्रपत्र डेटा सहेजने के लिए एक सरल सेवा बनाते हैं।

फार्म persist.service.ts

@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 उत्तीर्ण स्ट्रिंग कुंजी के साथ रूपों को पंजीकृत करने में सक्षम है। पंजीकरण का मतलब है कि हर परिवर्तन के साथ एलएस में प्रपत्र डेटा सहेजा जाएगा।
पंजीकरण करते समय, एलएस से निकाले गए मूल्य को भी लौटा दिया जाता है ताकि यह समझना संभव हो कि फॉर्म पहले ही सहेजा जा चुका है।

Unregister ( unregisterForm) सहेजें प्रक्रिया को समाप्त करता है और LS में प्रविष्टि को हटाता है।

मैं भंडारण की कार्यक्षमता का विवरणात्मक रूप से वर्णन करना चाहूंगा, और हर बार घटक कोड में नहीं करूंगा। कोणीय आपको निर्देशों की मदद से चमत्कार करने देता है, और अभी यही स्थिति है। प्रपत्र-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 { }

एसएनआईएलएस फॉर्म का तत्व


इसे कौन से कार्य सौंपे गए हैं?

सबसे पहले, इसे डेटा को मान्य करना होगा।

snilsValidator


कोणीय आपको नियंत्रण बनाने के लिए अपने सत्यापनकर्ताओं को संलग्न करने की अनुमति देता है, और यह समय आपका खुद का बनाने का है। एसएनआईएलएस की जांच करने के लिए, मैं बाहरी आरयू-सत्यापन-कोड लाइब्रेरी का उपयोग करता हूं और सत्यापनकर्ता काफी सरल होगा।

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


घटक टेम्पलेट में लिपटे इनपुट फ़ील्ड, कोणीय सामग्री पुस्तकालय से एक क्लासिक संस्करण शामिल है।

एक छोटे से जोड़ के साथ, एक इनपुट मास्क बाहरी लाइब्रेरी एनएक्स-मास्क का उपयोग करके इनपुट पर लगाया जाएगा, यहां से इनपुट पैरामीटर मास्क - मास्क सेट करता है और ड्रॉपस्पेशलच्रेक्टर्स - मूल्य से विशेष मुखौटा वर्णों को हटाने से बंद हो जाता है।

अधिक विवरण के लिए एनएक्सएक्स-मास्क प्रलेखन देखें।

यहां इनपुट-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>

सवाल यह है कि यह फॉर्मकंट्रोल क्या है? snilsErrors? त्रुटियों को प्रदर्शित करने के लिए यह एक कस्टम पाइप है, अब हम इसे बनाएंगे।

snils-errors.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;
    }
  }
}

पाइप साफ नहीं है, जिसका अर्थ है कि हर बार परिवर्तन का पता चलने पर इसे निष्पादित किया जाएगा।

पाइप एक त्रुटि प्रकार के पैरामीटर को स्वीकार करता है और तीन प्रकार की त्रुटियों का पता लगाता है:

  • "आवश्यक" - यह त्रुटि कोणीय निर्देश आवश्यक RequiredValidator अंतर्निहित से है
  • "Snils" - यह त्रुटि हमारे सत्यापनकर्ता snilsValidator की है
  • "मास्क त्रुटि" - एनएक्सएक्स-मास्क लाइब्रेरी से मास्कडायरेक्टिव निर्देश से यह त्रुटि

और यह एक बूलियन मान लौटाता है - क्या ऐसी कोई त्रुटि है या नहीं।

अब, इनपुट-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 });
  }
}

बहुत सारी चीजें हैं और मैं यह नहीं बताऊंगा कि कंट्रोलवैल एसेटर के साथ कैसे काम करना है, आप इस बारे में कोणीय दस्तावेज में पढ़ सकते हैं, या उदाहरण के लिए यहाँ tyapk.ru/blog/post/angular-custom-form-field-control यहाँ

क्या स्पष्टीकरण चाहिए?

सबसे पहले, हम फॉर्म फॉर्मकंट्रोल के आंतरिक नियंत्रण का उपयोग करते हैं, हम इसके बदलावों को ऑनचेंज और ऑनटच विधियों के माध्यम से शीर्ष पर मूल्य परिवर्तन भेजने के लिए देते हैं।
और इसके विपरीत, बाहरी रूप में परिवर्तन लिखने के माध्यम से हमारे पास आते हैंवैल्यू और सेटडिसिस्टेबल तरीके और फॉर्मकोर्ट्रोल में परिलक्षित होते हैं।

दूसरे, एक अज्ञात 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 है।

और मैट-एरर त्रुटियों का प्रतिपादन केवल तब किया जाता है जब वर्तमान नियंत्रण को छुआ जाता है। हमारी एक आवश्यकता है कि पुनर्निर्मित फॉर्म तुरंत सत्यापन त्रुटियों को प्रदर्शित करता है, इसलिए फॉर्मपर्सिस्टडायरेक्टिव मार्क मार्क को निष्पादित करता है। यदि स्थानीय मान से पढ़ा गया है तो फॉर्म वैल्यू पढ़ा गया है। लेकिन मैट-त्रुटियों को प्रदर्शित नहीं किया जाएगा, क्योंकि वे हमारे कंट्रोलवैल्यूएक्टर घटक के अंदर हैं, वे फॉर्म कंट्रोल कंट्रोल पर निर्भर करते हैं, और इस स्वतंत्र नियंत्रण पर छुआ हुआ राज्य अभी भी झूठा है।

हमें इन राज्यों को फेंकने के लिए एक तंत्र की आवश्यकता है। ऐसा करने के लिए, आप 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 को लागू करता है। जब किसी रूप की स्थिति बदलती है, तो उस क्षण को इंगित करना असंभव होता है, लेकिन हम जानते हैं कि जब भी कोई प्रपत्र बदलता है, तो कोणीय एक परिवर्तन का पता लगाने के चक्र की प्रतीक्षा में घटक को चिह्नित करता है। परीक्षण किया गया घटक और उसके वंशज ngDoCheck जीवनचक्र हुक को निष्पादित करते हैं, जिसे हमारा निर्देशन उपयोग करेगा।

FormStatusesDirective


निर्देशन प्रपत्र- statuses.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 को लागू करने वाले घटक की जाँच करनी चाहिए।

यदि टोकन द्वारा कुछ भी नहीं पाया जाता है, तो कुछ भी नहीं होता है, सेटसैटचाउट और सेटस्वाप्रीस्टिन के तरीके खाली कार्य होंगे।

यदि StateValueAccessor पाया जाता है, तो इसके सेटटचस्टस्टेट और सेटप्रिस्टाइनस्टैट तरीकों को प्रत्येक ज्ञात राज्य परिवर्तन के लिए बुलाया जाएगा।

यह फॉर्म-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 निर्देश प्रपत्र पर लटका हुआ है, कोणीय इसके बारे में प्रपत्र [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 पर लिया जा सकता है स्टैकब्लिट्ज़
पर डेमो स्टैकब्लिट्ज़ पर
कोड थोड़ा अलग है, इस तथ्य के कारण कि टाइपस्क्रिप्ट संस्करण अभी तक एल्विस ऑपरेटर का समर्थन नहीं करता है।
इसके अलावा, अभी भी बारीकियां हैं जो लेख में परिलक्षित नहीं होती हैं, अगर किसी को इसकी आवश्यकता है, तो मैं इसे पूरक करूंगा।

All Articles