هل تساءلت يومًا كيف يعمل الجمع بين النماذج الزاوية وعناصر 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