Usando Slots no Vue.js
Publicados: 2022-03-10Com o recente lançamento do Vue 2.6, a sintaxe para usar slots ficou mais sucinta. Essa mudança nos slots fez com que eu me interessasse novamente em descobrir o poder potencial dos slots para fornecer reutilização, novos recursos e legibilidade mais clara para nossos projetos baseados em Vue. Do que os slots são realmente capazes?
Se você é novo no Vue ou não viu as mudanças da versão 2.6, continue lendo. Provavelmente o melhor recurso para aprender sobre slots é a própria documentação do Vue, mas vou tentar fazer um resumo aqui.
O que são slots?
Slots são um mecanismo para componentes Vue que permite que você componha seus componentes de uma maneira diferente do relacionamento pai-filho estrito. Os slots oferecem uma saída para colocar conteúdo em novos locais ou tornar os componentes mais genéricos. A melhor maneira de entendê-los é vê-los em ação. Vamos começar com um exemplo simples:
// frame.vue <template> <div class="frame"> <slot></slot> </div> </template>
Este componente tem um wrapper div
. Vamos fingir que div
existe para criar um quadro estilístico em torno de seu conteúdo. Este componente pode ser usado genericamente para envolver um quadro em qualquer conteúdo que você desejar. Vamos ver como é usá-lo. O componente frame
aqui se refere ao componente que acabamos de fazer acima.
// app.vue <template> <frame><img src="an-image.jpg"></frame> </template>
O conteúdo que estiver entre as tags frame
de abertura e fechamento será inserido no componente frame
onde está o slot
, substituindo as tags de slot
. Essa é a maneira mais básica de fazer isso. Você também pode especificar o conteúdo padrão para entrar em um slot simplesmente preenchendo-o:
// frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Então agora se usarmos assim:
// app.vue <template> <frame /> </template>
O texto padrão de “Este é o conteúdo padrão se nada for especificado para ir aqui” aparecerá, mas se o usarmos como fizemos antes, o texto padrão será substituído pela tag img
.
Slots Múltiplos/Nomeados
Você pode adicionar vários slots a um componente, mas se fizer isso, todos, exceto um deles, deverão ter um nome. Se houver um sem nome, é o slot padrão. Veja como você cria vários slots:
// titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Mantivemos o mesmo slot padrão, mas desta vez adicionamos um slot chamado header
onde você pode inserir um título. Você usa assim:
// app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>
Assim como antes, se quisermos adicionar conteúdo ao slot padrão, basta colocá-lo diretamente dentro do componente titled-frame
. Para adicionar conteúdo a um slot nomeado, porém, precisávamos envolver o código em uma tag de template
com uma diretiva v-slot
. Você adiciona dois pontos ( :
) após v-slot
e, em seguida, escreve o nome do slot para o qual deseja que o conteúdo seja passado. Observe que o v-slot
é novo no Vue 2.6, portanto, se você estiver usando uma versão mais antiga, precisará ler os documentos sobre a sintaxe de slot obsoleta.
Slots com escopo
Mais uma coisa que você precisa saber é que os slots podem passar dados/funções para seus filhos. Para demonstrar isso, precisaremos de um componente de exemplo completamente diferente com slots, um que seja ainda mais artificial que o anterior: vamos copiar o exemplo dos documentos criando um componente que fornece os dados sobre o usuário atual para seus slots:
// current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>
Este componente possui uma propriedade chamada user
com detalhes sobre o usuário. Por padrão, o componente mostra o sobrenome do usuário, mas observe que está usando v-bind
para vincular os dados do usuário ao slot. Com isso, podemos usar este componente para fornecer os dados do usuário ao seu descendente:
// app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>
Para obter acesso aos dados passados para o slot, especificamos o nome da variável de escopo com o valor da diretiva v-slot
.
Há algumas notas a serem feitas aqui:
- Especificamos o nome de
default
, embora não seja necessário para o slot padrão. Em vez disso, poderíamos usar apenasv-slot="slotProps"
. - Você não precisa usar
slotProps
como o nome. Você pode chamá-lo do que quiser. - Se você estiver usando apenas um slot padrão, poderá pular essa tag de
template
interna e colocar a diretivav-slot
diretamente na tagcurrent-user
. - Você pode usar a desestruturação de objetos para criar referências diretas aos dados do slot com escopo definido em vez de usar um único nome de variável. Em outras palavras, você pode usar
v-slot="{user}"
ao invés dev-slot="slotProps"
e então você pode usaruser
diretamente ao invés deslotProps.user
.
Levando essas notas em consideração, o exemplo acima pode ser reescrito assim:
// app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>
Mais algumas coisas para manter em mente:
- Você pode vincular mais de um valor com diretivas
v-bind
. Então, no exemplo, eu poderia ter feito mais do que apenasuser
. - Você também pode passar funções para slots com escopo. Muitas bibliotecas usam isso para fornecer componentes funcionais reutilizáveis, como você verá mais adiante.
-
v-slot
tem um alias de#
. Então, em vez de escreverv-slot:header="data"
, você pode escrever#header="data"
. Você também pode especificar#header
em vez dev-slot:header
quando não estiver usando slots com escopo definido. Quanto aos slots padrão, você precisará especificar o nome dodefault
ao usar o alias. Em outras palavras, você precisará escrever#default="data"
em vez de#="data"
.
Há mais alguns pontos menores que você pode aprender nos documentos, mas isso deve ser suficiente para ajudá-lo a entender do que estamos falando no restante deste artigo.
O que você pode fazer com slots?
Os slots não foram construídos para um único propósito, ou pelo menos se fossem, eles evoluíram muito além da intenção original de ser uma ferramenta poderosa para fazer muitas coisas diferentes.
Padrões Reutilizáveis
Os componentes sempre foram projetados para serem reutilizados, mas alguns padrões não são práticos para aplicar com um único componente “normal” porque o número de props
necessários para personalizá-lo pode ser excessivo ou você precisa passe grandes seções de conteúdo e potencialmente outros componentes através dos props
. Slots podem ser usados para abranger a parte "externa" do padrão e permitir que outros HTML e/ou componentes sejam colocados dentro deles para personalizar a parte "interior", permitindo que o componente com slots defina o padrão e os componentes injetados no slots para ser único.
Para nosso primeiro exemplo, vamos começar com algo simples: um botão. Imagine que você e sua equipe estão usando o Bootstrap*. Com o Bootstrap, seus botões geralmente são amarrados com a classe base `btn` e uma classe que especifica a cor, como `btn-primary`. Você também pode adicionar uma classe de tamanho, como `btn-lg`.
* Eu não encorajo nem desencorajo você a fazer isso, eu só precisava de algo para o meu exemplo e é bastante conhecido.
Vamos agora supor, para simplificar, que seu aplicativo/site sempre usa btn-primary
e btn-lg
. Você não quer sempre ter que escrever todas as três classes em seus botões, ou talvez você não confie em um novato para lembrar de fazer todas as três. Nesse caso, você pode criar um componente que tenha automaticamente todas essas três classes, mas como você permite a personalização do conteúdo? Um prop
não é prático porque uma tag de button
pode ter todos os tipos de HTML nela, então devemos usar um slot.
<!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>
Agora podemos usá-lo em qualquer lugar com o conteúdo que você quiser:

<!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>
Claro, você pode usar algo muito maior do que um botão. Continuando com o Bootstrap, vamos ver um modal, ou pelo menos a parte HTML; Eu não vou entrar na funcionalidade... ainda.
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>
Agora, vamos usar isso:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>
O tipo de caso de uso para slots acima é obviamente muito útil, mas pode fazer ainda mais.
Reutilizando Funcionalidade
Os componentes Vue não são apenas sobre HTML e CSS. Eles são construídos com JavaScript, então eles também são sobre funcionalidade. Os slots podem ser úteis para criar funcionalidades uma vez e usá-las em vários lugares. Vamos voltar ao nosso exemplo modal e adicionar uma função que fecha o modal:
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>
Agora, ao usar este componente, você pode adicionar um botão ao rodapé que pode fechar o modal. Normalmente, no caso de um modal do Bootstrap, você pode simplesmente adicionar data-dismiss="modal"
a um botão, mas queremos ocultar coisas específicas do Bootstrap dos componentes que serão encaixados nesse componente modal. Então, passamos a eles uma função que eles podem chamar e eles não sabem sobre o envolvimento do Bootstrap:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>
Componentes sem renderização
E, finalmente, você pode pegar o que sabe sobre o uso de slots para distribuir funcionalidades reutilizáveis e remover praticamente todo o HTML e usar apenas os slots. Isso é essencialmente o que é um componente sem renderização: um componente que fornece apenas funcionalidade sem qualquer HTML.
Tornar os componentes realmente sem renderização pode ser um pouco complicado porque você precisará escrever funções de render
em vez de usar um modelo para remover a necessidade de um elemento raiz, mas nem sempre isso será necessário. Vamos dar uma olhada em um exemplo simples que nos permite usar um modelo primeiro:
<template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
Este é um exemplo estranho de um componente sem renderização porque nem sequer tem JavaScript nele. Isso ocorre principalmente porque estamos apenas criando uma versão reutilizável pré-configurada de uma função interna sem renderização: transition
.
Sim, o Vue possui componentes sem renderização integrados. Este exemplo em particular foi retirado de um artigo sobre transições reutilizáveis de Cristi Jora e mostra uma maneira simples de criar um componente sem renderização que pode padronizar as transições usadas em todo o seu aplicativo. O artigo de Cristi se aprofunda muito e mostra algumas variações mais avançadas de transições reutilizáveis, então eu recomendo dar uma olhada.
Para nosso outro exemplo, criaremos um componente que lida com a alternância do que é mostrado durante os diferentes estados de uma promessa: pendente, resolvido com êxito e com falha. É um padrão comum e, embora não exija muito código, pode atrapalhar muitos dos seus componentes se a lógica não for retirada para reutilização.
<!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>
Então o que está acontecendo aqui? Primeiro, observe que estamos recebendo uma prop chamada promise
que é uma Promise
. Na seção watch
, observamos as alterações na promessa e quando ela muda (ou imediatamente na criação do componente, graças à propriedade immediate
), limpamos o estado, chamamos then
e catch
a promessa, atualizando o estado quando ela termina com êxito ou falha.
Então, no modelo, mostramos um slot diferente com base no estado. Observe que falhamos em mantê-lo realmente sem renderização porque precisávamos de um elemento raiz para usar um modelo. Também estamos passando data
e error
para os escopos de slot relevantes.
E aqui está um exemplo de uso:
<template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...
Passamos somePromise
ao componente sem renderização. Enquanto esperamos que termine, estamos exibindo “Trabalhando nisso…” graças ao slot pending
. Se for bem-sucedido, exibimos “Resolvido:” e o valor da resolução. Se falhar, exibimos “Rejeitado:” e o erro que causou a rejeição. Agora não precisamos mais rastrear o estado da promessa nesse componente porque essa parte é extraída em seu próprio componente reutilizável.
Então, o que podemos fazer sobre esse span
envolvendo os slots no promised.vue
? Para removê-lo, precisaremos remover a parte do template
e adicionar uma função de render
ao nosso componente:
render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }
Não há nada muito complicado acontecendo aqui. Estamos apenas usando alguns blocos if
para encontrar o estado e, em seguida, retornar o slot com escopo correto (via this.$scopedSlots['SLOTNAME'](...)
) e passar os dados relevantes para o escopo do slot. Quando não estiver usando um modelo, você pode pular o uso da extensão de arquivo .vue
retirando o JavaScript da tag de script
e simplesmente colocando-o em um arquivo .js
. Isso deve dar a você um pequeno aumento de desempenho ao compilar esses arquivos Vue.
Este exemplo é uma versão simplificada e levemente ajustada do vue-promised, que eu recomendaria usar o exemplo acima porque cobre algumas armadilhas em potencial. Existem muitos outros ótimos exemplos de componentes sem renderização por aí também. Baleada é uma biblioteca inteira cheia de componentes sem renderização que fornecem funcionalidades úteis como esta. Há também o vue-virtual-scroller para controlar a renderização do item da lista com base no que é visível na tela ou o PortalVue para “teleportar” conteúdo para partes completamente diferentes do DOM.
Estou fora
Os slots do Vue levam o desenvolvimento baseado em componentes a um nível totalmente novo e, embora eu tenha demonstrado muitas maneiras excelentes de usar os slots, existem inúmeras outras por aí. Que grande ideia você pode pensar? De que maneiras você acha que os slots poderiam ser atualizados? Se você tiver alguma, certifique-se de trazer suas ideias para a equipe Vue. Deus abençoe e feliz codificação.