Resumo
- Análise de requisitos.
- Definição de interfaces.
- Definição das propriedades dos componentes de formulário.
- Criação de arquivos JSON.
- Encapsulamento baseado em el-form para renderização via JSON.
- Implementação de múltiplas colunas, validações e divisão em seções.
- Uso de slots para extensões personalizadas.
- Desenvolvimento de componentes filhos personalizados (continuação no próximo artigo).
- Design e implementação de componentes filhos de formulário (continuação no próximo artigo).
- Ferramentas para manutenção de arquivos JSON (continuação no próximo próximo artigo).
Análise de Requisitos
Formulários são amplamente utilizados em diversos ambientes como páginas web, plataformas e sistemas administrativos. Sejam simples ou complexos, seu objetivo é coletar dados dos usuários para envio ao backend.
Requisitos básicos para componentes de formulário:
- Renderização baseada em JSON.
- Criação automática do modelo a partir de JSON.
- Facilidade na entrada de dados pelo usuário.
- Validação dos dados inseridos.
- Simplicidade para desenvolvedores implementarem funcionalidades.
- Suporte a múltiplas colunas.
- Divisão em seções.
- Extensibilidade personalizada.
O componente el-form oferece validações e extensibilidade com uma interface visual atraente. Podemos reutilizar essas funcionalidades para criar uma camada de abstração que suporte recursos adicionais como multi-coluna, divisão em seções e renderização via JSON.
Design da Interface
Inicialmente, dividimos as propriedades necessárias pelos componentes em duas categorias: propriedades do el-form e dados específicos para baixo código.
Classificação das Propriedades
Organizamos as propriedades em um mapa mental:
Interface do Componente de Formulário
Convertendo isso para uma interface:
IFromProps: Interface para o componente de formulário
/**
* Propriedades do componente de formulário
*/
export interface IFromProps {
/**
* Modelo do formulário, objeto contendo vários campos.
*/
model: any,
/**
* Modelo filtrado com base em opções, any
*/
partModel?: any,
/**
* Metadados necessários para o formulário
*/
formMeta: IFromMeta,
/**
* Propriedades dos componentes filhos do formulário, IFormItem
*/
itemMeta: IFormItemList,
/**
* Sufixo do rótulo, string
*/
labelSuffix: string,
/**
* Largura do rótulo, string
*/
labelWidth: string,
/**
* Tamanho do componente, ESize
*/
size: ESize,
/**
* Outras propriedades estendidas
*/
[propName: string]: any
}
- model: Dados do formulário, criados a partir do JSON.
- partModel: Dados visíveis após interações entre componentes.
- formMeta: Conjunto de metadados necessários para baixo código.
- itemMeta: Propriedades dos componentes filhos.
- Outros: Propriedades do componente el-form, acessíveis via $attrs.
Embora tenhamos tentado aplicar essa interface às props do componente, encontramos limitações:
- Com Option API, não há suporte à definição de interfaces complexas.
- Com Composition API, apesar do suporte, as interfaces só podem ser definidas internamente ao componente.
Portanto, optamos por não restringir todas as props com a interface no momento.
IFromMeta: Interface de metadados para baixo código
/**
* Metadados necessários para formulários em baixo código
*/
export interface IFromMeta {
/**
* ID do módulo, usado em contextos combinados
*/
moduleId: number | string,
/**
* ID do formulário, um módulo pode conter vários formulários
*/
formId: number | string,
/**
* Ordem e visibilidade dos campos do formulário, Array<number | string>
*/
colOrder: Array<number | string>,
/**
* Número de colunas do formulário, número
*/
columnsNumber: number,
/**
* Configuração de seções, ISubMeta
*/
subMeta: ISubMeta,
/**
* Regras de validação, IRuleMeta
*/
ruleMeta: IRuleMeta,
/**
* Relacionamentos de ligação entre componentes, ILinkageMeta
*/
linkageMeta: ILinkageMeta
}
- moduleId: Identificador do módulo.
- formId: Identificador do formulário dentro do módulo.
- colOrder: Lista ordenada dos IDs dos campos.
- columnsNumber: Quantidade de colunas do formulário.
ISubMeta: Interface para divisão em seções
/**
* Configurações de seções do formulário
*/
export interface ISubMeta {
type: ESubType, // Tipo de seção: card, tab, step, "" (sem seção)
cols: Array<{ // Informações das seções
title: string, // Nome da seção
colIds: Array<number> // IDs dos componentes nessa seção
}>
}
IRule, IRuleMeta: Interfaces para validação
/**
* Uma regra de validação, um componente pode ter várias regras
*/
export interface IRule {
/**
* Momento da validação: blur, change, click, keyup
*/
trigger?: "blur" | "change" | "click" | "keyup",
/**
* Mensagem de erro
*/
message?: string,
/**
* Campo obrigatório
*/
required?: boolean,
/**
* Tipo de dado: any, date, url, etc
*/
type?: string,
/**
* Comprimento
*/
len?: number, // Comprimento exato
/**
* Valor máximo
*/
max?: number,
/**
* Valor mínimo
*/
min?: number,
/**
* Expressão regular
*/
pattern?: string
}
/**
* Conjunto de regras de validação do formulário
*/
export interface IRuleMeta {
/**
* ID do componente como chave, array de regras de validação
*/
[key: string | number]: Array<IRule>
}
ILinkageMeta: Interface para ligações entre componentes
/**
* Configurações de ligação de componentes
*/
export interface ILinkageMeta {
/**
* ID do componente como chave, valor é um objeto com valores e IDs dos componentes a serem exibidos
*/
[key: string | number]: {
/**
* Valor do componente como chave, array de IDs dos componentes visíveis
*/
[id: string | number]: Array<number>
}
}
Definição das Props do Componente
Com as interfaces definidas, agora criamos as props do componente (implementando as interfaces).
Utilizamos Option API para permitir importações externas de interfaces:
import type { PropType } from 'vue'
import type {
IFromMeta // Metadados necessários para o componente de formulário
} from '../types/30-form'
import type { IFormItem, IFormItemList } from '../types/20-form-item'
import type { ESize } from '../types/enum'
import { ESize as size } from '../types/enum'
/**
* Propriedades necessárias para o componente de formulário
*/
export const formProps = {
/**
* Modelo completo do formulário
*/
model: {
type: Object as PropType<any>,
required: true
},
/**
* Modelo filtrado conforme opções
*/
partModel: {
type: Object as PropType<any>,
default: () => { return {}}
},
/**
* Metadados do componente de formulário
*/
formMeta: {
type: Object as PropType<IFromMeta>,
default: () => { return {}}
},
/**
* Metadados dos componentes filhos
*/
itemMeta: {
type: Object as PropType<IFormItemList>,
default: () => { return {}}
},
/**
* Sufixo do rótulo
*/
labelSuffix: {
type: String,
default: ':'
},
/**
* Largura do rótulo
*/
labelWidth: {
type: String,
default: '130px'
},
/**
* Tamanho do componente
*/
size: {
type: Object as PropType<ESize>,
default: size.small
}
}
Uso do Componente
Para usar, basta importar e desesrtuturar:
// Propriedades do componente de formulário
import { formProps, formController } from '../map'
export default defineComponent({
name: 'nf-el-from-div',
props: {
...formProps
// Pode adicionar outras propriedades aqui
},
setup (props, context) {
// ...
}
})
Arquivo JSON
Criamos um exemplo de arquivo JSON:
{
"formMeta": {
"moduleId": 142,
"formId": 14210,
"columnsNumber": 2,
"colOrder": [
90, 101, 100,
110, 111
],
"linkageMeta": {
"90": {
"1": [90, 101, 100],
"2": [90, 110, 111]
}
},
"ruleMeta": {
"101": [
{ "trigger": "blur", "message": "Por favor insira o nome do evento", "required": true },
{ "trigger": "blur", "message": "O comprimento deve ser entre 3 e 5 caracteres", "min": 3, "max": 5 }
]
}
},
"itemMeta": {
"90": {
"columnId": 90,
"colName": "kind",
"label": "Categoria",
"controlType": 153,
"isClear": false,
"defValue": 0,
"extend": {
"placeholder": "Categoria",
"title": "Código"
},
"optionList": [
{"value": 1, "label": "Texto"},
{"value": 2, "label": "Número"}
],
"colCount": 2
},
"100": {
"columnId": 100,
"colName": "area",
"label": "Texto multilinha",
"controlType": 100,
"isClear": false,
"defValue": 1000,
"extend": {
"placeholder": "Texto multilinha",
"title": "Texto multilinha"
},
"colCount": 1
},
"101": {
"columnId": 101,
"colName": "text",
"label": "Texto",
"controlType": 101,
"isClear": false,
"defValue": "",
"extend": {
"placeholder": "Texto",
"title": "Texto"
},
"colCount": 1
},
"110": {
"columnId": 110,
"colName": "number1",
"label": "Número",
"controlType": 110,
"isClear": false,
"defValue": "",
"extend": {
"placeholder": "Número",
"title": "Número"
},
"colCount": 1
},
"111": {
"columnId": 111,
"colName": "number2",
"label": "Slider",
"controlType": 111,
"isClear": false,
"defValue": "",
"extend": {
"placeholder": "Slider",
"title": "Slider"
},
"colCount": 1
}
}
}
Encapsulamento com el-form
Com os preparativos concluídos, encapsulamos o componente el-form:
<el-form
:model="model"
ref="formControl"
:inline="false"
class="demo-form-inline"
:label-suffix="labelSuffix"
:label-width="labelWidth"
:size="size"
v-bind="$attrs"
>
<el-row :gutter="15">
<el-col
v-for="(ctrId, index) in colOrder"
:key="'form_' + ctrId + index"
:span="formColSpan[ctrId]"
v-show="showCol[ctrId]"
><!---->
<transition name="el-zoom-in-top">
<el-form-item
:label="itemMeta[ctrId].label"
:prop="itemMeta[ctrId].colName"
:rules="ruleMeta[ctrId] ?? []"
:label-width="itemMeta[ctrId].labelWidth??''"
:size="size"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[itemMeta[ctrId].controlType]"
:model="model"
v-bind="itemMeta[ctrId]"
>
</component>
</el-form-item>
</transition>
</el-col>
</el-row>
</el-form>
- Props: Propriedades definidas são vinculadas diretamente, como
:label-suffix="labelSuffix". - $attrs: Propriedades não definidas nas props são armazenadas em
$attrs, acessíveis viav-bind="$attrs". - Componente dinâmico: Utilizamos
<component>para carregar componentes filhos. - Validação: As regras são aplicadas via
:rules="ruleMeta[ctrId] ?? []".
Múltiplas Colunas
Utilizamos el-row e el-col para criar múltiplas colunas:
- Formulário de uma coluna
- Formulário de duas colunas
- Formulário de três colunas
- Formulário com múltiplas colunas até 24
- Ajuste de layout
Divisão em Seções
Para diferentes estilos de seção, usamos componentes como el-card, el-tab e el-step:
<el-form
:model="model"
ref="formControl"
:inline="false"
class="demo-form-inline"
:label-suffix="labelSuffix"
:label-width="labelWidth"
:size="size"
v-bind="$attrs"
>
<el-tabs
v-model="tabIndex"
type="border-card"
>
<el-tab-pane
v-for="(item, index) in cardOrder"
:key="'tabs_' + index"
:label="item.title"
:name="item.title"
>
<el-row :gutter="15">
<el-col
v-for="(ctrId, index) in item.colIds"
:key="'form_' + ctrId + index"
:span="formColSpan[ctrId]"
v-show="showCol[ctrId]"
>
<transition name="el-zoom-in-top">
<el-form-item
:label="itemMeta[ctrId].label"
:prop="itemMeta[ctrId].colName"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[itemMeta[ctrId].controlType]"
:model="model"
v-bind="itemMeta[ctrId]"
>
</component>
</el-form-item>
</transition>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-form>
Extensões com Slots
Apesar de componetnes pré-definidos, demandas variadas exigem extensões personalizadas. Utilizamos slots:
<nf-form
v-form-drag="formMeta"
:model="model"
:partModel="model2"
v-bind="formMeta"
>
<template v-slot:text>
<h1>Slot externo</h1>
<input v-model="model.text"/>
</template>
</nf-form>
Extensão de Componentes Filhos
Para componentes complexos, slots podem ser pouco práticos. Implementamos novos componentes que seguem a interface:
TypeScript
- Por que interfaces? Para clareza estrutural e intenção.
- JSON e TypeScript? O JSON é convertido em objetos JS em tempo de execução, então as interfaces ajudam apenas durante o desenvolvimento.