Skip to content

اسلات‌ها - Slots

در این صفحه فرض شده که شما از قبل مبانی کامپوننت‌ها را مطالعه کرده اید. اگر با کامپوننت‌ها آشنایی ندارید، ابتدا آن را بخوانید.

محتوا و خروجی اسلات

آموختیم که کامپوننت‌ها می‌توانند پراپ‌ها را دریافت کنند، که می‌تواند داده‌های جاوااسکریپت از هر تایپی باشند. اما محتوای تمپلیت چطور؟ در بعضی موارد نیاز داریم بخشی از تمپلیت را به کامپوننت فرزند منتقل کنیم، و اجازه دهیم که کامپوننت فرزند آن بخش را درون تمپلیت خودش رِندر کند.

برای مثال، ما یک کامپوننت <FancyButton> داریم که کاربرد زیر را پشتیبانی می‌کند:

template
<FancyButton>
  Click me! <!-- محتوای اسلات -->
</FancyButton>

تمپلیتِ <FancyButton> اینگونه خواهد بود:

template
<button class="fancy-btn">
  <slot></slot> <!-- خروجی اسلات -->
</button>

المنت <slot> یک خروجی اسلات است که نشان می‌دهد محتوای اسلات فراهم شده توسط والد در کجا باید رِندر شود.

slot diagram

و نتیجه نهایی DOM رِندر شده:

html
<button class="fancy-btn">Click me!</button>

با استفاده از اسلات‌ها، کامپوننت <FancyButton> مسئول رِندر کردن <button> خروجی (و استایل‌های کلاس fancy-btn آن) است، درحالیکه محتوای درونی توسط کامپوننت والد فراهم می‌شود.

روش دیگر برای درک اسلات‌ها مقایسه آنها با توابع جاوااسکریپتی هست:

js
// کامپوننت والد محتوای اسلات را ارسال می‌کند
FancyButton('Click me!')

// FancyButton محتوای اسلات را درون تمپلیت خودش رِندر می‌کند
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

محتوای اسلات فقط به یک متن ساده محدود نمی‌شود. می‌تواند هر محتوای معتبر تمپلیت باشد. برای مثال، می‌توانیم چندین المنت یا حتی کامپوننت‌های دیگر را ارسال کنیم:

template
<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

بوسیله اسلات‌ها، کامپوننت <FancyButton> ما انعطاف پذیرتر و قابل استفاده دوباره است. ما حالا می‌توانیم در مکان‌های مختلف با محتوای درونی متفاوت از آن استفاده کنیم، اما با همان استایل fancy-btn.

سازوکار اسلاتِ کامپوننت‌های Vue از المنت <slot> کامپوننت وب بومی الهام گرفته است، اما با امکانات اضافی که بعدا خواهیم دید.

اسکوپ رِندر

محتوای اسلات به اسکوپ داده کامپوننت والد دسترسی دارد، زیرا درون والد تعریف شده است. برای مثال:

template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

در اینجا هردو {{ message }} محتوای یکسانی را رِندر خواهند کرد.

محتوای اسلات به داده کامپوننت فرزند دسترسی ندارد. عبارات تمپلیت‌های Vue فقط به اسکوپی که درون آن تعریف شده اند، سازگار با اسکوپ واژگانی جاوااسکریپت دسترسی دارند. به عبارت دیگر:

عبارات در تمپلیت والد فقط به اسکوپ والد دسترسی دارند و عبارات در تمپلیت فرزند فقط به اسکوپ فرزند دسترسی دارند.

محتوای بازگشتی

مواردی وجود دارد که مفید است برای اسلات در هنگامی که محتوایی برای ارائه ندارد محتوای بازگشتی (مقدار پیش فرض) تعیین کنیم. برای مثال، در یک کامپوننت <SubmitButton>:

template
<button type="submit">
  <slot></slot>
</button>

اگر والد هیچگونه محتوایی ارائه نکرده باشد ممکن است بخواهیم نوشته "Submit" را درون <button> رِندر کنیم. برای اینکه "Submit" را محتوای بازگشتی کنیم، می‌توانیم آن را بین تگ های <slot> قرار دهیم:

template
<button type="submit">
  <slot>
    Submit <!-- محتوای بازگشتی -->
  </slot>
</button>

اکنون که کامپوننت <SubmitButton> را در یک کامپوننت والد استفاده می‌کنیم، بدون هیچگونه محتوای فراهم شده ای برای اسلات:

template
<SubmitButton />

کامپوننت <SubmitButton> محتوای بازگشتی "Submit" را رِندر خواهد کرد:

html
<button type="submit">Submit</button>

ولی اگر محتوا را فراهم کنیم:

template
<SubmitButton>Save</SubmitButton>

در نتیجه محتوای ارائه شده به جای آن رِندر خواهد شد:

html
<button type="submit">Save</button>

اسلات‌های نام گذاری شده

مواقعی پیش می‌آید که نیاز داریم چندین خروجی اسلات در یک کامپوننت داشته باشیم. برای مثال، در یک کامپوننت <BaseLayout> با تمپلیت زیر:

template
<div class="container">
  <header>
    <!-- را در اینجا می‌خواهیم header ما محتوای -->
  </header>
  <main>
    <!-- را در اینجا می‌خواهیم main ما محتوای -->
  </main>
  <footer>
    <!-- را در اینجا می‌خواهیم footer ما محتوای -->
  </footer>
</div>

برای چنین مواردی، المنت <slot> ویژگی به خصوصی به نام name دارد، که یک ID منحصر بفرد برای اسلات‌های مختلف تعیین می‌کند تا شما بتوانید مشخص کنید محتوا کجا باید رِندر شود:

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

خروجی <slot> بدون name به طور ضمنی نام "default" را دارد.

در کامپوننت والد که از <BaseLayout> استفاده می‌کند، به راهی برای ارسال بخش‌های متعدد محتوای اسلات که هرکدام خروجی اسلات متفاوتی دارند نیاز داریم. در اینجا اسلات‌های نام گذاری شده وارد می‌شوند.

برای ارسال اسلات نام گذاری شده، نیاز داریم المنت <template> را همراه با دایرکتیو v-slot به کار ببریم، و در نهایت نام اسلات را به عنوان آرگومان به v-slot ارسال کنیم:

template
<BaseLayout>
  <template v-slot:header>
    <!-- header محتوا برای اسلات -->
  </template>
</BaseLayout>

v-slot مخفف اختصاصی # را دارد، پس <template v-slot:header> می‌تواند به <template #header> خلاصه شود. به آن به عنوان "رِندر کردن این بخش از تمپلیت در اسلات header کامپوننت فرزند" فکر کنید.

نمودار اسلات‌های نام گذاری شده

در اینجا محتوای ارسال شده برای هر سه اسلات کامپوننت <BaseLayout> با استفاده از نوشتار خلاصه شده را داریم:

template
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

هنگامی که یک کامپوننت هر دو اسلات default و اسلات نام گذاری شده را دریافت می‌کند، همه نودهای سطح بالا که در تگ <template> حضور ندارند به عنوان محتوای اسلات default به حساب می‌آیند. بنابراین کد بالا همچنین می‌تواند به صورت زیر نوشته شود:

template
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- بصورت ضمنی default اسلات -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

حالا هر چیزی درون المنت‌های <template> به اسلات‌های مربوطه ارسال خواهد شد. HTML رِندر شده نهایی به صورت زیر خواهد بود: (مترجم: دقت کنید که در کدهای بالا برای تگ main همان اسلات default را در نظر گرفته بود)

html
<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

دوباره، این ممکنه به شما کمک کند که اسلات‌های نام گذاری شده را با استفاده از مقایسه تابع جاوااسکریپت بهتر درک کنید:

js
// ارسال بخش های متعدد اسلات به همراه نام های مختلف
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// آنها را در جاهای مختلف رِندر می‌کند <BaseLayout>
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

اسلات‌های شرطی

گاهی اوقات می‌خواهید چیزی را بر اساس وجود یا عدم وجود یک اسلات رندر کنید.

می‌توانید از پراپرتی ‎$slots در ترکیب با v-if برای رسیدن به این هدف استفاده کنید.

در مثال زیر یک کامپوننت کارت را با دو اسلات شرطی تعریف می‌کنیم: header و footer. زمانی که هدر / فوتر حضور داشته باشد، می‌خواهیم آنها را با یک المنت جدا بپیچیم تا استایل اضافی اعمال شود:

template
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    
    <div class="card-content">
      <slot />
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

آن را در Playground امتحان کنید

نام‌ دهی اسلات بصورت پویا

آرگومان‌های دایرکتیو پویا نیز در v-slot کار می‌کند، اجازه تعریف نام اسلات بصورت پویا را می‌دهد:

template
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- به اختصار -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

توجه داشته باشید که عبارت، منوط به محدودیت‌های نوشتاری آرگومان‌های دایرکتیو پویا است.

اسلات‌های دارای اسکوپ

همانطور که در اسکوپ رِندر بحث شد، محتوای اسلات به استیت(state) کامپوننت فرزند دسترسی ندارد.

اگرچه، مواردی وجود دارد که مفید است اگر محتوای یک اسلات بتواند از داده هر دو اسکوپ والد و اسکوپ فرزند استفاده کند. برای دستیابی به این هدف، ما به راهی نیاز داریم که داده را از فرزند به اسلات در هنگام رِندر کردن آن ارسال کنیم.

در واقع، ما می‌توانیم دقیقا همچنین کاری کنیم - می‌توانیم اتریبیوت‌ها را به خروجی یک اسلات همانند پراپ‌های یک کامپوننت ارسال کنیم:

template
<!-- <MyComponent> تمپلیت -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

دریافت پراپ‌های اسلات هنگامی که از یک اسلات default در مقابل استفاده از اسلات‌های نام گذاری شده استفاده می‌شود کمی متفاوت است. ما در ابتدا بوسیله v-slot که مستقیما در تگ کامپوننت فرزند آورده شده است چگونگی دریافت پراپ‌ها با استفاده از اسلات را نشان می دهیم:

template
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

scoped slots diagram

پراپ‌هایی که به اسلات ارسال می‌شوند توسط فرزند به عنوان مقدار دایرکتیو v-slot مربوطه در دسترس هستند که می‌توانند بوسیله عبارات درون اسلات دریافت شوند.

شما می‌توانید به "اسلات‌های دارای اسکوپ" به عنوان تابعی که به کامپوننت فرزند ارسال شده است فکر کنید. کامپوننت فرزند سپس آن را صدا می زند و پراپ‌ها را به عنوان آرگومان به آن پاس می‌دهد.

js
MyComponent({
  // ولی به عنوان تابع ،default ارسال اسلات
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return `<div>${
    // تابع اسلات را همرا با پراپ‌ها صدا بزنید
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

در حقیقت کد بالا به این مورد که "اسلات‌های دارای اسکوپ" چگونه کامپایل می‌شوند و نحوه استفاده از "اسلات‌های دارای اسکوپ" در راهنمای توابع رِندر چگونه است خیلی نزدیک است.

توجه کنید که v-slot="slotProps"‎ چگونه با امضای تابع اسلات مطابقت دارد. درست همانند آرگومان‌های تابع می‌توانیم از جداسازی ساختار(destructuring) در v-slot استفاده کنیم:

template
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

اسلات‌های دارای اسکوپ نام گذاری شده

اسلات‌های دارای اسکوپ نام گذاری شده به طور مشابه کار می کنند - پراپ‌های اسلات به عنوان مقدار دایرکتیو v-slot در دسترس هستند: v-slot:name="slotProps"‎. هنگامی که از خلاصه نویسی استفاده می کنید به شکل زیر در می‌آید:

template
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

ارسال پراپ‌ها به اسلات نام گذاری شده:

template
<slot name="header" message="hello"></slot>

توجه داشته باشید که ویژگی name اسلات در پراپ‌ها گنجانده نمی‌شود - بنابراین نتیجه نهایی headerProps به صورت { message: 'hello' } می‌شود.

اگر اسلات‌های نام گذاری شده را با اسلات‌های دارای اسکوپ default ترکیب کنید، نیاز هست که تگ <template> را به طور مشخص برای اسلات پیش فرض به کار ببرید. تلاش برای قرار دادن دایرکتیو v-slot به صورت مستقیم بر روی کامپوننت خطای کامپایلر را نتیجه خواهد داد. این کار برای جلوگیری از هرگونه ابهام درباره محدوده پراپ‌های اسلات پیش فرض است. برای مثال:

template
<!-- این تمپلیت کامپایل نخواهد شد -->
<template>
  <MyComponent v-slot="{ message }">
    <p>{{ message }}</p>
    <template #footer>
      <!-- message به اسلات پیش فرض تعلق دارد و اینجا قابل دسترس نیست -->
      <p>{{ message }}</p>
    </template>
  </MyComponent>
</template>

استفاده از تگ <template> به طور مشخص برای اسلات پیش فرض به روشن شدن اینکه پراپ message درون اسلات دیگر قابل دسترس نیست کمک می‌کند:

template
<template>
  <MyComponent>
    <!-- از اسلات پیش فرض مشخص استفاده کنید -->
    <template #default="{ message }">
      <p>{{ message }}</p>
    </template>

    <template #footer>
      <p>Here's some contact info</p>
    </template>
  </MyComponent>
</template>

مثال Fancy List

ممکنه از خود بپرسید یک مورد استفاده خوب برای اسلات‌های دارای اسکوپ چیست. در اینجا یک مثال آورده شده است: یک کامپوننت <FancyList> را تصور کنید که لیستی از آیتم ها را رِندر می‌کند - ممکن است منطق بارگیری داده‌ها از یک مکان دیگر را پیاده سازی کند، استفاده از داده ها برای نمایش لیست یا حتی ویژگی های پیشرفته مانند صفحه بندی یا پیمایش بی نهایت(infinite scrolling) را اصطلاحاً کپسوله سازی کند. با این حال، ما می‌خواهیم که ظاهر هر آیتم انعطاف‌پذیر باشد و پیاده سازی استایل هر آیتم را به کامپوننت والد مصرف‌کننده آن بسپاریم. بنابراین استفاده مورد نظر ممکن است به این صورت باشد:

template
<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

در داخل <FancyList>، می‌توانیم همان <slot> را چندین بار با داده‌های آیتم‌های مختلف رِندر کنیم(توجه داشته باشید که ما از v-bind برای ارسال یک آبجکت به عنوان پراپ‌های اسلات استفاده می‌کنیم):

template
<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

Renderless Components

مورد استفاده <FancyList> که در بالا مورد بحث قرار دادیم، هم منطق قابلیت استفاده مجدد (دریافت داده، صفحه بندی و غیره) و هم خروجی بصری(visual output) را در بر می‌گیرد، در حالی که بخشی از خروجی بصری را از طریق اسلات‌های دارای اسکوپ به کامپوننت مصرف کننده واگذار می‌کند.

اگر این مفهوم را کمی بیشتر پیش ببریم، می‌توانیم به کامپوننت‌هایی برسیم که فقط منطق را در بر می‌گیرند و هیچ چیزی را به خودی خود رِندر نمی‌کنند - خروجی بصری به طور کامل به کامپوننت مصرف‌کننده با اسلات‌های دارای اسکوپ واگذار می‌شود. ما این نوع کامپوننت را کامپوننت بدون رِندر می نامیم.

یک نمونه کامپوننت بدون رِندر می‌تواند کامپوننتی باشد که منطق ردیابی موقعیت فعلی ماوس را در بر می گیرد:

template
<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

در حالی که یک الگوی جالب است، بسیاری از چیزهایی که می‌توان با کامپوننت‌های بدون رِندر به دست آورد را می‌توان به روشی کارآمدتر با Composition API به دست آورد، بدون اینکه متحمل سربار اضافی تودرتویی کامپوننت شود. بعداً خواهیم دید که چگونه می‌توانیم همان عملکرد ردیابی ماوس را به عنوان یک Composable پیاده سازی کنیم.

با این اوصاف، اسلات‌های دارای اسکوپ هنوز هم در مواردی مفید هستند که نیاز داریم هم منطق و هم خروجی بصری را محصور کنیم، مانند مثال <FancyList>.

اسلات‌ها - Slots has loaded