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.
- 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.
- 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
- Relación dentro de un componente Esto puede lograrse usando el-cascader.
- Relación entre múltiples componentes Puede realizarse de forma sencilla.
- Interacción entre componentes Se puede configurar si otro componente se muestra según el valor de uno específico.
- Video demostrativo Ver demostración dinámica: https://www.zhihu.com/zvideo/1378258091499208704
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
