Ranhuras
Neste material assume-se conhecimento dos Fundamentos dos Componentes. Precisamos entender o básico de componentes para acompanhar este material.
Conteúdo da Ranhura e a Saída de Ranhura
Nós temos aprendido que os componentes podem aceitar propriedades, as quais podem ser valores de JavaScript de qualquer tipo. Mas quanto ao conteúdo do modelo de marcação? Em alguns casos, podemos desejar passar um fragmento de modelo de marcação para um componente filho, e permitir que o componente filho interprete o fragmento dentro do seu próprio modelo de marcação.
Por exemplo, podemos ter um componente <FancyButton>
que suporta este tratamento:
template
<FancyButton>
Click me! <!-- conteúdo da ranhura -->
</FancyButton>
O modelo de marcação do <FancyButton>
se parece com isto:
template
<button class="fancy-btn">
<slot></slot> <!-- saída de ranhura -->
</button>
O elemento <slot>
é uma saída de ranhura que indica onde o conteúdo de ranhura fornecido pelo componente deve ser interpretado.
E o DOM interpretado final:
html
<button class="fancy-btn">Click me!</button>
Com as ranhuras, a <FancyButton>
é responsável pela interpretação do <button>
externo (e sua estilização fantástica), enquanto o conteúdo interno é fornecido pelo componente pai.
Um outro maneira de entender as ranhuras é comparando-os as funções de JavaScript:
js
// componente pai passando o conteúdo de ranhura
FancyButton('Click me!')
// "FancyButton" interpreta o conteúdo de ranhura no seu...
// próprio modelo de marcação
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}
O conteúdo de ranhura não é apenas limitado ao texto. Ele pode ser qualquer conteúdo de modelo de marcação válido. Por exemplo, podemos passar vários elementos, ou mesmo outros componentes:
template
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
Ao utilizar as ranhuras, o nosso <FancyButton>
é mais flexível e reutilizável. Nós podemos agora utilizá-lo em diferentes lugares com conteúdo interno diferente, mas todos com a mesma estilização fantástica.
O mecanismo de ranhura dos componentes de Vue são inspirados pelo elemento <slot>
de Componente de Web nativo, mas com capacidades adicionais que veremos mais tarde.
Escopo de Interpretação
O conteúdo de ranhura tem acesso ao escopo de dados do componente pai, porque é definido no componente pai. Por exemplo:
template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
Aqui ambos interpolações de {{ message }}
interpretarão o mesmo conteúdo.
O conteúdo de ranhura não tem acesso aos dados do componente filho. As expressões nos modelos de marcação de Vue podem apenas acessar o escopo em que são definidas, consistente com a possibilidade léxica da JavaScript. Em outras palavras:
As expressões no modelo de marcação do componente pai apenas têm acesso ao escopo do componente pai; as expressões no modelo de marcação do componente filho apenas têm acesso ao escopo do componente filho.
Conteúdo Retrocessivo
Existem casos onde é útil especificar conteúdo retrocessivo (por exemplo, padrão) para uma ranhura, para ser interpretado apenas quando nenhum conteúdo for fornecido. Por exemplo, em um componente <SubmitButton>
:
template
<button type="submit">
<slot></slot>
</button>
Nós podemos desejar que o texto "Submit" seja interpretado dentro do <button>
se o componente pai não forneceu qualquer conteúdo de ranhura. Para tornar o "Submit" o conteúdo retrocessivo, podemos colocá-lo entre os marcadores <slot>
:
template
<button type="submit">
<slot>
Submit <!-- conteúdo retrocessivo -->
</slot>
</button>
Agora quando utilizarmos <SubmitButton>
em componente pai, sem nenhum conteúdo sendo fornecido para a ranhura:
template
<SubmitButton />
Isto interpretará o conteúdo retrocessivo, "Submit":
html
<button type="submit">Submit</button>
Mas se fornecermos o conteúdo:
template
<SubmitButton>Save</SubmitButton>
Então o conteúdo fornecido será interpretado no lugar:
html
<button type="submit">Save</button>
Ranhuras Nomeadas
Existem momentos quando é útil ter várias saídas de ranhura em um único componente. Por exemplo, em um componente <BaseLayout>
com o seguinte modelo de marcação:
template
<div class="container">
<header>
<!-- Nós queremos o conteúdo de cabelo aqui -->
</header>
<main>
<!-- Nós queremos o conteúdo principal aqui -->
</main>
<footer>
<!-- Nós queremos o conteúdo de rodapé aqui -->
</footer>
</div>
Para estes casos, o elemento <slot>
tem um atributo especial, name
, o qual pode ser utilizado para atribuir um identificador único para ranhuras diferentes assim podes determinar onde o conteúdo dever ser interpretado:
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
Uma saída de <slot>
sem name
implicitamente tem o nome de "default".
Em um componente pai utilizando <BaseLayout>
, precisamos de uma maneira de passar vários fragmentos de conteúdo de ranhura, cada mirando uma saída de ranhura diferente. Isto é onde ranhuras nomeadas entram.
Para passar uma ranhura nomeada, precisamos utilizar um elemento <template>
com a diretiva v-slot
, e depois passar o nome da ranhura como um argumento para v-slot
:
template
<BaseLayout>
<template v-slot:header>
<!-- conteúdo para a ranhura de cabeçalho -->
</template>
</BaseLayout>
A v-slot
tem um abreviação dedicada #
, assim <template v-slot:header>
pode ser abreviada para apenas <template #header>
. Pense disto como "interpretar este fragmento de modelo de marcação na ranhura 'header' do componente filho".
Aqui está o código passando conteúdo para todas as três ranhuras para <BaseLayout>
utilizando a sintaxe abreviada:
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>
Quando um componente aceita ambos uma ranhura padrão e ranhuras nomeadas, todos os nós de alto nível que não são <template>
são implicitamente tratados como conteúdo para a ranhura padrão. Assim o exemplo acima também pode ser escrito como:
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- ranhura padrão implícita -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
Agora tudo dentro dos elementos <template>
será passado às ranhuras correspondente. O resultado de HTML interpretado será:
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>
Além disso, isto pode ajudar-te a entender as ranhuras nomeadas melhor utilizando a analogia de função de JavaScript:
js
// passando vários fragmentos de ranhura com diferentes nomes
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// "<BaseLayout>" interpreta-os em diferentes lugares
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
Nomes de Ranhura Dinâmicos
Os argumentos de diretiva dinâmicos também funcionam sobre a v-slot
, permitindo a definição de nomes de ranhura dinâmicos:
template
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- com a forma abreviada -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
Toma nota que a expressão está sujeita às restrições de sintaxe dos argumentos de diretiva dinâmicos.
Ranhuras Isoladas
Conforme discutido no Escopo de Interpretação, o conteúdo de ranhura não tem acesso ao estado no componente filho.
No entanto, existem casos onde poderia ser útil se um conteúdo da ranhura poder fazer uso dos dados de ambos escopo do componente pai e escopo do componente filho. Para alcançar isto, precisamos de uma maneira para o componente filho passar dados para uma ranhura quando estiver interpretando-o.
De fato, podemos fazer exatamente isto - nós podemos passar atributos para uma ranhura de saída da mesma maneira que passamos propriedades para um componente.
template
<!-- modelo de marcação do <MyComponent> -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
O recebimento das propriedades de ranhura é um pouco diferente de quando estamos utilizando uma única ranhura padrão versus a utilização de ranhuras nomeadas. Nós mostraremos como receber as propriedades utilizando uma única ranhura padrão primeiro, utilizando v-slot
diretamente no marcador do componente filho:
template
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
As propriedades passadas para a ranhura pelo componente filho estão disponíveis como valor da diretiva v-slot
correspondente, a qual pode ser acessada pelas expressões dentro da ranhura.
Tu podes pensar em uma ranhura isolada como uma função sendo passada para o componente filho. O componente filho então chama-a, passando propriedades como argumentos:
js
MyComponent({
// passando a ranhura padrão, mas como uma função
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// chamar a função da ranhura com as propriedades!
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
De fato, isto está muito próximo de como as ranhuras isoladas são compiladas, e de como usaríamos as ranhuras isoladas no manual de funções de interpretação.
Repare que as v-slot="slotProps"
correspondem a assinatura da função de ranhura. Tal como com os argumentos de função, podemos utilizar a desestruturação na v-slot
:
template
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
Ranhuras Isoladas Nomeadas
As ranhuras isoladas nomeadas funcionam de maneira semelhante - propriedades de ranhura são acessíveis como valor da diretiva v-slot
: v-slot:name="slotProps"
. Quando estiveres utilizando a forma abreviada, se parece com isto:
template
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
Passando as propriedades para uma ranhura nomeada:
template
<slot name="header" message="hello"></slot>
Nota que o name
de uma ranhura não será incluído nas propriedades porque está reservado - então o headerProps
resultante seria { message: 'hello' }
.
Se estiveres misturando as ranhuras nomeadas com a ranhura isolada padrão, precisas utilizar um marcador <template>
explícito para a ranhura padrão. A tentativa de colocar a diretiva v-slot
diretamente sobre o componente resultará em um erro de compilação. Isto é para evitar qualquer ambiguidade a respeito do escopo das propriedades da ranhura padrão. Por exemplo:
template
<!-- Este modelo de marcação não compilará -->
<template>
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- "message" pertence à ranhura padrão, e não está disponível aqui -->
<p>{{ message }}</p>
</template>
</MyComponent>
</template>
A utilização de um marcador <template>
explícito para a ranhura padrão ajuda a tornar claro de que a propriedade message
não está disponível dentro de outra ranhura:
template
<template>
<MyComponent>
<!-- Utilize ranhura padrão explícita -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
</template>
Exemplo de Lista Fantástica
Tu podes estar perguntando a si mesmo o que seria um bom caso de uso para ranhuras isoladas. Aqui está um exemplo: imagine um componente <FancyList>
que interpreta uma lista de itens - ele pode resumir a lógica para o carregamento de dados remoto, utilizando os dados para exibir uma lista, ou mesmo funcionalidades avançadas como paginação ou rolamento infinito. No entanto, queremos ser flexíveis a respeito da aparência de cada item e deixar a estilização de cada item para componente pai que está consumindo-o. Assim a utilização desejada se parece com isto:
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>
Dentro do <FancyList>
, podemos interpretar o mesmo <slot>
várias vezes com diferentes dados de item (repara que estamos utilizando a v-bind
para passar um objeto como propriedades de ranhura):
template
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
Componentes Sem Interpretação
O caso de uso de <FancyList>
que discutimos acima resume ambas a lógica reutilizável (requisição de dados, paginação etc) e a saída visual, enquanto está delegando parte da saída visual para o componente consumidor através das ranhuras isoladas.
Se empurrarmos este conceito um pouco adiante, podemos surgir com componentes que apenas resumem a lógica e não interpretam nada por si mesmos - a saída visual é completamente delegada ao componente consumidor com as ranhuras isoladas. Nós chamamos este tipo de componente um Componentes Sem Interpretação.
Um exemplo de componente sem interpretação poderia ser um que resume a lógica do rastreio da posição atual do rato:
template
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
Embora um padrão interessante, a maioria do que pode ser alcançada com os Componentes Sem Interpretação pode ser alcançada de uma maneira mais eficiente com a API de Composição, sem ficar sujeito a despesas gerais do encaixamento de componente adicionais. Depois, veremos como podemos implementar a mesma funcionalidade de rastreio do rato com uma Função de Composição.
Com isto dito, ranhuras isoladas ainda são úteis nos casos onde precisamos de ambos resumir a lógica e compor a saída visual, tal como no exemplo de <FancyList>
.