در جلسه قبل موفق شدیم که کدها را بدون اشکال بنویسیم اما هنوز یک مسئله برای من قابل قبول نیست. در کدهای ما تعداد افراد نمایش داده شده فقط به صورت عدد 5 می باشد و هیچ اطلاعات دیگری در مورد آن نمی بینیم. در این جلسه می خواهیم این مسئله را با یک Getter حل کنیم. کدهای مربوط به این قسمت در جلسه قبل بدین شکل بود (کلاس ProjectItem):
renderContent() { this.element.querySelector('h2')!.textContent = this.project.title; this.element.querySelector( 'h3' )!.textContent = this.project.people.toString(); this.element.querySelector('p')!.textContent = this.project.description; }
ما می توانیم مقداری را قبل از آن اضافه کنیم، مثلا:
renderContent() { this.element.querySelector('h2')!.textContent = this.project.title; this.element.querySelector( 'h3' )!.textContent = this.project.people.toString() + ' Persons assigned.'; this.element.querySelector('p')!.textContent = this.project.description; }
در زبان انگلیسی اگر فقط یک نفر داشته باشیم باید به جای persons از person استفاده کنیم (بدون S جمع) بنابراین به مشکل برمی خوریم چرا که اگر افراد بیشتر از یکی باشند باید s بگیریم و اگر یک نفر باشد باید s را حذف کنیم. به نظر شما راه حل چیست؟ من برای حل این مشکل درون کلاس ProjectItem از یک getter استفاده می کنم:
/ ProjectItem Class class ProjectItem extends Component<HTMLUListElement, HTMLLIElement> { private project: Project; get persons() { if (this.project.people === 1) { return '1 person'; } else { return `${this.project.people} persons`; } }
منطق این کدها بسیار ساده است و فقط یک رشته ساده را بر اساس شرط if برمی گرداند.حالا می توانیم کدهای متد renderContent در این کلاس را به شکل زیر بنویسیم:
renderContent() { this.element.querySelector('h2')!.textContent = this.project.title; this.element.querySelector('h3')!.textContent = this.persons + ' assigned'; this.element.querySelector('p')!.textContent = this.project.description; }
توجه کنید که getter خودمان را به صورت تابع (با پرانتز) صدا نمی زنیم. حالا که مشکل ما حل شده است به سراغ بحث اصلی می رویم.
قابلیت drag & drop یعنی قابلیت کشیدن و رها کردن اعضای مختلف صفحات وب. به زبان ساده تر می خواهم کاری کنم که بتوانیم یک پروژه را از active projects کشیده و در finished projects رها کنیم تا در این دسته بندی قرار بگیرد (یا برعکس). ما از این جلسه این کار را شروع کرده و در جلسات آینده آن را تمام می کنیم. برای شروع باید یک interface داشته باشیم تا نه تنها ساختار اشیاء مربوط به آن را مشخص کنیم بلکه به عنوان قراردادی برای کلاس های دیگر قرار بگیرد تا متدهای خاصی را پیاده سازی کنند. زمانی که کدنویسی را شروع کنیم متوجه حرف های من می شوید.
من دو interface مختلف را در ذهنم دارم: draggable و dragTarget. من می خواهم draggable را به هر کلاسی که مسئول نمایش آیتم های قابل Drag شدن است، اضافه کنم (در پروژه ما همان کلاس ProjectItem). کلاس ProjectItem مسئول نمایش آیتم های ما است و همین آیتم ها نیز باید قابل Drag شدن باشند بنابراین کلاس هدف ما برای draggable این کلاس است. dragTarget نیز باید محل های هدف ما برای رها کردن (drop) باشند که در پروژه ما می شود کلاس ProjectList. در interface اول نیاز به دو event-listener داریم بنابراین دو handler برای آن ها می خواهیم:
// Drag & Drop Interfaces interface Draggable { dragStartHandler(event: DragEvent): void; dragEndHandler(event: DragEvent): void; }
تایپ این دو متد از نوع DragEvent است که از قبل در تایپ اسکریپت تعریف شده است و void را برمی گردانند (یعنی یا چیزی برنمی گردانند یا اصلا برای ما اهمیتی ندارد که چیزی برگردانند یا خیر). متد اول شروع drag را مشخص کرده و متد دوم پایان آن را مشخص می کند.
برای dragTarget نیاز به سه متد جداگانه داریم:
// Drag & Drop Interfaces interface Draggable { dragStartHandler(event: DragEvent): void; dragEndHandler(event: DragEvent): void; } interface DragTarget { dragOverHandler(event: DragEvent): void; dropHandler(event: DragEvent): void; dragLeaveHandler(event: DragEvent): void; }
متد اول (dragOverHandler) در پروژه های Drag & drop استفاده می شود تا به جاوا اسکرپیت و مرورگر بگوییم هدفِ drop یک هدف معتبر است و drop را قبول می کند. متد دوم (dropHandler) برای انجام عملیات خاصی پس از Drop (رها) شدن عنصر است. به زبان ساده تر dropHandler اجازه این عملیات خاص و drop شدن را از dragOverHandler می گیرد و سپس عملیات خاصی را انجام می دهد. در نهایت متد dragLeaveHandler نیز در زمانی فعال می شود که کاربر از قسمت هدف خارج بشود و drop نکند (هنگام کنسل شدن عملیات).
حالا به سراغ کلاس ProjectItem می رویم تا interface خود را روی آن اعمال کنیم:
// ProjectItem Class class ProjectItem extends Component<HTMLUListElement, HTMLLIElement> implements Draggable { private project: Project; // بقیه کدها //
همانطور که قبلا یاد گرفتیم، با استفاده از کلیدواژه implements می توانیم interface ها را روی کلاس ها پیاده سازی کنیم. زمانی که این کار را می کنیم خطایی دریافت می کنیم. چرا؟ به دلیل اینکه کلاس ما از interface تعریف شده پیروی نمی کند، یعنی دارای متدهای تعریف شده در interface نیست. برای حل این خطا می گوییم:
// ProjectItem Class class ProjectItem extends Component<HTMLUListElement, HTMLLIElement> implements Draggable { private project: Project; get persons() { if (this.project.people === 1) { return '1 person'; } else { return `${this.project.people} persons`; } } constructor(hostId: string, project: Project) { super('single-project', hostId, false, project.id); this.project = project; this.configure(); this.renderContent(); } @autobind dragStartHandler(event: DragEvent) { console.log(event); } dragEndHandler(_: DragEvent) { console.log('DragEnd'); } configure() { this.element.addEventListener('dragstart', this.dragStartHandler); this.element.addEventListener('dragend', this.dragEndHandler); } // بقیه کدها //
اگر دقت کنید من ابتدا دو متد dragStartHandler و dragEndHandler را تعریف کرده ام. برای متد dragStartHandler بعدا از کلیدواژه This استفاده خواهیم کرد و به همین دلیل از autobind decorator خود استفاده کرده ایم تا به مشکل برنخوریم (این موضوع را در قسمت های اولیه این فصل توضیح دادیم). برای متد dragEndHandler نیز گفته ایم نیازی به event دریافتی نداریم (به همین خاطر از آندرلاین برای پارامتر ورودی آن استفاده کرده ایم). همانطور که می دانید در تایپ اسکریپت نمی توانید پارامترهای ورودی را بدون استفاده بگذارید و اگر مجبور شدید می توانید از علامت _ (آندرلاین) استفاده کنید تا خطای تایپ اسکریپت را از بین ببرید. هر دوی این متدها فقط یک عبارت یا رویداد خاص را log می کنند و فعلا برای تست نوشته شده اند.
در نهایت در متد configure دو event-listener مربوط به این متدها را نوشته ایم تا با شروع شدن رویداد drag فراخوانی شوند. توجه داشته باشید که رویداد های 'dragstart' و 'dragend' هر دو جزئی از جاوا اسکریپت ساده هستند و مربوط به typescript نمی باشند.
اگر الان کدهای برنامه را تست کنید، می بینید که هیچ چیز قابلیت Drag شدن ندارد. برای اینکه بتوانیم عنصری در html را draggable کنیم باید خصوصیت مربوط به آن را به همان عنصر اضافه کنیم. در واقع ما می خواهیم li های خود را draggable کنیم بنابراین به index.html رفته و می گوییم:
<template id="single-project"> <li draggable="true"> <h2></h2> <h3></h3> <p></p> </li> </template>
حالا می توانید کدها را تست کنید:
همانطور که مشاهده می کنید قابلیت drag & drop در ظاهر پیاده سازی شده است. البته یک کار باقی مانده است. به فایل app.css رفته و کد مربوط به li ها را به شکل زیر تغییر بدهید:
.projects li { box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.26); padding: 1rem; margin: 1rem; background: white; }
دلیل اضافه کردن background سفید این است که هنگام drag کردن عناصر، بتوانیم آن ها را بهتر ببینیم. در قسمت بعد روی drop کار خواهیم کرد.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.