Przejdź bezpośrednio do treści

Sloty ​

Ta strona zakłada, że przeczytałeś już Podstawy Komponentów. Przeczytaj je najpierw, jeśli dopiero zaczynasz pracę z komponentami.

Zawartość Slotów i Outlet ​

Dowiedzieliśmy się, że komponenty mogą przyjmować właściwości (props), które mogą być wartościami JavaScript dowolnego typu. Ale co z zawartością szablonu? W niektórych przypadkach możemy chcieć przekazać fragment szablonu do komponentu potomnego i pozwolić mu wyrenderować ten fragment w swoim własnym szablonie.

Na przykład, możemy mieć komponent <FancyButton>, który wspiera użycie w taki sposób:

template
<FancyButton>
  Click me! <!-- zawartość slotu -->
</FancyButton>

Szablon <FancyButton> wygląda następująco:

template
<button class="fancy-btn">
  <slot></slot> <!-- zawartość slotu -->
</button>

Element <slot> jest outletem slotu, który wskazuje, gdzie powinna zostać wyrenderowana zawartość slotu dostarczona przez rodzica.

schemat slotu

A końcowy wyrenderowany DOM:

html
<button class="fancy-btn">Wciśnij mnie!</button>

Dzięki slotom, <FancyButton> jest odpowiedzialny za renderowanie zewnętrznego <button> (oraz jego ozdobnego stylowania), podczas gdy wewnętrzna zawartość jest dostarczana przez komponent nadrzędny.

Innym sposobem na zrozumienie slotów jest porównanie ich do funkcji JavaScript:

js
// komponent nadrzędny przekazujący zawartość slotu
FancyButton('Wciśnij mnie!')

// FancyButton renderuje zawartość slotu w swoim własnym szablonie
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
     ${slotContent}
   </button>`
}

Zawartość slotu nie jest ograniczona tylko do tekstu. Może to być dowolna prawidłowa zawartość szablonu. Na przykład, możemy przekazać wiele elementów, a nawet inne komponenty:

template
<FancyButton>
  <span style="color:red">Wciśnij mnie!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

Dzięki wykorzystaniu slotów, nasz <FancyButton> jest bardziej elastyczny i możliwy do ponownego użycia. Możemy teraz używać go w różnych miejscach z różną zawartością wewnętrzną, ale zawsze z tym samym ozdobnym stylowaniem.

Mechanizm slotów w komponentach Vue jest inspirowany natywnym elementem <slot> Web Components, ale posiada dodatkowe możliwości, które zobaczymy później.

Zakres renderowania ​

Zawartość slotu ma dostęp do zakresu danych komponentu nadrzędnego, ponieważ jest zdefiniowana w rodzicu. Na przykład:

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

Tutaj obie interpolacje {{ message }} wyrenderują tę samą zawartość.

Zawartość slotu nie ma dostępu do danych komponentu potomnego. Wyrażenia w szablonach Vue mogą uzyskać dostęp tylko do zakresu, w którym są zdefiniowane, zgodnie z leksykalnym zakresem JavaScript. Innymi słowy:

Wyrażenia w szablonie nadrzędnym mają dostęp tylko do zakresu nadrzędnego; wyrażenia w szablonie potomnym mają dostęp tylko do zakresu potomnego.

Zawartość domyślna ​

Istnieją przypadki, gdy przydatne jest określenie zawartości zastępczej (tzn. domyślnej) dla slotu, która będzie renderowana tylko wtedy, gdy nie dostarczono żadnej zawartości. Na przykład, w komponencie <SubmitButton>:

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

Możemy chcieć, aby tekst "Prześlij" był renderowany wewnątrz <button>, jeśli rodzic nie dostarczył żadnej zawartości slotu. Aby ustawić "Prześlij" jako zawartość domyślną, możemy umieścić go pomiędzy tagami <slot>:

template
<button type="submit">
  <slot>
    Prześlij <!-- wartość domyślna -->
  </slot>
</button>

Teraz, gdy używamy <SubmitButton> w komponencie nadrzędnym, nie dostarczając żadnej zawartości dla slotu:

template
<SubmitButton />

Zostanie wyrenderowana zawartość domyślna, "Prześlij":

html
<button type="submit">Prześlij</button>

Ale jeśli dostarczymy zawartość:

template
<SubmitButton>Zapisz</SubmitButton>

Wtedy zostanie wyrenderowana dostarczona zawartość:

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

Sloty nazwane ​

Czasami przydatne jest posiadanie wielu wyjść slotów w pojedynczym komponencie. Na przykład, w komponencie <BaseLayout> z następującym szablonem:

template
<div class="container">
  <header>
    <!-- W tym miejscu chcemy mieć zawartość dla nagłówka -->
  </header>
  <main>
    <!-- W tym miejscu chcemy mieć zawartość główną -->
  </main>
  <footer>
    <!-- W tym miejscu chcemy mieć zawartość dla stopki -->
  </footer>
</div>

W takich przypadkach, element <slot> posiada specjalny atrybut name, który może być użyty do przypisania unikalnego identyfikatora do różnych slotów, dzięki czemu możesz określić, gdzie zawartość powinna być wyrenderowana:

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

Wyjście <slot> bez atrybutu name niejawnie posiada nazwę "default".

W komponencie nadrzędnym używającym <BaseLayout>, potrzebujemy sposobu na przekazanie wielu fragmentów zawartości slotów, z których każdy kierowany jest do innego wyjścia slotu. W tym miejscu pojawiają się nazwane sloty.

Aby przekazać nazwany slot, musimy użyć elementu <template> z dyrektywą v-slot, a następnie przekazać nazwę slotu jako argument do v-slot:

template
<BaseLayout>
  <template v-slot:header>
    <!-- zawartość dla slota nagłówka -->
  </template>
</BaseLayout>

v-slot posiada dedykowany skrót #, więc <template v-slot:header> może zostać skrócone do <template #header>. Możesz o tym myśleć jako "wyrenderuj ten fragment szablonu w slocie 'header' komponentu potomnego".

diagram nazwanych slotów

Oto kod przekazujący zawartość do wszystkich trzech slotów do <BaseLayout> przy użyciu składni skróconej:

template
<BaseLayout>
  <template #header>
    <h1>W tym miejscu może być tytuł strony</h1>
  </template>

  <template #default>
    <p>Paragraf głównej zawartości.</p>
    <p>I kolejny.</p>
  </template>

  <template #footer>
    <p>Nieco danych kontaktowych</p>
  </template>
</BaseLayout>

Kiedy komponent akceptuje zarówno domyślny slot jak i nazwane sloty, wszystkie węzły najwyższego poziomu nie będące elementami <template> są niejawnie traktowane jako zawartość domyślnego slotu. Tak więc powyższy kod może zostać zapisany również jako:

template
<BaseLayout>
  <template #header>
    <h1>W tym miejscu może być tytuł strony</h1>
  </template>

  <!-- niejawny slot domyślny -->
  <p>Paragraf głównej zawartości.</p>
  <p>I kolejny.</p>

  <template #footer>
    <p>Nieco danych kontaktowych</p>
  </template>
</BaseLayout>

Teraz wszystko wewnątrz elementów <template> zostanie przekazane do odpowiadających im slotów. Końcowy wyrenderowany HTML będzie:

html
<div class="container">
  <header>
    <h1>W tym miejscu może być tytuł strony</h1>
  </header>
  <main>
    <p>Paragraf głównej zawartości.</p>
    <p>I kolejny.</p>
  </main>
  <footer>
    <p>Nieco danych kontaktowych</p>
  </footer>
</div>

Ponownie, zrozumienie nazwanych slotów może być łatwiejsze przy użyciu analogii do funkcji JavaScript:

js
// przekazywanie wielu fragmentów slotów z różnymi nazwami
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renderuje je w różnych miejscach
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

Warunkowe Sloty ​

Czasami chcesz wyrenderować coś w zależności od tego, czy treść została przekazana do slotu.

Możesz użyć właściwości $slots w połączeniu z v-if, aby to osiągnąć.

W poniższym przykładzie definiujemy komponent Card z trzema warunkowymi slotami: header, footer i domyślnym default. Gdy obecna jest treść dla nagłówka / stopki / domyślnej zawartości, chcemy ją opakować, aby zapewnić dodatkowy styl:

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

    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>

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

Wypróbuj na Playground

Dynamiczne nazwy slotów ​

Dynamiczne argumenty dyrektyw działają również na v-slot, umożliwiając definiowanie dynamicznych nazw slotów:

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

  <!-- ze skróconą składnią -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Należy pamiętać, że wyrażenie podlega ograniczeniom składni dynamicznych argumentów dyrektyw.

Sloty z zakresem ​

Jak omówiono w Zakresie Renderowania, zawartość slotu nie ma dostępu do stanu w komponencie potomnym.

Jednak są przypadki, w których przydatne byłoby, gdyby zawartość slotu mogła wykorzystywać dane zarówno z zakresu rodzica, jak i zakresu potomka. Aby to osiągnąć, potrzebujemy sposobu, w który komponent potomny może przekazać dane do slotu podczas jego renderowania.

W rzeczywistości możemy zrobić dokładnie to - możemy przekazać atrybuty do wyjścia slotu, podobnie jak przekazujemy propsy do komponentu:

template
<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

Odbieranie propsów slotu wygląda nieco inaczej w przypadku używania pojedynczego domyślnego slotu w porównaniu do używania nazwanych slotów. Najpierw pokażemy, jak odbierać propsy przy użyciu pojedynczego domyślnego slotu, używając v-slot bezpośrednio na tagu komponentu potomnego:

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

scoped slots diagram

Właściwości (props) przekazane do slotu przez komponent potomny są dostępne jako wartość odpowiadającej dyrektywy v-slot, do której można uzyskać dostęp poprzez wyrażenia wewnątrz slotu.

Możesz myśleć o slotach z zasięgiem (scoped slot) jak o funkcji przekazywanej do komponentu potomnego. Komponent potomny następnie ją wywołuje, przekazując właściwości (props) jako argumenty:

js
MyComponent({
  // przekazując slot domyślny, ale jako funkcję
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return `<div>${
    // wywołaj funkcję slotu z właściwościami (props)!
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

W rzeczywistości, jest to bardzo zbliżone do tego, jak kompilowane są sloty z zasięgiem (scoped slots) i jak używałbyś ich w ręcznych funkcjach renderujących.

Zauważ, jak v-slot="slotProps" odpowiada sygnaturze funkcji slotu. Podobnie jak w przypadku argumentów funkcji, możemy użyć destrukturyzacji w v-slot:

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

Nazwane Sloty z Zasięgiem ​

Nazwane sloty z zasięgiem (named scoped slots) działają podobnie - właściwości slotu (slot props) są dostępne jako wartość dyrektywy v-slot: v-slot:name="slotProps". Używając skróconej składni, wygląda to tak:

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

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

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

Przekazywanie właściwości (props) do nazwanego slotu:

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

Zauważ, że name slotu nie zostanie dołączone do właściwości (props), ponieważ jest to nazwa zarezerwowana - w rezultacie headerProps będzie miało wartość { message: 'hello' }.

Jeśli łączysz nazwane sloty ze slotem domyślnym z zasięgiem (default scoped slot), musisz użyć jawnego tagu <template> dla slotu domyślnego. Próba umieszczenia dyrektywy v-slot bezpośrednio na komponencie spowoduje błąd kompilacji. Ma to na celu uniknięcie niejednoznaczności dotyczącej zasięgu właściwości (props) slotu domyślnego. Na przykład:

template
<!-- <MyComponent> template -->
<div>
  <slot :message="hello"></slot>
  <slot name="footer" />
</div>
template
<!-- Ten szablon się nie skompiluje -->
<MyComponent v-slot="{ message }">
  <p>{{ message }}</p>
  <template #footer>
    <!-- wiadomość należy do slotu domyślnego i nie jest tutaj dostępna -->
    <p>{{ message }}</p>
  </template>
</MyComponent>

Użycie jawnego tagu <template> dla slotu domyślnego pomaga wyraźnie pokazać, że właściwość (prop) message nie jest dostępna wewnątrz innego slotu:

template
<MyComponent>
 <!-- Użyj jawnego slotu domyślnego -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Tutaj znajdują się informacje kontaktowe</p>
  </template>
</MyComponent>

Przykład zaawansowanej listy ​

Możesz się zastanawiać, jaki byłby dobry przypadek użycia slotów z zasięgiem (scoped slots). Oto przykład: wyobraź sobie komponent <FancyList>, który renderuje listę elementów - może on enkapsulować logikę ładowania danych zdalnych, wykorzystywania danych do wyświetlania listy, a nawet zaawansowanych funkcji, takich jak paginacja czy nieskończone przewijanie. Jednak chcemy, aby był elastyczny w kwestii wyglądu każdego elementu i pozostawiamy stylizację poszczególnych elementów komponentowi nadrzędnemu, który z niego korzysta. Więc pożądane użycie może wyglądać tak:

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

Wewnątrz <FancyList> możemy renderować ten sam <slot> wielokrotnie z różnymi danymi elementów (zauważ, że używamy v-bind do przekazania obiektu jako właściwości (props) slotu):

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

Komponenty Bezrenderujące ​

Omawiany wyżej przypadek użycia <FancyList> enkapsuluje zarówno wielokrotnie używalną logikę (pobieranie danych, paginację itp.), jak i wyjście wizualne, jednocześnie delegując część wyjścia wizualnego do komponentu konsumującego poprzez sloty z zasięgiem (scoped slots).

Jeśli rozwiniemy tę koncepcję nieco dalej, możemy stworzyć komponenty, które enkapsulują tylko logikę i same nie renderują niczego - wyjście wizualne jest w całości delegowane do komponentu konsumującego za pomocą slotów z zasięgiem. Ten typ komponentu nazywamy Komponentem Bezrenderującym.

Przykładem komponentu bezrenderującego może być taki, który enkapsuluje logikę śledzenia aktualnej pozycji myszy:

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

Chociaż jest to interesujący wzorzec, większość tego, co można osiągnąć przy pomocy Komponentów Bezrenderujących, można zrealizować w bardziej wydajny sposób za pomocą Composition API, bez ponoszenia narzutu dodatkowego zagnieżdżania komponentów. Później zobaczymy, jak możemy zaimplementować tę samą funkcjonalność śledzenia myszy jako Composable.

Niemniej jednak, sloty z zasięgiem (scoped slots) są nadal przydatne w przypadkach, gdy potrzebujemy zarówno enkapsulacji logiki, jak i kompozycji wyjścia wizualnego, jak w przykładzie <FancyList>.

SlotyJest załadowany