Transformando bibliotecas UI em ferramentas de baixo código - Parte 1: Design de formulários

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 via v-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.

Código Fonte e Demonstração

Tags: vue element-plus low-code TypeScript JSON

Publicado em 10-17 03:06