npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@typhonsoftware/dyona-form

v0.0.1

Published

Componente React para representar formularios ABM dinámicos de Dyona.

Readme

Dyona Form JSON v1

Objetivo

Este documento define una primera versión del JSON que consumirá el componente de formularios dinámicos de Dyona.

La idea es contar con una estructura clara, extensible y compatible con formularios construidos en React usando Material UI (MUI). Esta versión inicial debe permitir representar formularios con título general, múltiples solapas, ayuda contextual, distribución de campos en filas y columnas, tipos de datos básicos, validaciones simples y reglas condicionales iniciales.


1. Estructura general del formulario

El formulario debería estar definido por un objeto principal que contenga los datos generales, la versión del esquema, las solapas y las acciones disponibles.

{
  "id": "persona_form",
  "version": "1.0",
  "title": "Datos de la persona",
  "description": "Formulario para registrar o editar una persona.",
  "helpUrl": "/ayuda/personas",
  "tabs": [],
  "actions": []
}

Campos recomendados a nivel formulario

| Campo | Descripción | |---|---| | id | Identificador técnico del formulario. | | version | Versión del esquema JSON. | | title | Título general visible del formulario. | | description | Descripción o texto introductorio opcional. | | helpUrl | Enlace de ayuda general del formulario. | | tabs | Lista de solapas del formulario. | | actions | Lista de acciones o botones del formulario. |


2. Solapas

Cada formulario puede tener múltiples solapas. Cada solapa debería tener su propio título, descripción opcional, link de ayuda y filas de campos.

{
  "id": "datos_generales",
  "title": "Datos generales",
  "description": "Información principal de identificación.",
  "helpUrl": "/ayuda/personas/datos-generales",
  "visible": true,
  "rows": []
}

Campos recomendados a nivel solapa

| Campo | Descripción | |---|---| | id | Identificador técnico de la solapa. | | title | Título visible de la solapa. | | description | Texto descriptivo opcional. | | helpUrl | Enlace de ayuda específico de la solapa. | | visible | Indica si la solapa se muestra o no. | | rows | Lista de filas con campos. |


3. Filas y columnas

Dentro de cada solapa se definen filas (rows). Cada fila contiene uno o más campos (fields).

Esto permite representar casos como:

  • Nombre y apellido en la misma fila.
  • Fecha de nacimiento y email en otra fila.
  • Una observación ocupando todo el ancho.
{
  "fields": [
    {
      "id": "nombre",
      "name": "nombre",
      "type": "string",
      "label": "Nombre",
      "required": true,
      "grid": {
        "xs": 12,
        "md": 6
      }
    },
    {
      "id": "apellido",
      "name": "apellido",
      "type": "string",
      "label": "Apellido",
      "required": true,
      "grid": {
        "xs": 12,
        "md": 6
      }
    }
  ]
}

Layout recomendado con MUI Grid

"grid": {
  "xs": 12,
  "sm": 12,
  "md": 6,
  "lg": 6
}

Esto permite que en pantallas chicas el campo ocupe todo el ancho, mientras que en escritorio pueda compartir fila con otro campo.


4. Campos comunes para todos los atributos

Cada campo debería tener un conjunto base de propiedades comunes, independientemente del tipo de dato.

| Campo | Descripción | |---|---| | id | Identificador único del campo dentro del formulario. | | name | Nombre del dato que se enviará o guardará. | | type | Tipo de campo. | | label | Etiqueta visible para el usuario. | | required | Indica si el campo es obligatorio. | | tooltip | Texto de ayuda contextual. | | placeholder | Texto sugerido dentro del input. | | helperText | Texto auxiliar debajo del campo. | | defaultValue | Valor inicial. | | disabled | Campo deshabilitado. | | readOnly | Campo solo lectura. | | visible | Campo visible o no. | | fullWidth | Indica si ocupa todo el ancho disponible. | | grid | Configuración responsive de columnas MUI. | | validationMessage | Mensaje personalizado de validación. |

Ejemplo:

{
  "id": "email",
  "name": "email",
  "type": "email",
  "label": "Correo electrónico",
  "required": true,
  "tooltip": "Ingresá un correo válido de contacto.",
  "placeholder": "[email protected]",
  "helperText": "Este correo se usará para notificaciones.",
  "defaultValue": "",
  "disabled": false,
  "readOnly": false,
  "visible": true,
  "fullWidth": true,
  "grid": {
    "xs": 12,
    "md": 6
  },
  "validationMessage": "Debe ingresar un correo electrónico válido."
}

5. Tipos de datos propuestos

Para mantener consistencia con TypeScript y facilitar el renderizado, se recomienda usar nombres técnicos simples en minúscula.

type FieldType =
  | 'string'
  | 'date'
  | 'number'
  | 'textarea'
  | 'email'
  | 'boolean'
  | 'multiOption'
  | 'dropdown';

Tipos iniciales

| Tipo | Uso | |---|---| | string | Campo de texto simple. | | date | Campo de fecha. | | number | Campo numérico. | | textarea | Campo largo o nota. | | email | Campo de correo electrónico. | | boolean | Campo sí/no. | | multiOption | Selector múltiple de opciones. | | dropdown | Selector de una única opción. |


6. Validaciones para campos string

Los campos de tipo string deberían permitir validaciones de longitud y patrón.

{
  "type": "string",
  "minLength": 2,
  "maxLength": 80,
  "trim": true
}

Propiedades recomendadas

| Campo | Descripción | |---|---| | minLength | Cantidad mínima de caracteres. | | maxLength | Cantidad máxima de caracteres. | | trim | Quitar espacios al inicio y final. | | uppercase | Convertir el valor a mayúsculas. | | pattern | Expresión regular opcional. | | mask | Máscara visual opcional. |

Ejemplo:

{
  "id": "cuit",
  "name": "cuit",
  "type": "string",
  "label": "CUIT",
  "required": true,
  "minLength": 11,
  "maxLength": 11,
  "pattern": "^[0-9]{11}$",
  "validationMessage": "El CUIT debe tener 11 números."
}

7. Validaciones para campos number

Los campos numéricos deberían permitir límites, tipo de número, decimales y formato visual.

{
  "type": "number",
  "numberType": "decimal",
  "min": 0,
  "max": 100,
  "decimals": 2,
  "step": 0.01
}

Propiedades recomendadas

| Campo | Descripción | |---|---| | min | Valor mínimo permitido. | | max | Valor máximo permitido. | | numberType | integer o decimal. | | decimals | Cantidad de decimales. | | step | Incremento permitido. | | prefix | Prefijo visual, por ejemplo $. | | suffix | Sufijo visual, por ejemplo %, kg, mts. |

Ejemplo:

{
  "id": "porcentaje_descuento",
  "name": "porcentajeDescuento",
  "type": "number",
  "label": "Descuento",
  "required": false,
  "numberType": "decimal",
  "min": 0,
  "max": 100,
  "decimals": 2,
  "suffix": "%"
}

8. Validaciones para campos date

Los campos de tipo fecha deberían permitir fecha mínima, máxima y restricciones relativas.

{
  "type": "date",
  "minDate": "1900-01-01",
  "maxDate": "today"
}

Propiedades recomendadas

| Campo | Descripción | |---|---| | minDate | Fecha mínima permitida. | | maxDate | Fecha máxima permitida. | | disablePast | No permite fechas anteriores al día actual. | | disableFuture | No permite fechas posteriores al día actual. | | format | Formato visual de fecha. |

Ejemplo:

{
  "id": "fecha_nacimiento",
  "name": "fechaNacimiento",
  "type": "date",
  "label": "Fecha de nacimiento",
  "required": true,
  "maxDate": "today",
  "grid": {
    "xs": 12,
    "md": 6
  }
}

9. Campo textarea o nota

El tipo textarea representa un campo de texto largo, observación o nota.

{
  "type": "textarea",
  "minLength": 0,
  "maxLength": 500,
  "rows": 4
}

Propiedades recomendadas

| Campo | Descripción | |---|---| | minLength | Cantidad mínima de caracteres. | | maxLength | Cantidad máxima de caracteres. | | rows | Cantidad inicial de filas visibles. | | autoGrow | Indica si el campo crece automáticamente. |


10. Campo email

El tipo email debería validar automáticamente el formato básico de correo electrónico.

{
  "type": "email",
  "allowMultiple": false
}

Propiedades posibles

| Campo | Descripción | |---|---| | allowMultiple | Permite ingresar más de un email. | | allowedDomains | Dominios permitidos. | | blockedDomains | Dominios bloqueados. |

Para la versión 1, alcanza con validar formato de email y definir si se permite uno o varios correos.


11. Campo boolean

El tipo boolean representa valores sí/no, verdadero/falso o activo/inactivo.

{
  "type": "boolean",
  "renderAs": "switch"
}

Formas de renderizado posibles

| Valor | Descripción | |---|---| | switch | Interruptor visual. | | checkbox | Checkbox simple. | | radio | Grupo de opciones sí/no. |

Ejemplo:

{
  "id": "activo",
  "name": "activo",
  "type": "boolean",
  "label": "Activo",
  "required": false,
  "defaultValue": true,
  "renderAs": "switch",
  "trueLabel": "Sí",
  "falseLabel": "No"
}

12. Campo multiOption

El tipo multiOption representa un selector múltiple de opciones.

{
  "type": "multiOption",
  "options": [
    {
      "value": "ventas",
      "label": "Ventas"
    },
    {
      "value": "compras",
      "label": "Compras"
    }
  ]
}

Propiedades recomendadas

| Campo | Descripción | |---|---| | options | Lista de opciones disponibles. | | minSelected | Cantidad mínima de opciones seleccionadas. | | maxSelected | Cantidad máxima de opciones seleccionadas. | | renderAs | Forma de renderizado: checkboxGroup, autocomplete, multiSelect. |

Ejemplo:

{
  "id": "modulos",
  "name": "modulos",
  "type": "multiOption",
  "label": "Módulos habilitados",
  "required": true,
  "renderAs": "checkboxGroup",
  "minSelected": 1,
  "options": [
    {
      "value": "ventas",
      "label": "Ventas"
    },
    {
      "value": "stock",
      "label": "Stock"
    },
    {
      "value": "compras",
      "label": "Compras"
    }
  ]
}

13. Campo dropdown

El tipo dropdown representa un selector de una única opción.

{
  "type": "dropdown",
  "options": [
    {
      "value": "dni",
      "label": "DNI"
    },
    {
      "value": "cuit",
      "label": "CUIT"
    }
  ]
}

Propiedades recomendadas

| Campo | Descripción | |---|---| | options | Lista de opciones. | | emptyOption | Permite opción vacía. | | emptyLabel | Texto para la opción vacía. | | searchable | Indica si permite búsqueda. | | renderAs | Forma de renderizado: select o autocomplete. |

Ejemplo:

{
  "id": "tipo_documento",
  "name": "tipoDocumento",
  "type": "dropdown",
  "label": "Tipo de documento",
  "required": true,
  "emptyOption": true,
  "emptyLabel": "Seleccione un tipo",
  "options": [
    {
      "value": "dni",
      "label": "DNI"
    },
    {
      "value": "cuit",
      "label": "CUIT"
    }
  ]
}

14. Opciones

Las opciones para dropdown y multiOption deberían definirse como objetos, no como strings simples.

{
  "value": "A",
  "label": "Cliente A",
  "disabled": false,
  "tooltip": "Cliente activo"
}

Campos recomendados para opciones

| Campo | Descripción | |---|---| | value | Valor técnico de la opción. | | label | Texto visible para el usuario. | | disabled | Indica si la opción está deshabilitada. | | tooltip | Ayuda contextual de la opción. |


15. Origen de datos para opciones

Las opciones pueden venir definidas en el propio JSON o cargarse desde una API.

Opciones estáticas

{
  "optionsSource": "static",
  "options": [
    {
      "value": "fisica",
      "label": "Persona física"
    },
    {
      "value": "juridica",
      "label": "Persona jurídica"
    }
  ]
}

Opciones remotas

{
  "optionsSource": "remote",
  "optionsUrl": "/api/catalogos/paises",
  "optionValueField": "id",
  "optionLabelField": "descripcion"
}

Ejemplo completo:

{
  "id": "pais",
  "name": "pais",
  "type": "dropdown",
  "label": "País",
  "required": true,
  "optionsSource": "remote",
  "optionsUrl": "/api/catalogos/paises",
  "optionValueField": "id",
  "optionLabelField": "descripcion"
}

16. Reglas condicionales básicas

Para la versión 1 conviene permitir reglas simples de visibilidad y obligatoriedad.

Visibilidad condicional

{
  "visibleWhen": {
    "field": "tiene_hijos",
    "operator": "equals",
    "value": true
  }
}

Ejemplo:

{
  "id": "cantidad_hijos",
  "name": "cantidadHijos",
  "type": "number",
  "label": "Cantidad de hijos",
  "required": false,
  "numberType": "integer",
  "min": 0,
  "visibleWhen": {
    "field": "tiene_hijos",
    "operator": "equals",
    "value": true
  }
}

Requerido condicional

{
  "requiredWhen": {
    "field": "tipo_persona",
    "operator": "equals",
    "value": "juridica"
  }
}

Ejemplo:

{
  "id": "razon_social",
  "name": "razonSocial",
  "type": "string",
  "label": "Razón social",
  "required": false,
  "requiredWhen": {
    "field": "tipo_persona",
    "operator": "equals",
    "value": "juridica"
  }
}

Operadores sugeridos para v1

| Operador | Uso | |---|---| | equals | Igual a un valor. | | notEquals | Distinto de un valor. | | greaterThan | Mayor que un valor. | | lessThan | Menor que un valor. | | in | El valor está dentro de una lista. | | notIn | El valor no está dentro de una lista. |


17. Acciones del formulario

Aunque el objetivo principal sea renderizar campos, conviene que la versión 1 contemple acciones básicas.

{
  "actions": [
    {
      "id": "save",
      "label": "Guardar",
      "type": "submit",
      "variant": "contained",
      "color": "primary"
    },
    {
      "id": "cancel",
      "label": "Cancelar",
      "type": "button",
      "variant": "outlined",
      "color": "secondary"
    }
  ]
}

Campos recomendados para acciones

| Campo | Descripción | |---|---| | id | Identificador de la acción. | | label | Texto visible del botón. | | type | Tipo: submit, reset, button. | | variant | Variante MUI: contained, outlined, text. | | color | Color MUI: primary, secondary, etc. |


18. Manejo de estilos

DyonaForm debe estar preparado para integrarse visualmente con el proyecto que lo consume.

La recomendación principal es que el componente herede el tema de Material UI definido por la aplicación contenedora mediante ThemeProvider. Esto permite que el formulario utilice automáticamente la paleta de colores, tipografías, tamaños, espaciados y variantes visuales del sistema principal.

Por este motivo, DyonaForm no debería definir colores rígidos o estilos visuales demasiado específicos dentro del componente. En su lugar, debe utilizar tokens del theme de MUI, por ejemplo:

color: 'text.primary'
backgroundColor: 'background.paper'
borderColor: 'divider'
color: 'primary.main'

De esta manera, si el proyecto consumidor cambia su theme, DyonaForm se adapta automáticamente sin necesidad de modificar la librería.

Estilos base del componente

DyonaForm puede incluir estilos base mínimos para asegurar una presentación ordenada:

  • Espaciado interno del formulario.
  • Separación entre título, solapas, campos y acciones.
  • Distribución responsive de filas y columnas.
  • Ubicación de los botones de acción.
  • Uso de colores del theme para textos, divisores e indicadores.

Estos estilos base deben ser neutrales y respetar siempre el theme de MUI.

Ejemplo:

<Paper
  elevation={0}
  sx={{
    p: 3,
    bgcolor: 'background.paper',
    color: 'text.primary',
  }}
>

Personalización desde el proyecto consumidor

Además de heredar el theme, DyonaForm puede permitir que el proyecto consumidor sobrescriba estilos puntuales mediante una propiedad styles.

Ejemplo de uso:

<DyonaForm
  schema={schema}
  initialValues={values}
  onSubmit={handleSubmit}
  styles={{
    title: {
      color: 'primary.main',
      fontWeight: 600,
      textAlign: 'center',
    },
    actions: {
      justifyContent: 'flex-end',
    },
    submitButton: {
      minWidth: 180,
    },
  }}
/>

Estructura sugerida para styles

type DyonaFormStyles = {
  root?: SxProps<Theme>;
  header?: SxProps<Theme>;
  title?: SxProps<Theme>;
  tabsContainer?: SxProps<Theme>;
  helpContainer?: SxProps<Theme>;
  rowsContainer?: SxProps<Theme>;
  row?: SxProps<Theme>;
  actions?: SxProps<Theme>;
  submitButton?: SxProps<Theme>;
};

Regla recomendada

La prioridad de estilos debería ser:

Theme del proyecto consumidor
↓
Estilos base mínimos de DyonaForm
↓
Overrides recibidos por props

Esto permite que DyonaForm sea reutilizable, consistente con la identidad visual del sistema principal y flexible para casos particulares.

Recomendación para la versión 1

Para la versión inicial se recomienda:

  • Heredar siempre el theme de MUI.
  • Evitar colores hardcodeados.
  • Usar tokens como text.primary, background.paper, divider y primary.main.
  • Permitir personalización mediante una prop styles.
  • No incluir estilos dentro del JSON del formulario en esta primera versión.

El JSON debe describir la estructura y comportamiento del formulario. La apariencia visual debe quedar principalmente en el theme del proyecto consumidor y, cuando sea necesario, en overrides controlados por props.

19. Ejemplo completo de JSON v1

{
  "id": "persona_form",
  "version": "1.0",
  "title": "Datos de la persona",
  "description": "Formulario para registrar o editar una persona.",
  "helpUrl": "/ayuda/personas",
  "tabs": [
    {
      "id": "datos_generales",
      "title": "Datos generales",
      "description": "Información principal de identificación.",
      "helpUrl": "/ayuda/personas/datos-generales",
      "visible": true,
      "rows": [
        {
          "fields": [
            {
              "id": "nombre",
              "name": "nombre",
              "type": "string",
              "label": "Nombre",
              "required": true,
              "tooltip": "Ingrese el nombre de la persona.",
              "placeholder": "Ej: Juan",
              "minLength": 2,
              "maxLength": 80,
              "trim": true,
              "fullWidth": true,
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "apellido",
              "name": "apellido",
              "type": "string",
              "label": "Apellido",
              "required": true,
              "tooltip": "Ingrese el apellido de la persona.",
              "placeholder": "Ej: Pérez",
              "minLength": 2,
              "maxLength": 80,
              "trim": true,
              "fullWidth": true,
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        },
        {
          "fields": [
            {
              "id": "fecha_nacimiento",
              "name": "fechaNacimiento",
              "type": "date",
              "label": "Fecha de nacimiento",
              "required": false,
              "maxDate": "today",
              "fullWidth": true,
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "email",
              "name": "email",
              "type": "email",
              "label": "Email",
              "required": true,
              "placeholder": "[email protected]",
              "fullWidth": true,
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        }
      ]
    },
    {
      "id": "configuracion",
      "title": "Configuración",
      "helpUrl": "/ayuda/personas/configuracion",
      "visible": true,
      "rows": [
        {
          "fields": [
            {
              "id": "activo",
              "name": "activo",
              "type": "boolean",
              "label": "Activo",
              "required": false,
              "defaultValue": true,
              "renderAs": "switch",
              "trueLabel": "Sí",
              "falseLabel": "No",
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "tipo_persona",
              "name": "tipoPersona",
              "type": "dropdown",
              "label": "Tipo de persona",
              "required": true,
              "emptyOption": true,
              "emptyLabel": "Seleccione un tipo",
              "optionsSource": "static",
              "options": [
                {
                  "value": "fisica",
                  "label": "Persona física"
                },
                {
                  "value": "juridica",
                  "label": "Persona jurídica"
                }
              ],
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        },
        {
          "fields": [
            {
              "id": "razon_social",
              "name": "razonSocial",
              "type": "string",
              "label": "Razón social",
              "required": false,
              "requiredWhen": {
                "field": "tipo_persona",
                "operator": "equals",
                "value": "juridica"
              },
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "observaciones",
              "name": "observaciones",
              "type": "textarea",
              "label": "Observaciones",
              "required": false,
              "maxLength": 500,
              "rows": 4,
              "fullWidth": true,
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        }
      ]
    }
  ],
  "actions": [
    {
      "id": "save",
      "label": "Guardar",
      "type": "submit",
      "variant": "contained",
      "color": "primary"
    },
    {
      "id": "cancel",
      "label": "Cancelar",
      "type": "button",
      "variant": "outlined",
      "color": "secondary"
    }
  ]
}

20. Recomendación de alcance para la versión 1

Para la versión 1 se recomienda incluir:

A nivel formulario

  • id
  • version
  • title
  • description
  • helpUrl
  • tabs
  • actions

A nivel solapa

  • id
  • title
  • description
  • helpUrl
  • visible
  • rows

A nivel fila

  • fields

A nivel campo

  • id
  • name
  • type
  • label
  • required
  • tooltip
  • placeholder
  • helperText
  • defaultValue
  • disabled
  • readOnly
  • visible
  • fullWidth
  • grid
  • validationMessage

Validaciones

  • minLength
  • maxLength
  • pattern
  • min
  • max
  • numberType
  • decimals
  • minDate
  • maxDate

Opciones

  • optionsSource
  • options
  • optionsUrl
  • optionValueField
  • optionLabelField
  • emptyOption
  • emptyLabel

Comportamiento básico

  • visibleWhen
  • requiredWhen

21. Funcionalidades que se recomienda dejar para versiones futuras

Para evitar sobredimensionar la versión inicial, se recomienda dejar fuera de la v1:

  • Cálculos entre campos.
  • Validaciones complejas con fórmulas.
  • Reglas de negocio avanzadas.
  • Workflows.
  • Eventos custom como onChange, onBlur o onSubmit.
  • Permisos por usuario o rol.
  • Dependencias remotas complejas entre combos.
  • Lógica de negocio pesada embebida en el JSON.

22. Conclusión

La versión 1 del JSON debe enfocarse en permitir construir formularios dinámicos, ordenados y extensibles.

El esquema propuesto permite:

  • Formularios con título general.
  • Múltiples solapas.
  • Links de ayuda por formulario y por solapa.
  • Campos organizados en filas y columnas.
  • Layout responsive compatible con MUI Grid.
  • Tipos de datos básicos.
  • Validaciones simples.
  • Selectores con opciones fijas o remotas.
  • Reglas condicionales iniciales.
  • Acciones básicas del formulario.

Con esta base, Dyona puede empezar a renderizar formularios dinámicos de manera consistente, manteniendo abierto el camino para incorporar reglas más avanzadas en futuras versiones.

23. Formularios con datos cargados para edición

Además de definir la estructura visual del formulario, el JSON debe poder representar un formulario con datos ya cargados.

Esto es necesario para casos como:

  • Editar un objeto existente.
  • Consultar un objeto en modo solo lectura.
  • Duplicar un registro.
  • Cargar un formulario desde una plantilla.
  • Precargar datos obtenidos desde una API.

Para resolverlo de manera ordenada, se recomienda separar claramente:

formulario = estructura
record = datos cargados
submit = forma de guardar

La definición del formulario no debería mezclarse con los valores actuales del objeto.


24. Modo del formulario

A nivel raíz se recomienda agregar la propiedad mode.

{
  "mode": "edit"
}

Modos posibles

| Modo | Descripción | |---|---| | create | Alta de un nuevo objeto. | | edit | Edición de un objeto existente. | | view | Consulta de un objeto sin permitir edición. | | duplicate | Alta de un nuevo objeto partiendo de datos ya existentes. |

Ejemplo:

{
  "id": "persona_form",
  "version": "1.0",
  "mode": "edit",
  "title": "Editar persona"
}

25. Datos cargados del objeto

Para enviar un formulario con datos cargados se recomienda agregar una propiedad record.

{
  "record": {
    "id": "PER-0001",
    "nombre": "Sergio",
    "apellido": "Cuba",
    "fechaNacimiento": "1985-06-15",
    "email": "[email protected]",
    "activo": true,
    "tipoPersona": "fisica",
    "observaciones": "Cliente cargado desde el padrón inicial."
  }
}

Cada campo del formulario toma su valor usando la propiedad name.

Campo definido en el formulario:

{
  "id": "nombre",
  "name": "nombre",
  "type": "string",
  "label": "Nombre"
}

Dato cargado en el objeto:

{
  "nombre": "Sergio"
}

El renderizador debería resolver el valor usando una lógica similar a:

const value =
  record?.[field.name] ??
  field.defaultValue ??
  getEmptyValueByType(field.type);

26. Diferencia entre defaultValue y record

Es importante distinguir entre defaultValue y record.

defaultValue

Representa el valor inicial sugerido para un campo cuando el formulario se usa para crear un nuevo objeto.

Ejemplo:

{
  "id": "activo",
  "name": "activo",
  "type": "boolean",
  "label": "Activo",
  "defaultValue": true
}

Esto indica que, al crear un nuevo registro, el campo activo debería iniciar en true.

record

Representa los valores reales del objeto existente que se está editando o consultando.

Ejemplo:

{
  "record": {
    "activo": false
  }
}

Esto indica que el objeto existente actualmente tiene activo en false.

Regla recomendada

Al renderizar el formulario:

  1. Si existe record[field.name], se usa ese valor.
  2. Si no existe valor en record, se usa field.defaultValue.
  3. Si no existe ninguno de los dos, se usa el valor vacío correspondiente al tipo de campo.

Ejemplos de valores vacíos por tipo:

| Tipo | Valor vacío sugerido | |---|---| | string | "" | | textarea | "" | | email | "" | | number | null | | date | null | | boolean | false | | dropdown | null | | multiOption | [] |


27. Metadata de la entidad editada

Para que el formulario tenga contexto del objeto que está editando, se recomienda agregar una propiedad entity.

{
  "entity": {
    "name": "persona",
    "idField": "id",
    "idValue": "PER-0001",
    "endpoint": "/api/personas/PER-0001"
  }
}

Campos recomendados para entity

| Campo | Descripción | |---|---| | name | Nombre técnico de la entidad. | | idField | Nombre del campo identificador. | | idValue | Valor del identificador del registro actual. | | endpoint | Endpoint asociado al objeto actual. |

Esto ayuda al frontend a conocer qué objeto está manipulando y cómo guardar los cambios.


28. Configuración de guardado

Además de las acciones visuales del formulario, conviene agregar una sección submit.

{
  "submit": {
    "method": "PUT",
    "url": "/api/personas/PER-0001",
    "payloadMode": "full"
  }
}

Para alta de objetos:

{
  "submit": {
    "method": "POST",
    "url": "/api/personas",
    "payloadMode": "full"
  }
}

Para edición de objetos:

{
  "submit": {
    "method": "PUT",
    "url": "/api/personas/PER-0001",
    "payloadMode": "full"
  }
}

Campos recomendados para submit

| Campo | Descripción | |---|---| | method | Método HTTP: POST, PUT o PATCH. | | url | Endpoint de guardado. | | payloadMode | Define si se envía todo el objeto o solo los cambios. |

Modos de payload

| Modo | Descripción | |---|---| | full | Envía todo el objeto completo. | | changesOnly | Envía solo los campos modificados. |

Para la versión 1 se recomienda usar full, porque es más simple de implementar y depurar.


29. Envío completo del objeto

En modo edit, al guardar se puede enviar el objeto completo actualizado.

Ejemplo:

{
  "id": "PER-0001",
  "nombre": "Sergio",
  "apellido": "Cuba",
  "fechaNacimiento": "1985-06-15",
  "email": "[email protected]",
  "activo": true,
  "tipoPersona": "fisica",
  "observaciones": "Cliente actualizado."
}

Esta opción es recomendable para la versión 1 por ser directa, clara y fácil de validar desde el backend.


30. Envío solo de campos modificados

En una versión posterior se podría permitir enviar solo los cambios.

Ejemplo:

{
  "id": "PER-0001",
  "changes": {
    "observaciones": "Cliente actualizado."
  }
}

Este enfoque requiere que el frontend mantenga el estado inicial y compare contra el estado actual del formulario.

Para una versión inicial, se recomienda dejarlo documentado pero no implementarlo como prioridad.


31. Datos anidados

Para objetos más complejos, la propiedad name podría representar paths.

Ejemplo de campo:

{
  "id": "nombre",
  "name": "persona.nombre",
  "type": "string",
  "label": "Nombre"
}

Ejemplo de record:

{
  "persona": {
    "nombre": "Sergio",
    "apellido": "Cuba"
  }
}

Otro ejemplo:

{
  "id": "calle",
  "name": "domicilio.calle",
  "type": "string",
  "label": "Calle"
}

Con un record como:

{
  "domicilio": {
    "calle": "Av. Siempre Viva",
    "numero": "742"
  }
}

Para la versión 1 se puede comenzar con objetos planos, pero conviene dejar preparado el concepto de name como posible path.


32. Ejemplo completo de formulario en modo edición

{
  "id": "persona_form",
  "version": "1.0",
  "mode": "edit",
  "title": "Editar persona",
  "description": "Formulario para editar los datos principales de una persona.",
  "helpUrl": "/ayuda/personas",
  "entity": {
    "name": "persona",
    "idField": "id",
    "idValue": "PER-0001",
    "endpoint": "/api/personas/PER-0001"
  },
  "record": {
    "id": "PER-0001",
    "nombre": "Sergio",
    "apellido": "Cuba",
    "fechaNacimiento": "1985-06-15",
    "email": "[email protected]",
    "activo": true,
    "tipoPersona": "fisica",
    "observaciones": "Cliente cargado desde el padrón inicial."
  },
  "tabs": [
    {
      "id": "datos_generales",
      "title": "Datos generales",
      "description": "Información principal de identificación.",
      "helpUrl": "/ayuda/personas/datos-generales",
      "visible": true,
      "rows": [
        {
          "fields": [
            {
              "id": "nombre",
              "name": "nombre",
              "type": "string",
              "label": "Nombre",
              "required": true,
              "minLength": 2,
              "maxLength": 80,
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "apellido",
              "name": "apellido",
              "type": "string",
              "label": "Apellido",
              "required": true,
              "minLength": 2,
              "maxLength": 80,
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        },
        {
          "fields": [
            {
              "id": "fecha_nacimiento",
              "name": "fechaNacimiento",
              "type": "date",
              "label": "Fecha de nacimiento",
              "required": false,
              "maxDate": "today",
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "email",
              "name": "email",
              "type": "email",
              "label": "Email",
              "required": true,
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        }
      ]
    },
    {
      "id": "configuracion",
      "title": "Configuración",
      "helpUrl": "/ayuda/personas/configuracion",
      "visible": true,
      "rows": [
        {
          "fields": [
            {
              "id": "activo",
              "name": "activo",
              "type": "boolean",
              "label": "Activo",
              "required": false,
              "renderAs": "switch",
              "trueLabel": "Sí",
              "falseLabel": "No",
              "grid": {
                "xs": 12,
                "md": 6
              }
            },
            {
              "id": "tipo_persona",
              "name": "tipoPersona",
              "type": "dropdown",
              "label": "Tipo de persona",
              "required": true,
              "optionsSource": "static",
              "options": [
                {
                  "value": "fisica",
                  "label": "Persona física"
                },
                {
                  "value": "juridica",
                  "label": "Persona jurídica"
                }
              ],
              "grid": {
                "xs": 12,
                "md": 6
              }
            }
          ]
        },
        {
          "fields": [
            {
              "id": "observaciones",
              "name": "observaciones",
              "type": "textarea",
              "label": "Observaciones",
              "required": false,
              "maxLength": 500,
              "rows": 4,
              "grid": {
                "xs": 12,
                "md": 12
              }
            }
          ]
        }
      ]
    }
  ],
  "actions": [
    {
      "id": "save",
      "label": "Guardar cambios",
      "type": "submit",
      "variant": "contained",
      "color": "primary"
    },
    {
      "id": "cancel",
      "label": "Cancelar",
      "type": "button",
      "variant": "outlined",
      "color": "secondary"
    }
  ],
  "submit": {
    "method": "PUT",
    "url": "/api/personas/PER-0001",
    "payloadMode": "full"
  }
}

33. Estructura raíz recomendada para formularios editables

La estructura conceptual completa de la versión 1 debería quedar así:

{
  "id": "persona_form",
  "version": "1.0",
  "mode": "edit",
  "title": "Editar persona",
  "description": "Formulario para editar una persona.",
  "helpUrl": "/ayuda/personas",
  "entity": {},
  "record": {},
  "tabs": [],
  "actions": [],
  "submit": {}
}

Campos adicionales recomendados para soportar edición

| Campo | Descripción | |---|---| | mode | Modo del formulario: alta, edición, consulta o duplicación. | | entity | Metadata del objeto que se está manipulando. | | record | Datos cargados del objeto. | | submit | Configuración para guardar el formulario. |


34. Recomendación para la versión 1 editable

Para soportar formularios con datos cargados, se recomienda sumar a la versión 1:

mode
entity
record
submit

Y mantener como regla principal:

La estructura del formulario no se mezcla con los valores del objeto.

Esto permite usar el mismo formulario para distintos escenarios:

  • Alta.
  • Edición.
  • Consulta.
  • Duplicación.
  • Precarga desde plantilla.
  • Precarga desde objeto remoto.

35. Conclusión ampliada

Con esta extensión, el JSON de formularios de Dyona no solamente describe cómo se debe dibujar un formulario, sino también cómo se comporta frente a un objeto existente.

La separación entre tabs, rows y fields por un lado, y record, entity y submit por el otro, permite mantener el diseño limpio y reusable.

Esta separación es importante porque el mismo formulario puede ser utilizado para crear, editar o consultar registros sin duplicar la definición de campos.

Para la primera ver