غالبًا ما يتم بناء الأشكال المختلفة في تطبيقات الويب لدينا من نفس عناصر الطوب. تساعدنا أطر المكونات في التخلص من التعليمات البرمجية القابلة للتكرار ، والآن أريد التفكير في أحد هذه الأساليب. لذا ، كما هي العادة في الزاوي.مهمة فنية:
- أنت بحاجة إلى إنشاء مكون "عنصر النموذج لإدخال 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.tsimport { 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
.الآن بعد أن أصبح لدينا نافذة ، دعنا ننشئ خدمة بسيطة للعمل مع LocalStoragestorage.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,
) { }
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]',
})
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.tsimport { 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.tstype 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.tsexport const STATE_VALUE_ACCESSOR = new InjectionToken<StateValueAccessor>('STATE_VALUE_ACCESSOR');
accessor.interface.tsexport interface StateValueAccessor {
setTouchedState?(touched: boolean): void;
setPristineState?(pristine: boolean): void;
}
تصف الواجهة متطلبات الفئة التي تنفذها للحصول على طريقتين محددتين (اختياريًا). يتم تنفيذ هذه الطرق في InputSnilsComponent وتفرض هذه الحالات على نموذج الرقابة الداخلية.ثم تحتاج إلى توجيه لربط NgControl ومكوننا الذي يقوم بتنفيذ StateValueAccessor. لا يمكن تحديد اللحظة التي تتغير فيها حالة النموذج ، ولكننا نعلم أنه كلما تغير النموذج ، تحدد Angular المكون على أنه ينتظر دورة الكشف عن التغيير. يقوم المكون المختبر ونسله بتنفيذ خطاف دورة حياة ngDoCheck ، والذي سيستخدمه توجيهنا.FormStatusesDirective
قم بإنشاء نموذج حالة التوجيه. directive.tsconst noop: (v?: boolean) => void = () => { };
@Directive({
selector: '[formControlName],[ngModel],[formControl]'
})
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 قليلاً ، نظرًا لأن إصدار النسخة المطبوعة هناك لا يدعم عامل الفيس بعد.بالإضافة إلى ذلك ، لا تزال هناك فروق دقيقة لا تنعكس في المقالة ، إذا احتاجها شخص ما ، فسأكملها.