No coração do framework FormKit está o @formkit/core
. Este pacote sem dependências é responsável por quase todas as funções críticas de baixo nível do FormKit, tais como:
A funcionalidade do núcleo do FormKit não é exposta à sua aplicação por meio de uma instância centralizada, mas sim por um conjunto distribuído de "nós" (FormKitNode
), onde cada nó representa uma única entrada.
Isso espelha o HTML — na verdade, a estrutura do DOM é realmente uma árvore geral e os nós do núcleo do FormKit refletem essa estrutura. Por exemplo, um formulário de login simples poderia ser representado pelo seguinte gráfico de árvore:
Neste diagrama, um nó form
é pai de três nós filhos — email
, password
e submit
. Cada componente de entrada no gráfico "possui" um nó do núcleo do FormKit, e cada nó contém suas próprias opções, configurações, props, eventos, plugins, ganchos de ciclo de vida, etc. Esta arquitetura garante que os recursos primários do FormKit sejam desacoplados do framework de renderização (Vue) — chave para reduzir efeitos colaterais e manter um desempenho extremamente rápido.
Além disso, esta arquitetura descentralizada permite uma flexibilidade tremenda. Por exemplo — um formulário pode usar plugins diferentes de outros formulários no mesmo aplicativo, uma entrada de grupo pode modificar a configuração de suas subentradas, e regras de validação podem até ser escritas para usar props de outra entrada.
Cada componente <FormKit>
possui um único nó central, e cada nó deve ser um dos três tipos:
Os nós centrais são sempre um dos três tipos (input, list ou group). Estes não são os mesmos que tipos de entrada — dos quais pode haver variação ilimitada. Estritamente falando, todas as entradas têm 2 tipos: seu tipo de nó (como input
), e seu tipo de entrada (como checkbox
).
A maioria das entradas nativas do FormKit tem um tipo de nó input
— operam em um único valor. O valor em si pode ser de qualquer tipo, como objetos, arrays, strings e números — qualquer valor é aceitável. No entanto, nós do tipo input
são sempre folhas — o que significa que não podem ter filhos.
import { createNode } from '@formkit/core'
const input = createNode({
type: 'input', // assume 'input' como padrão se não especificado
value: 'hello node world',
})
console.log(input.value)
// 'hello node world'
Uma lista é um nó que produz um valor de array. Os filhos de um nó de lista produzem um valor no array da lista. Os nomes dos filhos imediatos são ignorados — em vez disso, cada um é atribuído a um índice no array da lista.
import { createNode } from '@formkit/core'
const list = createNode({
type: 'list',
children: [
createNode({ value: 'paprika@example.com' }),
createNode({ value: 'bill@example.com' }),
createNode({ value: 'jenny@example.com' }),
],
})
console.log(list.value)
// ['paprika@example.com', 'bill@example.com', 'jenny@example.com']
Um grupo é um nó que produz um valor de objeto. Os filhos de um nó de grupo usam seu name
para produzir uma propriedade de mesmo nome no objeto de valor do grupo — <FormKit type="form">
é uma instância de um grupo.
import { createNode } from '@formkit/core'
const group = createNode({
type: 'group',
children: [
createNode({ name: 'meat', value: 'turkey' }),
createNode({ name: 'greens', value: 'salad' }),
createNode({ name: 'sweets', value: 'pie' }),
],
})
console.log(group.value)
// { meat: 'turkey', greens: 'salad', sweets: 'pie' }
Além de especificar o type
de nó ao chamar createNode()
, você pode passar qualquer uma das seguintes opções:
Opções | Padrão | Descrição |
---|---|---|
children | [] | Instâncias de FormKitNode filhos. |
config | {} | Opções de configuração. Estas se tornam os padrões do objeto props . |
name | {type}_{n} | O nome do nó/entrada. |
parent | null | A instância de FormKitNode pai. |
plugins | [] | Um array de funções de plugin. |
props | {} | Um objeto de pares chave/valor que representam os detalhes da instância atual. |
type | input | O tipo de FormKitNode a ser criado (list , group ou input ). |
value | undefined | O valor inicial da entrada. |
O FormKit usa um sistema de configuração baseado em herança. Quaisquer valores declarados na opção config
são automaticamente passados para os filhos (e todos os descendentes) daquele nó, mas não são passados para irmãos ou pais. Cada nó pode sobrescrever seus valores herdados fornecendo sua própria configuração, e esses valores serão por sua vez herdados por quaisquer filhos mais profundos e descendentes. Por exemplo:
const parent = createNode({
type: 'group',
config: {
color: 'yellow',
},
children: [
createNode({
type: 'list',
config: { color: 'pink' },
children: [createNode(), createNode()],
}),
createNode(),
],
})
O código acima resultará em cada nó tendo a seguinte configuração:
É uma boa prática ler valores de configuração de node.props
em vez de node.config
. A próxima seção detalha esse recurso.
Os objetos node.props
e node.config
são intimamente relacionados. node.config
é melhor pensado como os valores iniciais para node.props
. props
é um objeto de forma arbitrária que contém detalhes sobre a atual instância do nó.
A melhor prática é sempre ler dados de configuração e propriedades de node.props
, mesmo que o valor original seja definido usando node.config
. Props explicitamente definidas têm precedência sobre opções de configuração.
const child = createNode({
props: {
flavor: 'cherry',
},
})
const parent = createNode({
type: 'group',
config: {
size: 'large',
flavor: 'grape',
},
children: [child],
})
console.log(child.props.size)
// outputs: 'large'
console.log(child.props.flavor)
// outputs: 'cherry'
Ao usar o componente <FormKit>
, quaisquer props definidas para o tipo de entrada type
são automaticamente definidas como propriedades de node.props
. Por exemplo: <FormKit label="Email" />
resultaria em node.props.label
sendo Email
.
Você pode definir o valor inicial de um nó fornecendo a opção value
em createNode()
— mas o FormKit é todo sobre interatividade, então como atualizamos o valor de um nó já definido? Usando node.input(value)
.
import { createNode } from '@formkit/core'
const username = createNode()
username.input('jordan-goat98')
console.log(username.value)
// undefined 👀 espera — o quê!?
No exemplo acima, username.value
ainda está indefinido imediatamente após ser definido porque node.input()
é assíncrono. Se você precisar ler o valor resultante após chamar node.input()
, você pode aguardar a promessa retornada.
import { createNode } from '@formkit/core'
const username = createNode()
username.input('jordan-goat98').then(() => {
console.log(username.value)
// 'jordan-goat98'
})
Como node.input()
é assíncrono, o restante do nosso formulário não precisa recalcular suas dependências a cada tecla pressionada. Isso também fornece uma oportunidade para realizar modificações no valor não confirmado antes de ser "comprometido" com o restante do formulário. No entanto — apenas para uso interno do nó — uma propriedade _value
contendo o valor não confirmado da entrada também está disponível.
Você não pode atribuir diretamente o valor de uma entrada node.value = 'foo'
. Em vez disso, você sempre deve usar node.input(value)
Agora que entendemos que node.input()
é assíncrono, vamos explorar como o FormKit resolve o problema da "árvore estabilizada". Imagine um usuário digitando rapidamente seu endereço de e-mail e pressionando "enter" muito rapidamente — submetendo o formulário. Como node.input()
é assíncrono, é provável que dados incompletos sejam submetidos. Precisamos de um mecanismo para saber quando o formulário inteiro está "estabilizado".
Para resolver isso, os nós do FormKit rastreiam automaticamente a "perturbação" da árvore, subárvore e nó. Isso significa que o formulário (geralmente o nó raiz) sempre sabe o estado de estabilização de todos os inputs que contém.
O gráfico a seguir ilustra essa "contagem de perturbação". Clique em qualquer nó de input (azul) para simular a chamada de node.input()
e perceba como o formulário inteiro está sempre ciente de quantos nós estão "perturbados" a qualquer momento. Quando o nó raiz tem uma contagem de perturbação de 0
, o formulário está estabilizado e seguro para ser submetido.
import { createNode } from '@formkit/node'
const form = createNode({
type: 'group',
children: [
createNode()
createNode()
createNode()
],
})
// ...
// interação do usuário:
async function someEvent () {
await form.settled
// agora sabemos que o formulário está completamente "estabilizado"
// e que form.value é preciso.
}
O input <FormKit type="form">
já incorpora esse comportamento de espera. Ele não chamará seu manipulador @submit
até que seu formulário esteja completamente estabilizado. No entanto, ao construir inputs avançados, pode ser útil entender esses princípios subjacentes.
Às vezes, pode ser útil obter a instância subjacente de um nó a partir do componente Vue <FormKit>
. Existem três métodos principais para buscar o nó de um input.
getNode()
(ou o $formkit.get()
do plugin Vue para a API de Opções)useFormKitNodeById
@node
.ref
de template.getNode()
Ao usar o FormKit, você pode acessar um nó atribuindo-lhe um id
e depois acessando-o por essa propriedade através da função getNode()
.
Você deve atribuir um id
ao input para usar este método.
Ao usar a API de Opções do Vue, você pode acessar o mesmo comportamento de getNode()
usando this.$formkit.get()
.
useFormKitNodeById()
A função de composição useFormKitNodeById
permite que você acesse um nó pelo seu id
de dentro de um componente Vue, retornando um ref
do Vue que será preenchido com a instância do FormKitNode
assim que ela for criada.
Para outras funções de composição similares, veja a documentação de composables.
Você deve atribuir um id
ao input para usar este método.
Outra maneira de obter o node
subjacente é ouvir o evento @node
, que é emitido apenas uma vez quando o componente inicializa o nó pela primeira vez.
Atribuir um componente <FormKit>
a um ref
também permite fácil acesso ao nó.
Para navegar pelos nós dentro de um grupo ou lista, use node.at(address)
— onde address
é o name
do nó que está sendo acessado (ou o caminho relativo até o nome). Por exemplo:
import { createNode } from '@formkit/core'
const group = createNode({
type: 'group',
children: [createNode({ name: 'email' }), createNode({ name: 'password' })],
})
// Retorna o nó de email
group.at('email')
Se o nó inicial tiver irmãos, ele tentará localizar uma correspondência nos irmãos (internamente, é isso que o FormKit usa para regras de validação como confirm:address
).
import { createNode } from '@formkit/core'
const email = createNode({ name: 'email' })
const password = createNode({ name: 'password' })
const group = createNode({
type: 'group',
children: [email, password],
})
// Acessa o irmão para retornar o nó de senha
email.at('password')
Você pode ir mais fundo do que um nível usando um caminho relativo com sintaxe de ponto. Aqui está um exemplo mais complexo:
import { createNode } from '@formkit/core'
const group = createNode({
type: 'group',
children: [
createNode({ name: 'team' }),
createNode({
type: 'list',
name: 'users',
children: [
createNode({
type: 'group',
children: [
createNode({ name: 'email' }),
createNode({ name: 'password', value: 'foo' }),
],
}),
createNode({
type: 'group',
children: [
createNode({ name: 'email' }),
createNode({ name: 'password', value: 'fbar' }),
],
}),
],
}),
],
})
// saída: 'foo'
console.log(group.at('users.0.password').value)
Note como a navegação pela list
usa chaves numéricas, isso porque o tipo list
usa índices de array automaticamente.
Endereços de nós também podem ser expressos como arrays. Por exemplo, node.at('foo.bar')
poderia ser expresso como node.at(['foo', 'bar'])
.
Também estão disponíveis para uso em node.at()
alguns "tokens" especiais:
Token | Descrição |
---|---|
$parent | O ancestral imediato do nó atual. |
$root | O nó raiz da árvore (o primeiro nó sem pai). |
$self | O nó atual na travessia. |
find() | Uma função que realiza uma busca em largura por um valor e propriedade correspondentes. Por exemplo: node.at('$root.find(555, value)') |
Esses tokens são usados em endereços de sintaxe de ponto assim como você usaria o nome de um nó:
import { createNode } from '@formkit/core'
const secondEmail = createNode({ name: 'email' })
createNode({
type: 'group',
children: [
createNode({ name: 'team', value: 'charlie@factory.com' }),
createNode({
type: 'list',
name: 'users',
children: [
createNode({
type: 'group',
children: [
createNode({ name: 'email', value: 'james@peach.com' }),
createNode({ name: 'password', value: 'foo' }),
],
}),
createNode({
type: 'group',
children: [
secondEmail, // Vamos começar aqui.
createNode({ name: 'password', value: 'fbar' }),
],
}),
],
}),
],
})
// Navegar do segundo email para o primeiro
console.log(secondEmail.at('$parent.$parent.0.email').value)
// exibe: charlie@factory.com
Os nós têm seus próprios eventos que são emitidos durante o ciclo de vida do nó (não relacionados aos eventos do Vue).
Para observar um evento específico, use node.on()
.
// Ouvir qualquer propriedade sendo definida ou alterada.
node.on('prop', ({ payload }) => {
console.log(`prop ${payload.prop} foi definida para ${payload.value}`)
})
node.props.foo = 'bar'
// exibe: prop foo foi definida para bar
Os callbacks de manipuladores de eventos recebem um único argumento do tipo FormKitEvent
, a estrutura do objeto é:
{
// O conteúdo do evento — uma string, um objeto, etc.
payload: { cause: 'sorvete', duration: 200 },
// O nome do evento, isso corresponde ao primeiro argumento de node.on().
name: 'brain-freeze',
// Se este evento deve ou não borbulhar para o próximo pai.
bubble: true,
// O FormKitNode original que emitiu o evento.
origin: node,
}
Os eventos de nó (por padrão) borbulham pela árvore de nós, mas node.on()
responderá apenas aos eventos emitidos pelo mesmo nó. No entanto, se você quiser também capturar eventos que borbulham de descendentes, você pode acrescentar a string .deep
ao final do nome do seu evento:
import { createNode } from '@formkit/core'
const group = createNode({ type: 'group' })
group.on('created.deep', ({ payload: child }) => {
console.log('nó filho criado:', child.name)
})
const child = createNode({ parent: group, name: 'party-town-usa' })
// exibe: 'nó filho criado: party-town-usa'
Cada chamada para registrar um observador com node.on()
retorna um "recibo" — uma chave gerada aleatoriamente — que pode ser usada mais tarde para parar de observar aquele evento (similar a setTimeout()
e clearTimeout()
) usando node.off(recibo)
.
const recibo = node.on('input', ({ payload }) => {
console.log('recebido input: ', payload)
})
node.input('foobar')
// saída: 'recebido input: foobar'
node.off(recibo)
node.input('fizz buzz')
// sem saída
A seguir está uma lista abrangente de todos os eventos emitidos por @formkit/core
. Códigos de terceiros podem emitir eventos adicionais não incluídos aqui.
Nome | Payload | Bubbles | Descrição |
---|---|---|---|
commit | qualquer | sim | Emitido quando o valor de um nó é confirmado, mas antes de ser transmitido para o restante do formulário. |
config:{property} | qualquer (o valor) | sim | Emitido sempre que uma opção de configuração específica é definida ou alterada. |
count:{property} | qualquer (o valor) | não | Emitido sempre que o valor do contador de um livro-razão muda. |
child | FormKitNode | sim | Emitido quando um novo nó filho é adicionado, criado ou atribuído a um pai. |
created | FormKitNode | sim | Emitido imediatamente antes do nó ser retornado ao chamar createNode() (plugins e recursos já foram executados). |
defined | FormKitTypeDefinition | sim | Emitido quando o "tipo" de um nó é definido, isso geralmente acontece durante createNode() . |
destroying | FormKitNode | sim | Emitido quando o node.destroy() é chamado, depois de ter sido desanexado de quaisquer pais. |
dom-input-event | Event | sim | Emitido quando o manipulador DOMInput é chamado, útil para obter o evento de entrada HTML original no core. |
input | qualquer (o valor) | sim | Emitido quando node.input() é chamado — após o hook input ter sido executado. |
message-added | FormKitMessage | sim | Emitido quando uma nova mensagem node.store foi adicionada. |
message-removed | FormKitMessage | sim | Emitido quando uma mensagem node.store foi removida. |
message-updated | FormKitMessage | sim | Emitido quando uma mensagem node.store foi alterada. |
mounted | nenhum | sim | Emitido quando o componente <FormKit> que possui este nó é montado no dom. |
prop:{propName} | qualquer (o valor) | sim | Emitido sempre que uma prop específica é definida ou alterada. |
prop | { prop: string, value: any } | sim | Emitido sempre que uma prop é definida ou alterada. |
reset | FormKitNode | sim | Emitido sempre que um formulário ou grupo é reiniciado. |
settled | booleano | não | Emitido sempre que a contagem de perturbações de um nó se estabiliza ou desestabiliza. |
settled:{counterName} | booleano | não | Emitido sempre que um contador específico do livro-razão se estabiliza (volta a zero). |
unsettled:{counterName} | booleano | não | Emitido sempre que um contador específico do livro-razão se desestabiliza (sobe acima de zero). |
text | string ou FormKitTextFragment | não | Emitido após o hook text ter sido executado — geralmente quando processando texto de interface que pode ter sido traduzido. |
Quando uma opção de configuração muda, todos os nós herdeiros (incluindo o nó de origem) também emitirão eventos prop
e prop:{propName}
, desde que eles não substituam essa propriedade em seus próprios objetos props
ou config
.
Eventos do Node são emitidos com node.emit()
. Você pode aproveitar esse recurso para emitir seus próprios eventos sintéticos a partir de seus próprios plugins.
node.emit('myEvent', payloadGoesHere)
Um terceiro argumento opcional bubble
também está disponível. Quando definido como false
, ele impede que seu evento se propague pela árvore do formulário.
Hooks são despachantes de middleware que são acionados durante operações predefinidas do ciclo de vida. Esses hooks permitem que o código externo estenda a funcionalidade interna do @formkit/core
. A tabela a seguir detalha todos os hooks disponíveis:
Hook | Valor | Descrição |
---|---|---|
classes |
| Despachado após todas as operações de classe serem executadas, antes da conversão final para uma string. |
commit | any | Despachado ao definir o valor de um nó após o input e o debounce de node.input() ser chamado. |
commitRaw | any | Despachado ao definir o valor de um nó após o input e o debounce de node.input() ser chamado. |
error | string | Despachado ao processar um erro lançado — erros são geralmente entradas, e a saída final deve ser uma string. |
init | FormKitNode | Despachado após o nó ser inicialmente criado, mas antes de ser retornado em createNode() . |
input | any | Despachado de forma síncrona em cada evento de entrada (cada tecla pressionada) antes do commit . |
message | FormKitMessage | Despachado quando uma mensagem está sendo definida em node.store |
prop |
| Despachado quando qualquer propriedade está sendo atribuída. |
setErrors | { localErrors: ErrorMessages, childErrors?: ErrorMessages } | Despachado quando erros explícitos estão sendo definidos em um nó (não erros de validação). |
submit | Record<string, any> | Despachado quando o formulário FormKit é submetido e passa pela validação. Esse hook permite que você modifique os valores do formulário (clonados) antes de serem passados para o manipulador de envio |
text | FormKitTextFragment | Despachado quando uma string gerada pelo FormKit precisa ser exibida — permitindo que i18n ou outros plugins interceptem. |
Para utilizar esses hooks, você deve registrar um middleware de hook. Um middleware é simplesmente uma função que aceita 2 argumentos — o valor do hook e next
— uma função que chama o próximo middleware na pilha e retorna o valor.
Para registrar um middleware, passe-o para o node.hook
que você deseja usar:
import { createNode } from '@formkit/core'
const node = createNode()
// Isso transformaria todos os rótulos em "Rótulo Diferente!"
node.hook.prop((payload, next) => {
if ((payload.prop = 'label')) {
payload.value = 'Rótulo Diferente!'
}
return next(payload)
})
Hooks podem ser registrados em qualquer lugar da sua aplicação, mas o local mais comum onde os hooks são usados é em um plugin.
Plugins são o mecanismo principal para estender a funcionalidade do FormKit. O conceito é simples — um plugin é apenas uma função que aceita um nó. Essas funções são então automaticamente chamadas quando um nó é criado ou quando o plugin é adicionado ao nó. Plugins funcionam de maneira semelhante às opções de configuração — eles são automaticamente herdados por filhos e descendentes.
import { createNode } from '@formkit/core'
// Um plugin para mudar o valor de uma propriedade.
const myPlugin = (node) => {
if (node.type === 'group') {
node.props.color = 'amarelo'
} else {
node.props.color = 'azul-petróleo'
}
}
const node = createNode([
plugins: [myPlugin],
children: [createNode()]
])
No exemplo acima, o plugin é definido apenas no pai, mas o filho também herda o plugin. A função myPlugin
será chamada duas vezes — uma para cada nó no gráfico (que só tem dois neste exemplo):
Além de estender e modificar nós, os plugins têm um papel adicional — expor bibliotecas de entrada. Uma "biblioteca" é uma função atribuída à propriedade library
de um plugin que aceita um nó e determina se sabe como "definir" esse nó. Se sim, ela chama node.define()
com uma definição de entrada.
Por exemplo, se quiséssemos criar um plugin que expusesse algumas novas entradas: italy
e france
, poderíamos escrever um plugin para fazer isso:
Desenvolvedores experientes notarão algumas propriedades interessantes desse padrão de biblioteca de plugin:
node.define()
. Frequentemente, isso é simplesmente verificar node.props.type
, mas você pode definir diferentes entradas com base em outras condições, como se uma propriedade específica estiver definida.Cada nó possui seu próprio armazenamento de dados. Os objetos nestes armazenamentos são chamados de "mensagens" e essas mensagens são especialmente valiosas para três casos de uso principais:
Cada mensagem (FormKitMessage
em TypeScript) no armazenamento é um objeto com a seguinte estrutura:
{
// Indica se esta mensagem bloqueia o envio do formulário (padrão: false).
blocking: true,
// Deve ser um valor de string único (padrão: string aleatória).
key: 'yourkey',
// (opcional) Objeto de metadados sobre esta mensagem (padrão: {}).
meta: {
// (opcional) Se definido, i18n usa isso em vez da chave para encontrar mensagens de localidade.
messageKey: 'i18nKey',
// (opcional) Se definido, esses argumentos serão espalhados para a função de localidade i18n.
i18nArgs: [...any],
// (opcional) Se definido como false, a mensagem verificará a localização.
localize: true,
// (opcional) A localidade da mensagem (padrão: node.config.locale)
locale: 'en',
// Qualquer outro metadado que você desejar.
...any
},
// Uma categoria arbitrária à qual esta mensagem pertence (para fins de filtragem).
// Por exemplo: 'validation' ou 'success' (padrão: 'state')
type: string,
// (opcional) deve ser uma string, número ou booleano (padrão: undefined).
value: 'Ops, nosso servidor está quebrado!',
// Esta mensagem deve ser mostrada aos usuários finais? (padrão: true)
visible: true
}
Uma função auxiliar createMessage({})
pode ser importada de @formkit/core
para mesclar seus dados de mensagem com os valores padrão acima para criar um novo objeto de mensagem.
Para adicionar ou atualizar uma mensagem, use node.store.set(FormKitMessage)
. As mensagens são então disponibilizadas em node.store.{messageKey}
import { createMessage, createNode } from '@formkit/core'
const node = createNode()
const message = createMessage({
key: 'clickHole',
value: 'Por favor, clique 100 vezes.',
})
node.store.set(message)
console.log(node.store.clickHole.value)
// saída: 'Por favor, clique 100 vezes.'
As mensagens serão automaticamente traduzidas se o plugin @formkit/i18n
estiver instalado e uma chave correspondente estiver disponível na localidade ativa. Leia a documentação de i18n.
Uma das chaves para o desempenho do FormKit é sua capacidade de contar eficientemente mensagens que correspondem a um determinado critério (na loja), e então manter um total contínuo dessas mensagens à medida que mudanças são feitas (incluindo de nós filhos). Esses contadores são criados usando node.ledger
.
Vamos dizer que queremos contar quantas mensagens estão sendo exibidas atualmente. Poderíamos fazer isso contando mensagens com a propriedade visible
definida como true
.
Note que o segundo argumento de node.ledger.count()
é uma função. Esta função aceita uma mensagem como argumento e espera que o valor de retorno seja um booleano, indicando se essa mensagem deve ser contada ou não. Isso permite que você crie contadores arbitrários para qualquer tipo de mensagem.
Ao usar um contador em um nó group
ou list
, o contador se propagará pela árvore somando o valor de todas as mensagens que passam pela função de critério e, em seguida, rastreando essa contagem para mudanças na loja.
O plugin de validação já declara um contador chamado blocking
que conta a propriedade de bloqueio de todas as mensagens. É assim que os formulários FormKit sabem se todos os seus filhos estão "válidos".