Componente de formulario dinámico basado en JSON usando el-form

Funcionalidades del componente nf-form

Se ha desarrollado un componente de formulario basado en el-form, incluyendo sus controles secundarios. Dado que se trata de una encapsulación, es necesario implementar todas las funciones posibles para evitar dejar nada por hacer. Después de todo, las funcionalidades proporcionadas por las bibliotecas UI son muy poderosas, no se debe desperdiciar.

  • Creación dinámica del formulario basada en JSON
  • Soporte para múltiples filas y columnas
  • Personalización del diseño
  • Posibilidad de definir controles personalizados (slots y componentes dinámicos)
  • Extensión de controles secundarios del formulario
  • Validación de datos
  • Relaciones entre datos
  • Interacción entre componentes
  • Generación automática del modelo a partir de JSON

Demostración de funcionalidades

Antes de mostrar el código, veamos cómo funciona.

  • Formulario de una sola columna Este es básico, simplemente se muestra la imagen.
  • Formulario de múltiples columnas A veces se requiere un formulario con dos o tres columnas, lo cual también debe ser soportado.

Dado que se utiliza el-col para implementar múltiples columnas, teóricamente se pueden soportar hasta 24 columnas, aunque esto dependerá del ancho de la pantalla.

  • Ajuste del diseño Al observar las imágenes anteriores, se puede notar un problema: al cambiar el número de columnas, la apariencia del formulario se vuelve menos atractiva. En este caso, necesitamos realizar ajustes, como hacer que cierto componente ocupe dos espacios o reorganizar el orden de los componentes.

【Combinación en una sola columna】

Tras el ajuste, la página puede volverse más compacta. Dos componentes pueden ocupar una fila, o tres componentes pueden hacerlo, dependiendo del ancho de la pantalla y el tamaño de cada componente.

【Ocupando toda una fila en múltiples columnas】

  • Controles personalizados Si los controles proporcionados por el formulario no satisfacen las necesidades, ¿qué hacer? Se puede crear uno personalizado.
  1. Uso de slots El uso de slots es simple y flexible, permite control completo desde fuera del componente de formulario, ideal para situaciones puntuales. Dentro de un slot pueden colocarse múltiples componentes.
  2. Componentes dinámicos Aunque los slots son flexibles, no son convenientes para reutilización. Si se necesita usar en varios lugares, primero se puede crear un componente separado y luego integrarlo mediante componentes dinámicos.

Aquí se usa un componente dinámico para integrar el componente de transferecnia de Element, también se pueden agregar otros tipos de componentes.

  • Validación de datos Se puede utilizar directamente la función de validación de el-form, configurando las reglas dentro del JSON.
  • Relación entre datos
  1. Relación dentro de un componente Esto puede lograrse usando el-cascader.
  2. Relación entre múltiples componentes Puede realizarse de forma sencilla.

Encapsulamiento de controles secundarios del formulario

El formulario necesita muchos controles secundarios, por lo tanto, primero se deben encapsular estos elementos antes de poder construir el componente principal.

Definición de interfaces para estandarización

Los controles secundarios del formulario comparten una necesidad común: deben permitir intercambio de propiedades y datos v-model. Como Element encapsula el valor en v-model, no se puede vincular directamente a las propiedades del componente, por lo tanto se requiere una variable interna para esta vincualción. Se implementa una conversión mediante un ref personalizado, además de añadir funcionalidad de debounce.

Aunque el componente formulario no necesita debounce en sí mismo, sí es útil en consultas donde se requiere esta funcionalidad, y puede ser reutilizado en componentes de búsqueda.

Definición de v-model y my-change

// Ref personalizado 
/**
 * Ref personalizado para convertir entre propiedades y variables internas
 * @param { reactive } props Propiedades del componente
 * @param { object } context Contexto del componente
 * @param { number } delay Tiempo de espera para actualización, en milisegundos, por defecto: 0
 * @param { string } name Nombre de la propiedad correspondiente, por defecto: modelValue
 * @returns Ref personalizado
 */
export const debounceRef = (props, context, delay = 0, name = 'modelValue') => {
  let _value = props[name]

  // Temporizador
  let timeout
  // Estado de entrada. Durante entrada se usa value; al terminar se usa modelValue
  let isInput = false
  return customRef((track, trigger) => {
    return {
      get () {
        track()
        if (isInput) {
          // console.log(isInput)
          return _value
        } else {
          // console.log(isInput)
          return props[name]
        }
      },
      set (newValue) {
        isInput = true
        _value = newValue // Valor vinculado
        trigger() // Actualización interna del template
        clearTimeout(timeout) // Cancelar temporizador anterior
        timeout = setTimeout(() => {
          // Actualizar propiedad modelValue
          context.emit(`update:${name}`, newValue) // Enviar al padre
          // Para identificar qué componente activó el evento.
          context.emit('my-change', newValue, props.controlId, props.colName)
          isInput = false
        }, delay)
      }
    }
  })
}


Encapsulamiento de diversos controles de formulario

Siguiendo el principio de atomicidad, los controles están encapsulados con gran detalle, se muestran las imágenes:

El código es extenso, no se describirán línea por línea, interesados pueden revisar el código fuente.

Encapsulamiento del componente de formulario

Con el trabajo base realizado, ahora podemos encapsular el el-form.

Definición de propiedades

Basándonos en las propiedades de el-form, se definen algunas claves:

Descripción de propiedades
/**
 * Propiedades requeridas por el componente de formulario
 */
export const formProps = {
  modelValue: Object, // Modelo completo
  partModel: Object, // Modelo filtrado por opciones
  miniModel: Object, // Modelo reducido
  /*
  * Componentes personalizados key:value
  * * key: ID. 1: slot; 100-200: IDs reservados
  * * value: string: etiqueta; función: componente asíncrono, similar a rutas
  */
  customerControl: { // Componentes secundarios personalizados
    type: Object,
    defaule: () => {}
  },
  colOrder: { // Orden de los campos del formulario
    type: Array,
    default: () => []
  },
  formColCount: { // Número de columnas del formulario
    type: Number,
    default: 1
  },
  reload: {
    type: Boolean, // Recargar configuración, debe alternarse
    default: false
  },
  itemMeta: {
    type: Object, // Propiedades de los controles secundarios
    default: () => {}
  },
  ruleMeta: { // Información de validación
    type: Object, 
    default: () => {}
  },
  formColShow: { // Mostrar u ocultar componentes según cambios de datos
    type: Object,
    default: () => {}
  } 
}


Definición del modelo interno

Generalmente se usa un solo modelo, pero dado que hay interacción entre componentes, se creó un modelo local para obtener solo los valores visibles.

Implementación de múltiples filas/columnas y ajuste de diseño

Se utiliza el-col para lograr múltiples columnas, controlando el span para manejar varias columnas. Teóricamente, se soportan hasta 24 columnas, dependiendo del ancho de la pantalla.

/**
 * Manejo de campos que ocupan múltiples celdas
 * @param { object } props Propiedades del componente de formulario
 * @returns 
 */
const getColSpan = (props) => {
  // Determinar cuántas celdas ocupa un componente
  const formColSpan = reactive({})
  
  // Propiedades de los controles secundarios del formulario
  const formItemProps = props.itemMeta

  // Establecer formColSpan según colCount
  const setFormColSpan = () => {
    const formColCount = props.formColCount // Número de columnas
    const moreColSpan = 24 / formColCount // Cantidad de partes por celda

    if (formColCount === 1) {
    // Caso de una sola columna
      for (const key in formItemProps) {
        const m = formItemProps[key]
        if (typeof m.colCount === 'undefined') {
          formColSpan[m.controlId] = moreColSpan
        } else {
          if (m.colCount >= 1) {
            // Una columna, máximo 24 celdas
            formColSpan[m.controlId] = moreColSpan
          } else if (m.colCount < 0) {
            // Compactar, 24 dividido por cantidad de partes
            formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
          }
        }
      }
    } else {
      // Múltiples columnas
      for (const key in formItemProps) {
        const m = formItemProps[key]
        if (typeof m.colCount === 'undefined') {
          formColSpan[m.controlId] = moreColSpan
        } else {
          if (m.colCount < 0 || m.colCount === 1) {
            // Múltiples columnas, compactar a una parte
            formColSpan[m.controlId] = moreColSpan
          } else if (m.colCount > 1) {
            // Múltiples columnas, partes * cantidad de celdas
            formColSpan[m.controlId] = moreColSpan * m.colCount
          }
        }
      }
    }
  }

  return {
    formColSpan,
    setFormColSpan
  }
}


Primero se calcula cuántos span ocupará una columna, dividiendo 24 por el número de columnas. Luego se determina si es una sola columna, para tratar casos donde múltiples componentes ocupan una posición, o múltiples columnas, donde un componente puede ocupar varias posiciones.

Implementación de extensibilidad

Los controles secundarios pueden variar, por lo que es necesario permitir su extensión. Gracias a la funcionalidad de componentes dinámicos de Vue, es muy fácil extender los controles.

Se usan component y componentes dinámicos para cargar los controles secundarios.

<component
    :is="formItemListKey[getCtrMeta(ctrId).controlType]"
    v-model="formModel[getCtrMeta(ctrId).colName]"
    v-bind="getCtrMeta(ctrId)"
    @my-change="myChange">
  </component>


export const formItemList = {
  // Texto defineComponent
  'el-form-text': defineAsyncComponent(() => import('./t-text.vue')),
  'el-form-area': defineAsyncComponent(() => import('./t-area.vue')),
  'el-form-url': defineAsyncComponent(() => import('./t-url.vue')),
  'el-form-password': defineAsyncComponent(() => import('./t-password.vue')),
  // Números
  'el-form-number': defineAsyncComponent(() => import('./n-number.vue')),
  'el-form-range': defineAsyncComponent(() => import('./n-range.vue')),
  // Fechas y horas
  'el-form-date': defineAsyncComponent(() => import('./d-date.vue')),
  'el-form-datetime': defineAsyncComponent(() => import('./d-datetime.vue')),
  'el-form-year': defineAsyncComponent(() => import('./d-year.vue')),
  'el-form-month': defineAsyncComponent(() => import('./d-month.vue')),
  'el-form-week': defineAsyncComponent(() => import('./d-week.vue')),
  'el-form-time-select': defineAsyncComponent(() => import('./d-time-select.vue')),
  'el-form-time-picker': defineAsyncComponent(() => import('./d-time-picker.vue')),
  // Selección y switches
  'el-form-checkbox': defineAsyncComponent(() => import('./s-checkbox.vue')),
  'el-form-switch': defineAsyncComponent(() => import('./s-switch.vue')),
  'el-form-checkboxs': defineAsyncComponent(() => import('./s-checkboxs.vue')),
  'el-form-radios': defineAsyncComponent(() => import('./s-radios.vue')),
  'el-form-select': defineAsyncComponent(() => import('./s-select.vue')),
  'el-form-selwrite': defineAsyncComponent(() => import('./s-selwrite.vue')),
  'el-form-select-cascader': defineAsyncComponent(() => import('./s-select-cascader.vue'))

}

/**
 * Diccionario de componentes dinámicos, para bucles v-for
 */
export const formItemListKey = {
  // Texto
  100: formItemList['el-form-area'], // Texto multilínea
  101: formItemList['el-form-text'], // Texto de una línea
  102: formItemList['el-form-password'], // Contraseña
  103: formItemList['el-form-text'], // Teléfono
  104: formItemList['el-form-text'], // Correo
  105: formItemList['el-form-url'], // URL
  106: formItemList['el-form-text'], // Búsqueda
  // Números
  120: formItemList['el-form-number'], // Número
  121: formItemList['el-form-range'], // Deslizador
  // Fechas y horas
  110: formItemList['el-form-date'], // Fecha
  111: formItemList['el-form-datetime'], // Fecha + Hora
  112: formItemList['el-form-month'], // Año-Mes
  113: formItemList['el-form-week'], // Año-Semana
  114: formItemList['el-form-year'], // Año
  115: formItemList['el-form-time-picker'], // Hora específica
  116: formItemList['el-form-time-select'], // Selección fija de hora
  // Selección y switches
  150: formItemList['el-form-checkbox'], // Checkbox
  151: formItemList['el-form-switch'], // Switch
  152: formItemList['el-form-checkboxs'], // Grupo de checkboxes
  153: formItemList['el-form-radios'], // Grupo de radios
  160: formItemList['el-form-select'], // Selector
  161: formItemList['el-form-selwrite'], // Selector múltiple
  162: formItemList['el-form-select-cascader'] // Selector en cascada
}


Para extender controles, solo se agrega el componente al diccionario y se asigna un nuevo ID.

  // Agregar componente dinámico temporal
  formProps.customerControl = {
    300: 'el-transfer'
  }
  // Configurar campo del formulario
  childMeta.select.controlType = 300


¿Por qué usar IDs? Aunque no son legibles, son estables y flexibles. Si se quiere desarrollar para Ant Design Vue, se pueden usar IDs directamente, pero con nombres sería necesario distinguir entre prefijos como el- y a-.

Implementación de relaciones de datos

Las relaciones se dividen en datos y componentes. Las relaciones de datos pueden implementarse usando componentes de UI o la reactividad de Vue. Por ejemplo, la relación provincia-ciudad-distrito puede usarse con el-cascader. Si se requieren múltiples componentes, se puede observar el cambio de valor para actualizar las opciones de los siguientes componentes.

// Relación de datos
  watch (() => model.provinces, (v1, v2) => {
    console.log('Observando cambios', v1)
    const arr = [
      {"value": 1 + v1, "label": "Opción 1" + v1},
      {"value": 2 + v1, "label": "Opción 2" + v1}
    ]
  
    childMeta.city.optionList.length = 0
    childMeta.city.optionList.push(...arr)
  })


Vue es impulsado por datos, por lo tanto, las relaciones se observan directamente cambiando el valor, sin necesidad de eventos change como antes.

Implementación de interacción entre componentes

La interacción entre componentes implica que el cambio de valor de un componente afecte la visibilidad de otros.

Por ejemplo, en registro, se puede elegir entre usuario corporativo o personal. Si es corporativo, se requiere información adicional como nombre de empresa; si es personal, solo se necesita el nombre.

Entonces, los componentes visibles deben ajustarse.

Para este tipo de casos, se configura la propiedad formColShow.

    "formColShow": {
      "90": {  // ID del componente
        "1": [90, 101, 100, 102, 105],  // Componentes a mostrar según valor
        "2": [90, 120, 121],
        "3": [90, 110, 114, 112, 113, 115, 116],
        "4": [90, 150, 151, 152, 153, 160, 162]
      }
    },


Después de configurar, se hace un watch interno:

  // Observar cambios para mostrar componentes
  if (typeof props.formColShow !== 'undefined') {
    for (const key in props.formColShow) {
      const ctl = props.formColShow[key]
      const colName = props.itemMeta[key].colName
      // Observar el valor del componente
      watch(() => formModel[colName], (v1, v2) => {
        if (typeof ctl[v1] === 'undefined') {
          // No configurado, mostrar por defecto
          setFormColSort()
        } else {
          // Mostrar según configuración
          setFormColSort(ctl[v1])
          // Actualizar modelo parcial
          createPartModel(ctl[v1])
        }
      })
    }


Formato JSON

Todo el formulario se renderiza dinámicamente a partir de JSON, ¿cuál es el formato? Se divide en dos partes: las propiedades del componente de formulario y las propiedades de los controles secundarios, junto con reglas de validación.

{
  "formTest": {
    "baseProps": { // Propiedades del componente de formulario
      "formColCount": 1, // Columnas
      "colOrder": [ // IDs de componentes a mostrar
        90,  101, 102,
        110, 111, 114, 112, 113, 115, 116,
        120, 121, 100, 
        150, 151, 152, 153,
        160, 162
      ]
    },
    "formColShow": { // Información de interacción de componentes
      "90": { // Componente disparador
        "1": [90, 101, 100, 102, 105], // IDs a mostrar según valor
        "2": [90, 120, 121],
        "3": [90, 110, 114, 112, 113, 115, 116],
        "4": [90, 150, 151, 153, 152, 160, 162]
      }
    },
    "ruleMeta": { // Reglas de validación
      "101": [ // ID del control secundario
        { "trigger": "blur", "message": "Ingrese el nombre", "required": true },
        { "trigger": "blur", "message": "Longitud entre 3 y 5 caracteres", "min": 3, "max": 5 }
      ]
    },
    "itemMeta": { // Propiedades de controles secundarios
      "90": {  
        "controlId": 90,
        "colName": "kind",
        "label": "Categoría",
        "controlType": 153,
        "isClear": false,
        "defaultValue": "",
        "placeholder": "Categoría",
        "title": "ID",
        "optionList": [
          {"value": 1, "label": "Texto"},
          {"value": 2, "label": "Número"},
          {"value": 3, "label": "Fecha"},
          {"value": 4, "label": "Selección"}
        ],
        "colCount": 1
      },
      "100": {  
        "controlId": 100,
        "colName": "area",
        "label": "Texto multilínea",
        "controlType": 100,
        "isClear": false,
        "defaultValue": 1000,
        "placeholder": "Texto multilínea",
        "title": "Texto multilínea",
        "colCount": 1
      },
      ...
    }
  }
}


Recorrido de controles secundarios

Como los controles secundarios están encapsulados, solo se necesita recorrerlos:

  <el-form
    :model="formModel"
    :rules="rules"
    ref="formControl"
    :inline="false"
    class="demo-form-inline"
    label-suffix=":"
    label-width="130px"
    size="mini"
  >
    <el-row>
      <!--No recorrer filas, recorrer columnas directamente, se desplazará automáticamente.-->
      <el-col
        v-for="(ctrId, index) in formColSort"
        :key="'form_'+index"
        :span="formColSpan[ctrId]"
      ><!--:prop="getCtrMeta(ctrId).colName"-->
        <el-form-item
          :label="getCtrMeta(ctrId).label"
          :prop="getCtrMeta(ctrId).colName"
        >
          <!--Verificar si se carga slot-->
          <template v-if="getCtrMeta(ctrId).controlType === 1">
            <!--<slot :name="ctrId">Sin slot definido</slot>-->
            <slot :name="getCtrMeta(ctrId).colName">Sin slot definido</slot>
          </template>
          <!--Componente de elemento, usando componentes dinámicos-->
          <template v-else>
            <component
              :is="dictControl[getCtrMeta(ctrId).controlType]"
              v-model="formModel[getCtrMeta(ctrId).colName]"
              v-bind="getCtrMeta(ctrId)"
              @my-change="myChange">
            </component>
          </template>
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>


Por espacio, no se explican todas las partes, el resto se puede ver en el código fuente.

Repositorio

https://gitee.com/naturefw/nf-vite2-element

Natural Framework/nf-vite2-element

Etiquetas: Vue element-plus formulario-dinamico json componente-dinamico

Publicado el 9-7 04:14