Skip to content

Komponenty Podstawy

Komponenty umożliwiają podzielenie interfejsu użytkownika na niezależne części, które można ponownie wykorzystać, i myślenie o każdej z nich osobno. Często zdarza się, że aplikacja jest zorganizowana w drzewo zagnieżdżonych komponentów:

Drzewo Komponentów

Jest to bardzo podobne do tego, jak zagnieżdżamy natywne elementy HTML, ale Vue implementuje swój własny model komponentu, który pozwala nam hermetyzować własną zawartość i logikę w każdym komponencie. Vue dobrze współpracuje także z natywnymi komponentami sieciowymi. Jeśli jesteś ciekaw relacji między komponentami Vue a natywnymi komponentami sieciowymi, przeczytaj więcej tutaj.

Definiowanie Komponentu

Kiedy używamy kroku budowania, zazwyczaj definiujemy każdy komponent Vue w dedykowanym pliku z rozszerzeniem .vue - znanym jako Single-File Component (w skrócie SFC):

vue
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">Kliknąłeś mnie {{ count }} razy.</button>
</template>
vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">Kliknąłeś mnie {{ count }} razy.</button>
</template>

Gdy nie używa się kroku budowania, komponent Vue można zdefiniować jako zwykły obiekt JavaScript zawierający opcje specyficzne dla Vue:

js
export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
}
js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
  // or `template: '#my-template-element'`
}

Szablon jest tutaj wklejany jako kod JavaScript, który Vue skompiluje w locie. Można też użyć selektora ID wskazującego na element (zwykle rodzimy element <template>) - Vue użyje jego zawartości jako źródła szablonu.

W powyższym przykładzie zdefiniowano pojedynczy komponent i wyeksportowano go jako domyślny eksport pliku .js, ale można użyć nazwanych eksportów, aby wyeksportować wiele komponentów z tego samego pliku.

Użycie komponentu

TIP

W dalszej części tego podręcznika będziemy używać składni SFC - koncepcje dotyczące komponentów są takie same niezależnie od tego, czy używamy kroku budowania, czy nie. Sekcja Przykłady pokazuje użycie komponentów w obu scenariuszach.

Aby użyć komponentu potomnego, musimy zaimportować go w komponencie nadrzędnym. Zakładając, że umieściliśmy nasz komponent licznika wewnątrz pliku o nazwie ButtonCounter.vue, komponent ten będzie widoczny jako domyślny eksport tego pliku:

vue
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Tu jest komponent dziecko - podrzędny!</h1>
  <ButtonCounter />
</template>

Aby udostępnić zaimportowany komponent naszemu szablonowi, musimy go zarejestrować za pomocą opcji components. Komponent będzie wtedy dostępny jako znacznik używający klucza, pod którym został zarejestrowany.

vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Tu jest komponent dziecko - podrzędny!</h1>
  <ButtonCounter />
</template>

Dzięki <script setup> importowane komponenty są automatycznie udostępniane szablonowi.

Możliwe jest również globalne zarejestrowanie komponentu, dzięki czemu będzie on dostępny dla wszystkich komponentów w danej aplikacji bez konieczności jego importowania. Zalety i wady rejestracji globalnej i lokalnej są omówione w specjalnej sekcji Rejestracja komponentów.

Komponenty mogą być używane dowolną liczbę razy:

template
<h1>Tu jest komponent dziecko - podrzędny!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

Zauważ, że podczas klikania na przyciski każdy z nich ma swoją własną, oddzielną liczbę. Dzieje się tak dlatego, że za każdym razem, gdy używasz komponentu, tworzona jest jego nowa instancja.

W SFC zaleca się używanie nazw znaczników PascalCase dla komponentów potomnych, aby odróżnić je od rodzimych elementów HTML. Chociaż w natywnych nazwach znaczników HTML nie jest rozróżniana wielkość liter, Vue SFC jest formatem skompilowanym, więc możemy w nim używać nazw znaczników rozróżniających wielkość liter. Możemy również używać /> do zamykania znaczników.

Jeśli tworzysz swoje szablony bezpośrednio w DOM (np. jako zawartość natywnego elementu <template>), szablon będzie podlegał natywnemu parsowaniu HTML przez przeglądarkę. W takich przypadkach należy używać kebab-case oraz jawnych znaczników zamykających dla elementów:

template
<!-- if this template is written in the DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

See DOM template parsing caveats for more details.

Przekazywanie Props

Jeśli budujemy blog, prawdopodobnie będziemy potrzebować komponentu reprezentującego post na blogu. Chcemy, aby wszystkie posty na blogu miały ten sam układ wizualny, ale różną treść. Taki komponent nie będzie użyteczny, jeśli nie będziemy mogli przekazać do niego danych, takich jak tytuł i treść konkretnego wpisu, który chcemy wyświetlić. W tym miejscu z pomocą przychodzą props.

Props to niestandardowe atrybuty, które można zarejestrować w komponencie. Aby przekazać tytuł do naszego komponentu wpisu na blogu, musimy zadeklarować go na liście props akceptowanych przez ten komponent, używając polecenia props optiondefineProps macro:

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

Gdy wartość jest przekazywana do atrybutu prop, staje się ona właściwością instancji komponentu. Wartość tej właściwości jest dostępna w szablonie oraz w kontekście this komponentu, tak jak każda inna właściwość komponentu.

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps jest makrem czasu kompilacji, które jest dostępne tylko wewnątrz <script setup> i nie musi być jawnie importowane. Zadeklarowane props są automatycznie przekazywane do szablonu. defineProps zwraca również obiekt, który zawiera wszystkie rekwizyty przekazane do komponentu, tak abyśmy mogli mieć do nich dostęp w JavaScript, jeśli zajdzie taka potrzeba:

js
const props = defineProps(['title'])
console.log(props.title)

Zobacz także: Typowanie Component Props

Jeśli nie używasz <script setup>, props powinny być zadeklarowane przy użyciu opcji props, a obiekt rekwizytu zostanie przekazany do setup() jako pierwszy argument:

js
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

Komponent może mieć dowolną liczbę props, a domyślnie do każdego prop można przekazać dowolną wartość.

Po zarejestrowaniu prop można przekazywać do niego dane jako atrybut niestandardowy, tak jak w poniższym przykładzie:

template
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

W typowej aplikacji tablica postów znajduje się w komponencie nadrzędnym:

js
export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}
js
const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])

Następnie chcemy renderować komponent dla każdego z nich, używając v-for:

template
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

Zauważ, że możemy użyć v-bind do przekazania dynamicznych props. Jest to szczególnie przydatne, gdy nie znamy dokładnej zawartości, która ma być renderowana z wyprzedzeniem.

To wszystko, co powinieneś wiedzieć o props na razie, ale gdy skończysz czytać tę stronę i poczujesz się komfortowo z jej treścią, zalecamy wrócić później, aby przeczytać pełny przewodnik na temat Props.

Nasłuchiwanie Zdarzeń

W miarę rozwoju komponentu <BlogPost> niektóre funkcje mogą wymagać komunikacji z jego rodzicem. Na przykład, możemy zdecydować się na dołączenie funkcji dostępności, która powiększa tekst postów na blogu, pozostawiając resztę strony w domyślnym rozmiarze.

W elemencie nadrzędnym możemy obsłużyć tę funkcję, dodając element postFontSize data propertyref:

js
data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}
js
const posts = ref([
  /* ... */
])

const postFontSize = ref(1)

Którego można użyć w szablonie do kontrolowania rozmiaru czcionki wszystkich wpisów na blogu:

template
<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

Dodajmy teraz przycisk do szablonu komponentu <BlogPost>:

vue
<!-- BlogPost.vue, pomoniecie <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Powiększ tekst</button>
  </div>
</template>

Przycisk nie robi jeszcze nic - chcemy, aby jego kliknięcie zakomunikowało rodzicowi, że powinien on powiększyć tekst wszystkich postów. Aby rozwiązać ten problem, instancje komponentów udostępniają własny system zdarzeń. Rodzic może nasłuchiwać dowolnego zdarzenia na instancji komponentu-dziecka za pomocą v-on lub @, tak jak w przypadku natywnego zdarzenia DOM:

template
<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

Następnie komponent potomny może wywołać zdarzenie na sobie poprzez wywołanie wbudowanej metody $emit, przekazując nazwę zdarzenia:

vue
<!-- BlogPost.vue, pomijając <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Powiksz tekst</button>
  </div>
</template>

Dzięki listenerowi @enlarge-text="postFontSize += 0.1", rodzic otrzyma zdarzenie i zaktualizuje wartość postFontSize.

Opcjonalnie możemy zadeklarować emitowane zdarzenia za pomocą polecenia emitsdefineEmits:

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

Dokumentuje to wszystkie zdarzenia emitowane przez komponent i opcjonalnie waliduje je. Pozwala to także Vue uniknąć domyślnego stosowania ich jako natywnych nasłuchiwaczy do elementu głównego komponentu potomnego.

Podobnie jak defineProps, defineEmits jest również użyteczne tylko w <script setup> i nie wymaga importu. Zwraca ona funkcję emit, jest odpowiednikiem metody $emit.

vue
<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

Zobacz także: Typing Component Emits

Jeśli nie używasz <script setup>, możesz zadeklarować emitowane zdarzenia używając opcji emits. Możesz uzyskać dostęp do funkcji emit jako właściwości kontekstu setupu (przekazanego do setup() jako drugi argument):

js
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

To na razie wszystko, co musisz wiedzieć o zdarzeniach komponentów niestandardowych, ale po przeczytaniu tej strony i zapoznaniu się z jej treścią zalecamy powrót do pełnego przewodnika po Zdarzeniach niestandardowych.

Content Distribution with Slots

Just like with HTML elements, it's often useful to be able to pass content to a component, like this:

template
<AlertBox>
  Something bad happened.
</AlertBox>

Which might render something like:

This is an Error for Demo Purposes

Something bad happened.

This can be achieved using Vue's custom <slot> element:

vue
<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

As you'll see above, we use the <slot> as a placeholder where we want the content to go – and that's it. We're done!

That's all you need to know about slots for now, but once you've finished reading this page and feel comfortable with its content, we recommend coming back later to read the full guide on Slots.

Dynamic Components

Sometimes, it's useful to dynamically switch between components, like in a tabbed interface:

The above is made possible by Vue's <component> element with the special is attribute:

template
<!-- Component changes when currentTab changes -->
<component :is="currentTab"></component>
template
<!-- Component changes when currentTab changes -->
<component :is="tabs[currentTab]"></component>

In the example above, the value passed to :is can contain either:

  • the name string of a registered component, OR
  • the actual imported component object

You can also use the is attribute to create regular HTML elements.

When switching between multiple components with <component :is="...">, a component will be unmounted when it is switched away from. We can force the inactive components to stay "alive" with the built-in <KeepAlive> component.

DOM Template Parsing Caveats

If you are writing your Vue templates directly in the DOM, Vue will have to retrieve the template string from the DOM. This leads to some caveats due to browsers' native HTML parsing behavior.

TIP

It should be noted that the limitations discussed below only apply if you are writing your templates directly in the DOM. They do NOT apply if you are using string templates from the following sources:

  • Single-File Components
  • Inlined template strings (e.g. template: '...')
  • <script type="text/x-template">

Case Insensitivity

HTML tags and attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, PascalCase component names and camelCased prop names or v-on event names all need to use their kebab-cased (hyphen-delimited) equivalents:

js
// camelCase in JavaScript
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
template
<!-- kebab-case in HTML -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

Self Closing Tags

We have been using self-closing tags for components in previous code samples:

template
<MyComponent />

This is because Vue's template parser respects /> as an indication to end any tag, regardless of its type.

In DOM templates, however, we must always include explicit closing tags:

template
<my-component></my-component>

This is because the HTML spec only allows a few specific elements to omit closing tags, the most common being <input> and <img>. For all other elements, if you omit the closing tag, the native HTML parser will think you never terminated the opening tag. For example, the following snippet:

template
<my-component /> <!-- we intend to close the tag here... -->
<span>hello</span>

will be parsed as:

template
<my-component>
  <span>hello</span>
</my-component> <!-- but the browser will close it here. -->

Element Placement Restrictions

Some HTML elements, such as <ul>, <ol>, <table> and <select> have restrictions on what elements can appear inside them, and some elements such as <li>, <tr>, and <option> can only appear inside certain other elements.

This will lead to issues when using components with elements that have such restrictions. For example:

template
<table>
  <blog-post-row></blog-post-row>
</table>

The custom component <blog-post-row> will be hoisted out as invalid content, causing errors in the eventual rendered output. We can use the special is attribute as a workaround:

template
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

TIP

When used on native HTML elements, the value of is must be prefixed with vue: in order to be interpreted as a Vue component. This is required to avoid confusion with native customized built-in elements.

That's all you need to know about DOM template parsing caveats for now - and actually, the end of Vue's Essentials. Congratulations! There's still more to learn, but first, we recommend taking a break to play with Vue yourself - build something fun, or check out some of the Examples if you haven't already.

Once you feel comfortable with the knowledge you've just digested, move on with the guide to learn more about components in depth.

Komponenty Podstawy has loaded