Funções de Renderização & JSX

Introdução

Vue recomenda que templates sejam utilizados para construir seu HTML na grande maioria dos casos. Haverá situações, no entanto, em que você irá realmente precisar de todo o poder de programação do JavaScript. É quando você pode usar a função render, uma alternativa aos templates que é mais diretamente vinculada ao compilador.

Vamos mergulhar em um simples exemplo onde a função render seria prática. Digamos que você quer gerar links em cabeçalhos (headings):

<h1>
<a name="alo-mundo" href="#alo-mundo">
Olá Mundo!
</a>
</h1>

Para o código HTML acima, você decide que quer esta interface para o seu componente:

<anchored-heading :nivel="1">Olá Mundo!</anchored-heading>

Quando você começar a criar o componente de modo que gere os cabeçalhos de acordo com a propriedade nivel, rapidamente chegará a algo assim:

<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="nivel === 1">
<slot></slot>
</h1>
<h2 v-else-if="nivel === 2">
<slot></slot>
</h2>
<h3 v-else-if="nivel === 3">
<slot></slot>
</h3>
 <h4 v-else-if="nivel === 4">
<slot></slot>
</h4>
<h5 v-else-if="nivel === 5">
<slot></slot>
</h5>
<h6 v-else-if="nivel === 6">
<slot></slot>
</h6>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
nivel: {
type: Number,
required: true
}
}
})

Esse template não está passando uma boa sensação. Não é apenas extenso, ele inclusive duplica <slot></slot> para cada nível de cabeçalho, e teríamos que fazer o mesmo ao incluirmos um elemento <a>.

Enquanto templates funcionam muito bem para a maioria dos componentes, está claro que este caso é uma exceção. Então vamos tentar reescrevê-lo usando uma função render:

Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.nivel, // nome do elemento (tag)
this.$slots.default // array de elementos-filho (children)
)
},
props: {
nivel: {
type: Number,
required: true
}
}
})

Muito mais simples! Mais ou menos. O código é menor, mas requer maior familiaridade com as propriedades de uma instância Vue. Neste caso, você precisa saber que quando você inclui elementos filho em seu componente, sem especificar um atributo slot, como o Olá Mundo! dentro de anchored-heading, esses elementos estão acessíveis na instância do componente através de $slots.default. Se você ainda não leu, é altamente recomendado que leia a seção da API de propriedades da instância antes de se aprofundar em funções render.

Nós, Árvores e DOM Virtual

Antes de nos aprofundarmos nas funções de renderização, é importante saber um pouco sobre como os navegadores trabalham. Considere este HTML como exemplo:

<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>

Quando um navegador lê esse código, ele compila uma árvore de “nós DOM” para ajudar a manter o controle sobre todas as coisas, assim como você poderia querer construir uma árvore genealógica para manter o controle sobre a estensão de sua família.

A árvore de nós DOM para o HTML acima aparentaria isso:

Visualização da Árvore DOM

Cada elemento é um nó. Cada pedaço de texto é um nó. Até mesmo comentários são nós! Um nó é simplesmente um pedaço da página. E assim como na árvore genealógica, cada nó tem filhos (ou seja, cada pedaço pode conter outros pedaços dentro).

Atualizar todos esses nós eficientemente pode ser difícil, mas por sorte você nunca precisará fazer isso manualmente. Você apenas avisa o Vue qual HTML você quer em uma página, através de um template:

<h1>{{ blogTitle }}</h1>

Ou uma função de renderização:

render: function (createElement) {
return createElement('h1', this.blogTitle)
}

E em ambos os casos, Vue automaticamente mantém a página atualização, mesmo se blogTitle mudar.

DOM Virtual

Vue realiza isto través da construção de um DOM Virtual para manter o controle das mudanças que ele precisa aplicar no DOM real. Dê uma olhada mais de perto nesta linha:

return createElement('h1', this.blogTitle)

O que a função createElement retornará? Não é exatamente um elemento DOM real. Poderíamos chamar mais precisamente de createNodeDescription, uma vez que conterá a descrição para o Vue do tipo de nó a renderizar na página, incluindo as descrições de quaisquer nós filhos. Nós chamamos esta descrição de “nó virtual”, usualmente abreviado em inglês como VNode. “DOM Virtual” é como nós chamamos a árvore de VNodes completa, construída através da árvore de componentes Vue.

Parâmetros para createElement

A próxima coisa com a qual você precisa se familiarizar é como utilizar os recursos disponíveis em templates na função createElement. Estes são os parâmetros que createElement aceita:

// @returns {VNode}
createElement(
// {String | Object | Function}
// Um nome de elemento (tag) HTML, um objeto com opções de componente,
// ou uma função retornando um dentre os anteriores. Requerido.
'div',
// {Object}
// Um objeto de dados (chave: valor) correspondendo aos atributos
// que você usaria em template. Opcional.
{
// (veja detalhes na próxima seção, abaixo)
},
// {String | Array}
// VNodes filhos, mas usando `createElement()`,
// ou apenas Strings para 'VNodes textuais'. Opcional.
[
'Algum texto vem primeiro.',
createElement('h1', 'Um título')
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)

O Objeto de Dados em Detalhes

Importante observar: assim como v-bind:class e v-bind:style têm tratamento especial em templates, eles têm suas respectivas propriedades no primeiro nível de um objeto de dados VNode.

{
// Mesma API que `v-bind:class`
'class': {
foo: true,
bar: false
},
// Mesma API que `v-bind:style`
style: {
color: 'red',
fontSize: '14px'
},
// Atributos HTML comuns
attrs: {
id: 'foo'
},
// Propriedades (props) de componente
props: {
myProp: 'bar'
},
// Propriedades DOM
domProps: {
innerHTML: 'baz'
},
// Manipuladores de eventos são declarados dentro de
// `on`, porém modificadores como em `v-on:keyup.enter`
// não são suportados. Você terá que verificar
// manualmente o keyCode do evento no código.
on: {
click: this.clickHandler
},
// Somente para componentes. Permite a escuta a
// eventos nativos, ao invés de eventos emitidos
// pelo componente através de `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},
// Diretivas customizadas. Veja que como o bind de
// oldValue não pode ser setado, o Vue continua mantendo
// o valor dele para você
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// Slot na forma de
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// O nome do slot, se este componente é
// filho de outro componente
slot: 'name-of-slot'
// Outras propriedades especiais de primeiro nível
key: 'myKey',
ref: 'myRef'
}

Exemplo Completo

Com este conhecimento, nós agora podemos finalizar o componente que começamos:

var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
// criar id em kebabCase
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^\-|\-$)/g, '')
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
)
},
props: {
nivel: {
type: Number,
required: true
}
}
})

Restrições

VNodes Têm Que Ser Únicos

Todos os VNodes na árvore do componente têm que ser únicos. Isso significa que a seguinte função render é inválida:

render: function (createElement) {
var myParagraphVNode = createElement('p', 'oi')
return createElement('div', [
// Oh não! - VNodes em duplicidade!
myParagraphVNode, myParagraphVNode
])
}

Se você realmente quiser duplicar o mesmo elemento ou componente várias vezes, você pode fazê-lo com uma função factory. Por exemplo, a seguinte função render contém uma forma perfeitamente válida de renderizar 20 parágrafos idênticos:

render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'oi')
})
)
}

Substituindo templates por Código JavaScript

v-if e v-for

Sempre que algo possa ser facilmente realizado em código JavaScript tradicional, as funções de renderização não oferecerão alternativas específicas. Por exemplo, em templates usando v-if e v-for:

<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>Nenhum item encontrado.</p>

Isto poderia ser reescrito com if/else e map do JavaScript, em uma função render:

render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'Nenhum item encontrado.')
}
}

v-model

Não existe contrapartida direta de v-model em funções de renderização - você terá que implementar a lógica por si próprio:

render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.value = event.target.value
self.$emit('input', event.target.value)
}
}
})
}

Este é o custo de ir para um nível mais baixo, mas também oferece muito mais controle sobre os detalhes da interação em comparação ao v-model.

Eventos e Modificadores

Para modificadores de eventos .passive, .capture e .once, Vue oferece prefixos que podem ser usandos em conjunto com on:

Modificador Prefixo
.passive &
.capture !
.once ~
.capture.once ou
.once.capture
~!

Por exemplo:

on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
`~!mouseover`: this.doThisOnceInCapturingMode
}

Para todos os outros eventos e modificadores de teclado, nenhum prefixo proprietário é necessário, pois você pode simplesmente usar métodos do objeto event no código.

Modificador Equivalente no Código
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
Teclas:
.enter, .13
if (event.keyCode !== 13) return (troque 13 por outro key code para outros modificadores)
Especiais:
.ctrl, .alt, .shift, .meta
if (!event.ctrlKey) return (troque ctrlKey por altKey, shiftKey, ou metaKey)

Aqui está um exemplo com todos estes modificadores em conjunto:

on: {
keyup: function (event) {
// Aborta se o elemento emitindo o evento não é
// o elemento no qual o evento está vinculado
if (event.target !== event.currentTarget) return
// Aborta se a tecla acionada não foi o Enter (13)
// e a tecla Shift não foi pressionada ao mesmo tempo
if (!event.shiftKey || event.keyCode !== 13) return
// Para a propagação do evento
event.stopPropagation()
// Previne a execução padrão do keyup para o elemento
event.preventDefault()
// ...
}
}

Slots

Você pode acessar conteúdo estáticos de slots como Arrays de VNodes a partir de this.$slots:

render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}

E acessar slots com escopo como funções que retornar VNodes a partir de this.$scopedSlots:

render: function (createElement) {
// `<div><slot :text="msg"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.msg
})
])
}

Para passar slots a um componente filho usando funções de renderização, use o campo scopedSlots nos dados do VNode:

render (createElement) {
return createElement('div', [
createElement('child', {
// passe `scopedSlots` no objeto de dados
// na forma de { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}

JSX

Se você estiver escrevendo muitas funções render, pode se tornar cansativo e passível de erros escrever muitas linhas de código como essas:

createElement(
'anchored-heading', {
props: {
nivel: 1
}
}, [
createElement('span', 'Alô'),
' Mundo!'
]
)

Especialmente quando a versão usando template é tão simples em comparação:

<anchored-heading :nivel="1">
<span>Alô</span> Mundo!
</anchored-heading>

Por isso há um plugin para Babel destinado à utilização de JSX com o Vue, nos trazendo de volta a uma sintaxe mais semelhante à utilizada em templates:

import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render (h) {
return (
<AnchoredHeading nivel={1}>
<span>Alô</span> Mundo!
</AnchoredHeading>
)
}
})

Apelidar createElement como h é uma convenção comum que você verá na comunidade Vue e é necessária para o uso de JSX. Se h não estiver disponível no escopo, sua aplicação irá gerar um erro.

Para mais informações sobre como JSX é mapeado para JavaScript, veja a documentação de utilização.

Componentes Funcionais

O componente de cabeçalho com link que criamos anteriormente é relativamente simples. Ele não gerencia nenhum estado próprio, não observa nenhum estado passado para si, e não tem nenhum método ligado ao ciclo de vida da instância. Realmente, é apenas uma função com algumas propriedades.

Em casos como este, nós podemos marcar componentes como functional, o que significa que eles são stateless (sem estado, ou seja, sem data) e são instanceless (sem instância, ou seja, sem o contexto this). Um componente funcional tem este formato:

Vue.component('meu-componente', {
functional: true,
// Para compensar a inexistência de uma instância,
// é providenciado um segundo argumento: "context".
render: function (createElement, context) {
// ...
},
// Props são opcionais
props: {
// ...
}
})

Nota: em versões anteriores a 2.3.0, a opção props é obrigatória se você deseja aceitar propriedades em um componente funcional. Em 2.3.0+ você pode omitir props e todos os atributos encontrados no nó do componente serão implicitamente extraídos como propriedades.

Tudo que um componente funcional necessitar é passado através de context, o qual é um objeto contendo:

Após acrescentar functional: true, adaptar a função render do nosso componente de cabeçalho com link iria requerer somente acrescentar o parâmetro context, e atualizar this.$slots.default para context.children, e por fim atualizar this.level para context.props.level.

Como componentes funcionais são apenas funções, eles são muito mais leves para renderizar. Entretanto, por carecer de uma instância persistente, eles não são exibidos na árvore de componentes do Vue devtools.

Eles também são muito úteis como componentes encapsuladores. Por exemplo, quando você precisa:

Aqui está um exemplo de um componente smart-list que delega para componentes mais específicos, dependendo das propriedades (props) passadas a ele:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
},
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
}
})

slots() vs. children

Você pode ser perguntar por que nós precisamos de ambos - slots() e children. Não seria slots().default o mesmo que children? Em alguns casos, sim - mas o que aconteceria se você tivesse um componente funcional contendo os seguintes elementos filhos?

<meu-componente-funcional>
<p slot="foo">primeiro</p>
<p>segundo</p>
</meu-componente-funcional>

Para este componente, children lhe fornecerá ambos os parágrafos, enquanto slots().default lhe fornecerá apenas o segundo, e slots().foo lhe fornecerá apenas o primeiro. Tendo tanto children quanto slots() lhe permite escolher se este componente precisa saber sobre os slots ou talvez apenas delegar tal responsabilidade para outro componente simplesmente passando adiante children.

Compilação de Templates

Você pode estar interessado em saber que templates do Vue são compilados para funções render. Este é um detalhe de implementação que você não necessita saber, mas se você quiser ver como templates específicos ficam quando compilados, pode ser interessante. Abaixo uma pequena demonstração usando Vue.compile para compilar ao vivo uma String de template:

{{ result.render }}
_m({{ index }}): {{ fn }}
{{ result.staticRenderFns }}
{{ result }}