عندما تعمل في مكتبة تحتوي على مكونات قابلة لإعادة الاستخدام ، يكون سؤال واجهة برمجة التطبيقات (API) حادًا بشكل خاص. من ناحية ، تحتاج إلى اتخاذ قرار موثوق ودقيق ، من ناحية أخرى ، لإرضاء الكثير من الحالات الخاصة. هذا ينطبق على العمل مع البيانات ، وعلى الميزات الخارجية لحالات الاستخدام المختلفة. بالإضافة إلى ذلك ، ينبغي تحديث كل شيء بسهولة ونشره على المشروعات.
هذه المكونات تحتاج إلى مرونة غير مسبوقة. في الوقت نفسه ، لا يمكن جعل الإعداد معقدًا للغاية ، لأنه سيتم استخدامه بواسطة كل من كبار السن وحزيران / يونيو. يعد تقليل تكرار التعليمات البرمجية أحد مهام مكتبة المكونات. لذلك ، لا يمكن تحويل التكوين إلى نسخ التعليمات البرمجية.

مكونات البيانات الملحد
لنفترض أننا نصنع زرًا بقائمة منسدلة. ماذا سيكون API لها؟ بالطبع ، يحتاج إلى بعض العناصر كمدخل - مجموعة من عناصر القائمة. على الأرجح ، سيكون الإصدار الأول من الواجهة كما يلي:
interface MenuItem { readonly text: string; readonly onClick(): void; }
بسرعة كبيرة ، disabled: boolean
سوف disabled: boolean
إضافة إلى هذا. ثم يأتي المصممون ورسم قائمة مع الرموز. والرجال من المشروع المجاور ، سوف يرسمون الرموز من ناحية أخرى. والآن تنمو الواجهة ، من الضروري تغطية المزيد والمزيد من الحالات الخاصة ، ومن وفرة الأعلام يبدأ المكون في تشبه جمعية الأمم المتحدة.

الأدوية الجنيسة تأتي لإنقاذ. إذا قمت بتنظيم المكون بحيث لا يهتم بنموذج البيانات ، فستختفي كل هذه المشكلات. بدلاً من استدعاء item.onClick
بنقرة واحدة ، ستقوم القائمة ببساطة item.onClick
العنصر الذي تم النقر فوقه. ما يجب القيام به بعد ذلك مهمة لمستخدمي المكتبة. حتى لو كانوا يطلقون على نفس item.onClick
.
في حالة الحالة disabled
، على سبيل المثال ، يتم حل المشكلة باستخدام معالجات خاصة. يتم تمرير الأسلوب disabledItemHandler: (item: T) => boolean
ItemHandler إلى المكون disabledItemHandler: (item: T) => boolean
، يتم من خلاله تشغيل كل عنصر. النتيجة تقول ما إذا كان هذا العنصر مغلقًا أم لا.

إذا كنت تفعل ComboBox ، فقد تتبادر إلى الذهن واجهة يخزن سلسلة للعرض وقيمة تعسفية حقيقية يتم استخدامها في التعليمات البرمجية. هذه الفكرة واضحة. بعد كل شيء ، عندما يقوم المستخدم بكتابة النص ، يجب على مربع التحرير والسرد تصفية الخيارات وفقًا للسطر الذي تم إدخاله.
interface ComboBoxItem { readonly text: string; readonly value: any; }
لكن هنا أيضًا ، ستظهر قيود مثل هذا النهج - بمجرد ظهور التصميم الذي لا يكفي فيه الخط. بالإضافة إلى ذلك ، سيحتوي النموذج على غلاف بدلاً من القيمة الحقيقية ، لا يتم إجراء البحث دائمًا حصريًا من خلال تمثيل السلسلة (على سبيل المثال ، يمكننا القيادة في رقم هاتف ، ولكن يجب عرض اسم الشخص). وسيزداد عدد الواجهات مع ظهور مكونات أخرى ، حتى لو كان نموذج البيانات تحتها هو نفسه.
الأدوية الجنيسة والمعالجات ستساعد هنا أيضًا. دعنا نعطي الدالة (item: T) => string
المكون stringify
. القيمة الافتراضية هي item => String(item)
. وبالتالي ، يمكنك حتى استخدام الفئات كخيارات عن طريق تحديد طريقة toString()
فيها. كما ذكر أعلاه ، من الضروري تصفية الخيارات ليس فقط عن طريق تمثيل السلسلة. هذه أيضًا حالة جيدة لاستخدام معالجات. يمكنك توفير مكون بوظيفة تتلقى سلسلة بحث وعنصر كمدخل. سيعود boolean
- سيوضح هذا ما إذا كان العنصر مناسبًا للطلب.
مثال شائع آخر لاستخدام واجهة هو معرف فريد ، والذي يطابق نسخ من كائنات جافا سكريبت. عندما تلقينا قيمة النموذج فورًا ، وجاءت خيارات التحديد في طلب منفصل من الخادم - سيكون لديهم فقط نسخة من العنصر الحالي. تتم معالجة هذه المهمة بواسطة معالج يتلقى عنصرين كمدخلات ويعيد مساواتهما. المقارنة الافتراضية أمر طبيعي ===
.
في الواقع ، لا يحتاج مكون عرض علامة التبويب إلى معرفة الشكل الذي تم تمرير علامة التبويب إليه: على الأقل مع النص ، على الأقل مع كائن به حقول إضافية ، حتى مع كيفية القيام بذلك. معرفة التنسيق ليست ضرورية للتنفيذ ، لكن الكثير منهم يربطون بتنسيق معين. عدم وجود روابط يعني أن المكونات لن تستلزم كسر التغييرات أثناء التنقية ، ولن تجبر المستخدمين على تكييف بياناتهم الخاصة بهم ، وستسمح بدمج المكونات الذرية مع بعضها البعض مثل مكعبات ليغو.
يعد اختيار العناصر نفسه مناسبًا لكل من قائمة السياق و combobox ، ويتم تحديد المكونات المتعددة ، والبسيطة ، بسهولة في تصاميم أكثر تعقيدًا. ومع ذلك ، يجب أن تكون قادرًا على عرض البيانات التعسفية بطريقة أو بأخرى.

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

البيانات التعسفية تشبه الماء: ليس لها شكل ، فهي لا تحمل أي شيء بعينها. مهمتنا هي توفير فرصة لوضع "سفينة" لهم. في هذا المعنى ، يكون تطوير مكون مستخلص من المظهر كما يلي:

المكون عبارة عن نوع من الرف بالحجم المطلوب ، ويتم استخدام قالب مخصص لعرض المحتوى ، الذي يتم "وضعه" عليه. يتم وضع طريقة قياسية ، مثل إخراج تمثيل سلسلة ، في المكون في البداية ، ويمكن للمستخدم نقل خيارات أكثر تعقيدًا من الخارج. دعونا نلقي نظرة على الاحتمالات التي لديها Angular لهذا الغرض.
إن أبسط طريقة لتغيير المظهر هي الاستيفاء الخطي. لكن الخط الثابت ليس مناسبًا لعرض عناصر القائمة ، لأنه لا يعرف شيئًا عن كل عنصر - وسيبدو جميعها كما هو. سلسلة ثابتة محرومة من السياق . ولكنه مناسب تمامًا لإعداد النص "Nothing Found" إذا كانت قائمة الخيارات فارغة.
<div>{{content}}</div>
لقد تحدثنا بالفعل عن تمثيل سلسلة من البيانات التعسفية. والنتيجة هي أيضًا سلسلة ، ولكن يتم تحديدها حسب قيمة الإدخال. في هذه الحالة ، سيكون السياق عنصر قائمة. هذا خيار أكثر مرونة ، على الرغم من أنه لا يسمح بتصميم النتيجة - السلسلة غير محاطة بلغة HTML - وأكثر من ذلك ، لذا لن تسمح باستخدام التوجيهات أو المكونات.
<div>{{content(context)}}</div>
الزاوي يوفر ng-template
والتوجيه الهيكلي *ngTemplateOutlet
. من خلال مساعدتهم ، يمكننا تحديد جزء من HTML يتوقع إدخال بعض البيانات ونقله إلى المكون. هناك سيتم إنشاء مثيل له مع سياق محدد. سننقل عنصرنا إليه دون الحاجة إلى القلق بشأن النموذج. إن إعداد القالب الصحيح لكائناتك هو مهمة مطور المستهلك لمكوننا.
<ng-container *ngTemplateOutlet="content; context: context"></ng-container>
القالب أداة قوية للغاية ، لكنه يحتاج إلى تعريف في بعض المكونات الموجودة. هذا يعقد إلى حد كبير إعادة استخدامها. أحيانًا يكون المظهر نفسه مطلوبًا في أجزاء مختلفة من التطبيق وحتى في التطبيقات المختلفة. في ممارستي ، على سبيل المثال ، هو ظهور اختيار الحساب مع عرض الاسم والعملة والتوازن.

الطريقة الأكثر تعقيدًا لتخصيص الشكل الذي يحل هذه المشكلة هي المكونات الديناميكية. في Angular ، *ngComponentOutlet
توجيه *ngComponentOutlet
منذ فترة طويلة *ngComponentOutlet
. لا يسمح بنقل السياق ، ولكن يتم حل هذه المشكلة عن طريق تنفيذ التبعيات. يمكننا عمل رمز مميز للسياق وإضافته إلى Injector
الذي تم إنشاء المكون به.
<ng-container *ngComponentOutlet="content; injector: injector"></ng-container>
تجدر الإشارة إلى أن السياق لا يمكن أن يكون فقط العنصر الذي نريد عرضه ، ولكن أيضًا الظروف التي يقع فيها:
<ng-template let-item let-focused="focused"> </ng-template>
على سبيل المثال ، في حالة سحب الحساب ، تنعكس حالة تركيز العنصر في المظهر - تتغير خلفية الرمز من الرمادي إلى الأبيض. بشكل عام ، من المنطقي نقل الظروف التي قد تؤثر على عرض القالب إلى السياق. ربما تكون هذه النقطة هي واجهة التقييد الوحيدة لهذا النهج.

المخرج العالمي
الأدوات الموضحة أعلاه متوفرة في Angular من الإصدار الخامس. لكننا نريد التبديل بسهولة من خيار إلى آخر. للقيام بذلك ، سنقوم بتجميع مكون يقبل المحتوى والسياق كمدخلات وينفذ الطريقة المناسبة لإدراج هذا المحتوى تلقائيًا. بشكل عام ، يكفي أن نتعلم التمييز بين أنواع string
، number
، (context: T) => string | number
(context: T) => string | number
، TemplateRef<T>
Type<any>
(ولكن هناك بعض الفروق الدقيقة هنا ، والتي سنناقشها أدناه).
سيبدو قالب المكون كما يلي:
<ng-container [ngSwitch]="type"> <ng-container *ngSwitchCase="'primitive'">{{content}}</ng-container> <ng-container *ngSwitchCase="'function'">{{content(context)}}</ng-container> <ng-container *ngSwitchCase="'template'"> <ng-container *ngTemplateOutlet="content; context: context"></ng-container> </ng-container> <ng-container *ngSwitchCase="'component'"> <ng-container *ngComponentOutlet="content; injector: injector"></ng-container> </ng-container> </ng-container>
سوف تحصل على رمز getter نوع لتحديد الطريقة المناسبة. تجدر الإشارة إلى أنه بشكل عام لا يمكننا التمييز بين عنصر من وظيفة. عند استخدام الوحدات النمطية البطيئة ، نحتاج إلى Injector
يعرف بوجود المكون. للقيام بذلك ، سنقوم بإنشاء فئة المجمع. هذا سيجعل من الممكن تحديد ذلك بواسطة instanceof
:
export class ComponentContent<T> { constructor( readonly component: Type<T>, private readonly injector: Injector | null = null, ) {} }
أضف طريقة لإنشاء حاقن مع السياق الذي تم تمريره:
createInjectorWithContext(injector: Injector, context: C): Injector { return Injector.create({ parent: this.injector || injector, providers: [ { provide: CONTEXT, useValue: context, }, ], }); }
بالنسبة للقوالب ، في معظم الحالات ، يمكنك التعامل معها كما هي. ولكن يجب أن نضع في اعتبارنا أن القالب يخضع للتحقق من التغييرات في مكان تعريفه. إذا قمت بنقلها إلى طريقة العرض ، والتي تكون متوازية أو أعلى في الشجرة من مكان التعريف ، فلن يتم التقاط التغييرات التي قد يتم تشغيلها بها في طريقة العرض الأصلية.
لتصحيح هذا الموقف ، سوف نستخدم ليس فقط قالبًا ، ولكن أيضًا توجيه سيحتوي أيضًا على ChangeDetectorRef
مكانه. بهذه الطريقة يمكننا البدء في التحقق من التغييرات عند الضرورة.
أنماط متعددة الأشكال
في الممارسة العملية ، قد يكون من المفيد التحكم في سلوك القالب اعتمادًا على نوع المحتوى الذي جاء فيه.
على سبيل المثال ، نريد منح الفرصة لنقل قالب إلى مكون لشيء خاص. في الوقت نفسه ، في معظم الحالات ، تحتاج فقط إلى رمز. في مثل هذه الحالة ، يمكنك تكوين السلوك الافتراضي واستخدامه عند إدخال إدخال أو وظيفة بدائية. في بعض الأحيان ، يكون نوع البدائية مهمًا: على سبيل المثال ، إذا كان لديك مكون شارة لعرض عدد الرسائل غير المقروءة في علامة تبويب ، ولكن في نفس الوقت تريد تسليط الضوء على الصفحات التي تتطلب الانتباه باستخدام رمز خاص.

للقيام بذلك ، تحتاج إلى إضافة شيء آخر - تمرير قالب لعرض العناصر الأولية. أضف @ContentChild
إلى المكون ، الذي يأخذ TemplateRef
من المحتوى. إذا تم العثور على واحد وتم تمرير دالة أو سلسلة أو رقم إلى المحتوى ، فيمكننا إنشاء مثيل له باستخدام البدائي كسياق:
<ng-container *ngSwitchCase="'interpolation'"> <ng-container *ngIf="!template; else child">{{primitive}}</ng-container> <ng-template #child> <ng-container *ngTemplateOutlet="template; context: { $implicit: primitive }" ></ng-container> </ng-template> </ng-container>
الآن يمكننا تصميم الاستيفاء أو حتى تمرير النتيجة إلى بعض المكونات للعرض:
<content-outlet [content]="content" [context]="context"> <ng-template let-primitive> <div class="primitive">{{primitive}}</div> </ng-template> </content-outlet>
حان الوقت لوضع الشفرة موضع التنفيذ.
استخدام
على سبيل المثال ، نوضح مكونين: علامات التبويب و ComboBox . سيتألف قالب علامة التبويب ببساطة من منفذ محتوى لكل علامة تبويب ، حيث يكون الكائن الذي تم تمريره بواسطة المستخدم هو السياق:
<content-outlet *ngFor="let tab of tabs" [class.disabled]="disabledItemHandler(tab)" [content]="content" [context]="getContext(tab)" (click)="onClick(tab)" ></content-outlet>
تحتاج إلى تعيين الأنماط الافتراضية: على سبيل المثال ، حجم الخط ، تسطير تحت علامة التبويب الحالية ، اللون. ولكن سوف نترك مظهرًا ملموسًا للمحتوى. سيكون رمز المكون مثل هذا:
export class TabsComponent<T> { @Input() tabs: ReadonlyArray<T> = []; @Input() content: Content = ({$implicit}) => String($implicit); @Input() disabledItemHandler: (tab: T) => boolean = () => false; @Input() activeTab: T | null = null; @Output() activeTabChange = new EventEmitter<T>(); getContext($implicit: T): IContextWithActive<T> { return { $implicit, active: $implicit === this.activeTab, }; } onClick(tab: T) { this.activeTab = tab; this.activeTabChange.emit(tab); } }
لقد حصلنا على مكون يمكنه العمل مع صفيف اعتباطي ، وعرضه كعلامات تبويب. يمكنك ببساطة تمرير السلاسل هناك والحصول على الشكل الأساسي:

ويمكنك نقل الكائنات والقالب لعرضها وتخصيص المظهر ليناسب احتياجاتك ، وإضافة HTML ، والرموز ، والمؤشرات:

في حالة ComboBox ، سنقوم أولاً بإنشاء مكونين أساسيين يتكون منهما: حقل إدخال مع أيقونة وقائمة. هذا الأخير لا معنى للطلاء بالتفصيل - إنه يشبه إلى حد كبير علامات التبويب ، فقط رأسياً ولديه أنماط أساسية أخرى. ويمكن تنفيذ حقل الإدخال على النحو التالي:
<input #input [(ngModel)]="value"/> <content-outlet [content]="content" (mousedown)="onMouseDown($event, input)" > <ng-template let-icon> <div [innerHTML]="icon"></div> </ng-template> </content-outlet>
إذا قمت بإجراء الإدخال في وضع مطلق ، فسيتم حظر المنفذ وستكون جميع النقرات عليه. هذا مناسب لحقل إدخال بسيط برمز زخرفي ، مثل رمز العدسة المكبرة. في المثال أعلاه ، يتم تطبيق نهج القوالب المتعددة الأشكال - سيتم استخدام السلسلة المنقولة كـ innerHTML
لإدراج رمز SVG. على سبيل المثال ، إذا كنا بحاجة إلى إظهار الصورة الرمزية للمستخدم الذي تم إدخاله ، فيمكننا نقل القالب هناك.
يحتاج ComboBox أيضًا إلى رمز ، ولكن يجب أن يكون تفاعليًا. لمنعه من كسر التركيز ، أضف معالج onMouseDown
إلى المنفذ:
onMouseDown(event: MouseEvent, input: HTMLInputElement) { event.preventDefault(); input.focus(); }
تمرير القالب كمحتوى سيسمح لنا برفعه أعلى عبر CSS ببساطة عن طريق جعل الموضع: رمز نسبي . بعد ذلك ، يمكنك الاشتراك في النقرات الموجودة عليه في مربع التحرير والسرد نفسه:
<app-input [content]="icon"></app-input> <ng-template #icon> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="icon" [class.icon_opened]="opened" (click)="onClick()" > <polyline points="7,10 12,15 17,10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" /> </svg> </ng-template>
بفضل هذه المنظمة ، نحصل على السلوك المطلوب:

رمز المكون ، كما في حالة علامات التبويب ، يستغني عن المعرفة بنموذج البيانات. يبدو شيء مثل هذا:
export class ComboBoxComponent<T> { @Input() items: ReadonlyArray<T> = []; @Input() content: Content = ({$implicit}) => String($implicit); @Input() stringify = (item: T) => String(item); @Input() value: T | null = null; @Output() valueChange = new EventEmitter<T | null>(); stringValue = '';
تسمح لك هذه التعليمة البرمجية البسيطة باستخدام أي كائنات في ComboBox وتخصيص عرضها بمرونة كبيرة. بعد بعض التحسينات التي لا تتعلق بالمفهوم الموصوف ، تكون جاهزة للاستخدام. يمكن تخصيص المظهر لكل ذوق:

استنتاج
إنشاء مكونات اللاأدرية يلغي الحاجة إلى أن تأخذ في الاعتبار كل حالة معينة. في الوقت نفسه ، يحصل المستخدمون على أداة بسيطة لتكوين المكون لموقف معين. هذه الحلول هي سهلة لإعادة استخدامها. الاستقلال عن نموذج البيانات يجعل الشفرة عالمية وموثوقة وقابلة للتمديد. في الوقت نفسه ، لم نكتب الكثير من الخطوط واستخدمنا الأدوات الزاوية المدمجة بشكل أساسي.
باستخدام الطريقة الموضحة ، ستلاحظ بسرعة مدى ملاءمة التفكير فيما يتعلق بالمحتوى بدلاً من خطوط أو أنماط محددة. عرض رسائل خطأ التحقق من الصحة ، تلميحات الأدوات ، الإطارات مشروط - هذا النهج هو جيد ليس فقط لتخصيص المظهر ، ولكن أيضا لنقل المحتوى ككل. تخطيطات الرسم واختبار المنطق سهل! على سبيل المثال ، لإظهار النافذة المنبثقة ، لا يحتاج المستخدم إلى إنشاء مكون أو حتى قالب ، يمكنك فقط تمرير سلسلة كعب الروتين والعودة إليها لاحقًا.
لقد قمنا في Tinkoff.ru منذ فترة طويلة بتطبيق النهج الموصوف ونقله إلى مكتبة صغيرة مفتوحة المصدر (1 كيلوبايت gzip) تسمى ng-polymorpheus .
شفرة المصدر
حزمة npm
عرض تجريبي ورمل رمل
هل لديك أيضًا شيء تريد وضعه في مصدر مفتوح ، لكن هل أنت خائف من الأعمال المنزلية المرتبطة به؟ جرب بداية مكتبة الزاوي مفتوحة المصدر ، التي قطعناها على أنفسنا لمشاريعنا. تمت تهيئته بالفعل لـ CI ، والتحقق من الإلتزامات ، والنوافذ ، وإنشاء CHANGELOG ، وتغطية الاختبار وكل تلك الأشياء.