ControlValueAccessor وقناعة في الزاوي

هل تساءلت يومًا كيف يعمل الجمع بين النماذج الزاوية وعناصر HTML التي يدخل المستخدم من خلالها البيانات؟


منذ البداية استخدمنا ControlValueAccessor ، وهي واجهة خاصة تتكون من 4 طرق فقط:


 interface ControlValueAccessor { writeValue(value: any): void registerOnChange(fn: (value: any) => void): void registerOnTouched(fn: () => void): void setDisabledState(isDisabled: boolean)?: void } 

من خارج الصندوق ، يوجد لدى Angular العديد من هذه الملحقات: لمربعات الاختيار وأزرار الاختيار ، للإدخال والاختيار. ومع ذلك ، إذا كنت تقوم بتطوير محادثة تحتاج فيها إلى منح الفرصة للكتابة بالخط المائل ، أو جعل النص غامقًا أو ، على سبيل المثال ، إدراج الرموز ، فستستخدم على الأرجح السمة contenteditable لإنشاء محتوى منسق.


الزاوي لا يدعم استخدام النماذج مع contenteditable ، لذلك عليك أن تكتبها بنفسك.



ControlValueAccessor


التوجيه الذي نكتبه سيعمل بشكل مشابه للوصلات المدمجة - سوف يستجيب للسمة القابلة contenteditable . من أجل الحصول على القوالب والنماذج التفاعلية من خلال الحقن التبعي ، يكفي توفير InjectionToken المدمج:


 @Directive({ selector: '[contenteditable][formControlName],' + '[contenteditable][formControl],' + '[contenteditable][ngModel]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ContenteditableValueAccessor), multi: true, }, ], }) export class ContenteditableValueAccessor implements ControlValueAccessor { // ... } 

تتطلب واجهة ControlValueAccessor 3 طرق وتحتوي على طريقة واحدة اختيارية:


  • registerOnChange - وظيفة سوف تأتي في هذه الطريقة أثناء التهيئة. يتم استدعاؤه بقيمة جديدة عندما يقوم المستخدم بإدخال شيء ما في مكوننا بحيث يتم إدخال بيانات جديدة في عنصر التحكم.
  • registerOnTouched - وظيفة سوف تأتي في هذه الطريقة أثناء التهيئة. يجب أن يتم استدعاؤه عندما غادر المستخدم المكون الخاص بنا حتى يتم touched عنصر التحكم. يستخدم هذا للتحقق من الصحة.
  • writeValue - سيتم استدعاء هذه الطريقة بواسطة عنصر التحكم لتمرير القيمة إلى المكون الخاص بنا. يتم استخدامه إذا تغيرت القيمة من خلال الكود الخارجي ( setValue أو تغيير المتغير الذي يرتبط به ngModel ) ، وكذلك لإعداد القيمة الأولية.
    تجدر الإشارة إلى أنه ، على عكس الأشكال التفاعلية ، يتصرف ngModel بطريقة ngModel - بشكل خاص ، تتم تهيئة القيمة الأولية فيه بتأخير ، وطريقة "تشنجات" writeValue مرتين ، أول مرة يتم فيها استخدام null :
    https://github.com/angular/angular/issues/14988
  • setDisabledState (اختياري) - سيتم استدعاء هذه الطريقة بواسطة عنصر التحكم عندما تتغير حالة disabled . على الرغم من أن الطريقة اختيارية ، فمن الأفضل الاستجابة لهذا في المكون الخاص بك.

تنفيذ واجهة


للعمل مع عنصر DOM ، نحتاج إلى Renderer2 ، وفي الواقع ، العنصر نفسه ، لذلك أضفهم إلى المُنشئ:


 constructor( @Inject(ElementRef) private readonly elementRef: ElementRef, @Inject(Renderer2) private readonly renderer: Renderer2, ) {} 

نحن نحفظ الطرق التي يتحكم بها التحكم في الحقول الخاصة بالفصل:


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

الحالة disabled للمكون contenteditable تعادل تعطيل وضع التحرير - contenteditable="false" . تعيين القيمة من الخارج يكافئ استبدال DOM innerHTML للعنصر ، ويمكن مراقبة تحديث القيمة من قبل المستخدم وترك المكون من خلال الاشتراك في الأحداث المقابلة:


 @HostListener('input') onInput() { this.onChange(this.elementRef.nativeElement.innerHTML); } @HostListener('blur') onBlur() { this.onTouched(); } setDisabledState(disabled: boolean) { this.renderer.setAttribute( this.elementRef.nativeElement, 'contenteditable', String(!disabled), ); } writeValue(value: string) { this.renderer.setProperty( this.elementRef.nativeElement, 'innerHTML', value, ); } 

هذا ، في الواقع ، هو كل شيء. هذا يكفي للتنفيذ الأساسي لعمل الأشكال الزاويّة والعناصر contenteditable .


مستكشف الإنترنت


ومع ذلك ، هناك زوجين ولكن.


أولاً ، القيمة الأولية الفارغة للنموذج null ، وبعد كتابة writeValue في IE11 ، سنرى null في القالب. لتنفيذ العمل بشكل صحيح ، نحتاج إلى تطبيع القيمة:


 writeValue(value: string | null) { this.renderer.setProperty( this.elementRef.nativeElement, 'innerHTML', ContenteditableValueAccessor.processValue(value), ); } private static processValue(value: string | null): string { const processed = value || ''; return processed.trim() === '<br>' ? '' : processed; } 

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


ثانياً ، لا يدعم Internet Explorer أحداث input للعناصر contenteditable . سيتعين علينا تطبيق الاستعاضة باستخدام MutationObserver :


 private readonly observer = new MutationObserver(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); ngAfterViewInit() { this.observer.observe(this.elementRef.nativeElement, { characterData: true, childList: true, subtree: true, }); } ngOnDestroy() { this.observer.disconnect(); } 

لن نقوم بتنفيذ التحقق على متصفح معين. بدلاً من ذلك ، نقوم على الفور بتعطيل MutationObserver في حدث input الأول:


 @HostListener('input') onInput() { this.observer.disconnect(); // ... } 

يعمل مكوننا الآن في IE11 ونحن راضون عن أنفسنا!


انترنت اكسبلورر ლ (ಠ 益 ಠ ლ)


لسوء الحظ ، لن يتم ترك IE11. على ما يبدو ، هناك خلل في عمل MutationObserver . إذا كانت هناك علامات داخل العنصر contenteditable ، على سبيل المثال ، some <b>text</b> ، فعند حذف نص ، مما يستلزم حذف العلامة بأكملها ( text الكلمة في هذا المثال) ، سيتم استدعاء رد الاتصال من المراقب قبل الحقيقي التغييرات في DOM!



لسوء الحظ ، ليس لدينا خيار سوى الاعتراف بالهزيمة واستخدام setTimeout :


 private readonly observer = new MutationObserver(() => { setTimeout(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); }); 

استنتاج


بشرط أن يدعم Angular الإصدارات 9 و 10 و 11 من Internet Explorer ، يصبح من الواضح سبب عدم تنفيذها للعمل ذي contenteditable في المنزل.


بالإضافة إلى ذلك ، يجب أن تتذكر أن HTML يمكن أن يحتوي على تعليمات برمجية ضارة - لذلك ، لا تأخذ بجرأة محتوى غير معروف والصقه في عنصر التحكم ، ويجب أن يتم إدخال إدخال المستخدم في أحداث paste drop . تعمل التعليمات البرمجية الموضحة في هذه المقالة في Angular 4 والإصدارات الأحدث ، بما في ذلك FormHooks . إذا كنت ترغب في ذلك ، يمكنك إضافة دعم لـ Angular 2 ، إذا كنت تستخدم Renderer ، وليس Renderer2 . تتوفر شفرة المصدر وحزمة npm على الروابط:


https://github.com/TinkoffCreditSystems/angular-contenteditable-Accessor
https://www.npmjs.com/package/@tinkoff/angular-contenteditable-accessor


ولعب مع مثال هنا:
https://stackblitz.com/edit/angular2-contenteditable-value-Accessor

Source: https://habr.com/ru/post/ar443714/


All Articles