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