Skip to content

اسلات ها

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

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

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

برای مثال، ما یک کامپوننت <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> خروجی (و استایل های تجملی آن) است، درحالیکه محتوای درونی توسط کامپوننت والد فراهم می شود.

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

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> ما منعطف تر و بیشتر قابل استفاده دوباره است. ما حالا می توانیم در جاهای مختلف با محتوای درونی متفاوت از آن استفاده کنیم، اما با همان استایل تجملی.

سازوکار اسلات کامپوننت های 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>

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

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

  <!-- اسلات پیش فرض ضمنی -->
  <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 رِندر شده نهایی به صورت زیر خواهد بود:

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>`
}

نام های اسلات پویا

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

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

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

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

اسلات های محدوده

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

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

در واقع، ما میتوانیم دقیقا همچنین کاری کنیم - می توانیم ویژگی ها(attributes) را به خروجی یک اسلات همانند پراپ های یک کامپوننت ارسال کنیم:

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

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

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

scoped slots diagram

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

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

js
MyComponent({
  // ارسال اسلات پیش فرض، ولی به عنوان تابع
  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' } می شود.

اگر اسلات های نام گذاری شده را با اسلات محدوده پیش فرض ترکیب کنید، نیاز هست که تگ <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> را تصور کنید که لیستی از آیتم ها را رِندر می کند - ممکن است منطق بارگذاری داده دور دست، استفاده از داده ها برای نمایش لیست یا حتی ویژگی های پیشرفته مانند صفحه بندی(pagination) یا پیمایش بی نهایت(infinite scrolling) را محصور کند(encapsulate). با این حال، ما می‌خواهیم که با ظاهر هر آیتم انعطاف‌پذیر باشد و استایل هر آیتم را به کامپوننت والد مصرف‌کننده آن بسپاریم. بنابراین استفاده مورد نظر ممکن است به این صورت باشد:

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>.

اسلات ها has loaded