Skip to content

Componentes de formulario

innoboxrr-form-elements (Vue, 6.8.0) e innoboxrr-react-form-elements (React, 3.8.0) exportan los mismos 37 componentes, con los mismos nombres. Tiene una razón: LaraPack escribe el mismo form_component del laraimport.json en los formularios de Vue y en los de React, y un nombre que faltara en uno rompería el build de ese framework. Un test de paridad en cada repositorio falla si un paquete exporta algo que el otro no.

El aspecto sale del tema de form-core. Ningún componente necesita UIkit, Tailwind ni Font Awesome.

Instalar

bash
npm i innoboxrr-form-elements innoboxrr-form-core
bash
npm i innoboxrr-react-form-elements innoboxrr-form-core
js
import 'innoboxrr-form-core/styles'
import FormElements from 'innoboxrr-form-elements'

app.use(FormElements) // registra los 37 componentes de forma global

// o importa solo los que uses
import { TextInputComponent, DrawerComponent } from 'innoboxrr-form-elements'
jsx
import 'innoboxrr-form-core/styles'
import 'innoboxrr-react-form-elements/src/css/form-elements.css'
import '@yaireo/tagify/dist/tagify.css'       // si usas TagsInputComponent
import 'react-phone-number-input/style.css'   // si usas CountrySelectInputComponent

import { TextInputComponent, DrawerComponent } from 'innoboxrr-react-form-elements'
VueReact
Dependencias parvue ^3.5.0, sortablejs ^1.14.0, lightvue (opcional)react ^19.0.0, react-dom ^19.0.0
Registro globalapp.use(FormElements)No existe: React no tiene plugin de aplicación
Estilos propios de los componentesEn el <style scoped> de cada unoEn src/css/form-elements.css, que se importa una vez

Lo común a todos los controles

QuéVueReactPor qué
El valorv-modelvalue + onChange(valor)onChange recibe el valor, no el evento, igual que update:modelValue. El generador emite las dos formas desde el mismo JSON, así que tienen que significar lo mismo.
Etiqueta y ayudalabel, helplabel, helphelp pinta un icono con el texto en data-tooltip y aria-label.
ValidaciónvalidatorsvalidatorsSe escribe en data-validators, que lee js-validator.
Longitudmin_length, max_lengthminLength, maxLength (se aceptan también los de guion bajo)Van a data-min_length y data-max_length.
ClasecustomClasscustomClassReemplaza la clase del token del tema; no se suma.
Sin valorSe gobierna soloUn control de React sin value es no controlado. El modo se fija en el primer render, para no provocar el aviso de React.

Para el evento del DOM, React tiene onInput, onFocus, onBlur, onEnter y onPaste donde el gemelo Vue emite input, focus, blur, enter y paste.

GrupoComponentes
TextoTextInputComponent, TextareaInputComponent, TagsInputComponent, CountrySelectInputComponent, CodeInputComponent
SelecciónSelectInputComponent, SelectSearchInputComponent, ModelSearchInputComponent, TimezoneSelectInputComponent, CheckboxInputComponent, RadioInputComponent, SingleCheckboxInputComponent, MultiCheckboxInputComponent, SwitchComponent, StarsInputComponent, ColorPickerInputComponent
CompuestosDynamicGroupInputComponent, FqsInputComponent, PolymorphicInputComponent
EditoresEditorInputComponent, TextEditorMonoStyleInputComponent, CodeMirrorComponent
ArchivosFileInputComponent, FileDropInputComponent, SimpleFileInputComponent, AvatarInputComponent
BásicosButtonComponent, IconComponent, InputErrorComponent
EscritorioDialogComponent, DrawerComponent, MenuComponent, CommandPaletteComponent, SkeletonComponent, ClickToEditComponent, ToastRegionComponent, ConfirmHostComponent

Cada componente envuelve, en React, el equivalente de la librería que envuelve su gemelo Vue. Donde la librería no depende del framework, es literalmente la misma:

ComponenteVueReact
TextInputComponent (máscara)innoboxrr-maskjs/vueinnoboxrr-maskjs, el mismo motor
TagsInputComponent@yaireo/tagify@yaireo/tagify/react, la misma librería
EditorInputComponent@tinymce/tinymce-vue@tinymce/tinymce-react
CodeMirrorComponentvue-codemirror@uiw/react-codemirror, el mismo CodeMirror 6
SelectSearchInputComponentvue-selectreact-select
CountrySelectInputComponentvue-tel-inputreact-phone-number-input, el mismo libphonenumber-js
DynamicGroupInputComponentvuedraggable@dnd-kit/sortable, con reordenación por teclado
ColorPickerInputComponentlightvue (opcional; sin él, <input type="color">)react-colorful
IconComponent@iconify/vue@iconify/react
MenuComponentatributo popover y @floating-ui/domlo mismo

Texto

TextInputComponent

vue
<TextInputComponent
    type="text"
    name="phone"
    label="Teléfono"
    validators="required"
    :mask-format="{ mask: '(___) ___-____', format: '(***) ***-****' }"
    v-model="form.phone" />
jsx
<TextInputComponent
    type="text"
    name="phone"
    label="Teléfono"
    validators="required"
    maskFormat={{ mask: '(___) ___-____', format: '(***) ***-****' }}
    value={form.phone}
    onChange={(value) => setField('phone', value)} />
VueReactPor defectoQué hace
typetypeobligatorioEl tipo del <input>. Con password aparece un botón para ver la contraseña.
namenameobligatorio en Vue
label, help, iconigual'', null, ''icon es un nombre semántico o de Iconify.
placeholderplaceholdernull
validatorsvalidatorsnull
min_length, max_lengthminLength, maxLengthnullEn Vue también se escriben en los atributos min y max.
stepsstepsnullEl atributo step.
readonlyreadOnlysin valor
autofocus, autocompleteautoFocus, autoCompletesin valor
maskFormatmaskFormat{} en Vue, null en ReactUn { mask, format } de maskjs.
showPasswordLabel, hidePasswordLabeligual'Show password', 'Hide password'El aria-label del botón del ojo.
customClasscustomClasssin valor
idgeneradoLa etiqueta apunta a él.
eventos enter, input, focus, blur, pasteonEnter, onInput, onFocus, onBlur, onPasteReciben el evento del DOM. enter es soltar la tecla Enter.

TextareaInputComponent

VueReactPor defecto
namenameobligatorio en Vue
labellabel, help''
rowsrows5
placeholder, validatorsigualnull
min_length, max_lengthminLength, maxLength (o con guion bajo)null
customClasscustomClass, idsin valor

TagsInputComponent

VueReactPor defectoQué hace
namenameobligatorio en Vue
label, help, placeholderigual'', null, ''
customClasscustomClass'fe-input ' en Vue
modelValue (cadena o array)value[] en Vue
validators, id
whitelist, maxTags, duplicatesduplicates: falseOpciones de Tagify.
tagifyRefUna ref para llegar a la instancia de Tagify.

CountrySelectInputComponent

Un teléfono con selector de país, validado con libphonenumber-js.

VueReactPor defecto
labellabel, help''
defaultPhone, defaultCountryigual'', null
disableddisabledfalse
wrapperClass, containerClass, labelClassigualnull
dropdownOptions, inputOptions, preferredCountries{}, {}, [] (opciones de vue-tel-input)
name, id, placeholder, validators'telephone', null, 'Ingresa un número telefónico', null
evento changeonCountryChange

El evento no trae lo mismo en los dos frameworks

  • Vue emite change con { phone, country, isValid }. Con un número válido, phone es el número nacional. Al salir del campo con un número inválido, lo vacía y emite { phone: '', country, isValid: false }.
  • React llama a onCountryChange con { phone, country, callingCode, national, isValid }. phone es el valor completo y el nacional va aparte, en national.

CodeInputComponent

Un código de verificación, un carácter por casilla.

VueReactPor defecto (Vue / React)
fieldsfields3 / 6
fieldWidth, fieldHeightigual56 / 40
requiredrequiredtrue / false
disableddisabledfalse
title, classNameigualsin valor / null, ''
autoFocus, valuefalse
evento change(código)onChange(código)
evento complete(completo)onComplete(código)

En Vue, complete llega en cada cambio con un booleano. En React, onComplete solo se llama cuando todas las casillas están llenas, y recibe el código.

Selección

SelectInputComponent

vue
<SelectInputComponent name="status" label="Estado" validators="required" v-model="form.status">
    <option value="">Selecciona</option>
    <option value="draft">Borrador</option>
</SelectInputComponent>
jsx
<SelectInputComponent name="status" label="Estado" validators="required" value={form.status} onChange={setStatus}>
    <option value="">Selecciona</option>
    <option value="draft">Borrador</option>
</SelectInputComponent>
VueReactPor defecto
namenameobligatorio en Vue
label, helpigual'', null
multiplemultiplefalse
sizesizenull
validators, customClassigualnull
slot por defectochildrenLas <option>
id

SelectSearchInputComponent

Un select con búsqueda.

VueReactPor defectoQué hace
inputLabel, helpigual'', nullLa etiqueta del campo.
optionsoptions[]
labellabel'label'La clave de la opción que se muestra.
reducereducela opción enteraQué se guarda como valor.
multiple, clearable, disabled, loading, appendToBodyigualfalse, true, false, false, false
placeholderplaceholder''
customClasscustomClasssin valor
ajax, route, method, q, searchParams, minSearchLength, parseBeforeSubmit, debounceTimefalse, '', 'post', '', {}, 2, identidad, 300Búsqueda remota en Vue.
noOptionsText'Nothing here.'
evento search(respuesta)onSearch(término)
name, validators, id
  • Vue acepta además el resto de props de vue-select: taggable, pushTags, filterable, searchable, closeOnSelect, selectOnTab, getOptionLabel, getOptionKey, filterBy y demás. Con ajax, la búsqueda envía _token, paginate: 0, la clave q con el término y searchParams. Con method: 'get' todo eso viaja en la query.
  • React pasa el resto de props a react-select. Publica el valor en un <input type="hidden"> con name y data-validators, porque la librería no expone un input donde ponerlos.

ModelSearchInputComponent

Busca registros de un modelo contra una ruta del API.

VueReactPor defectoQué hace
labelStr, placeholderStrigualobligatorios en Vue
routerouteobligatorio en VueLa URL de búsqueda.
methodmethod'get'
qq'id'El parámetro que lleva el término.
externalFiltersexternalFilters{}Filtros que se suman a la búsqueda.
reducereduce(option) => option.id
getOptionLabeloptionLabel(option) => `ID: ${option.id}` en Vue; 'name' en ReactEn Vue es una función; en React, la clave del registro.
multiple, hideOnEmitigualfalse
debounceTimedebounce300Milisegundos.
minLength1Longitud mínima para buscar.
noOptionsText'Nothing results found'
eventos submit, selectedonSubmit, onSelected
customClasscustomClassnull

TimezoneSelectInputComponent

VueReactPor defecto
namenameobligatorio en Vue
label, helpigual'', null
placeholderplaceholder'Select a timezone'
validatorsvalidators''
multiple, sizefalse, null

CheckboxInputComponent y RadioInputComponent

VueReactPor defectoQué hace
namenameobligatorio en Vue
texttext''El texto junto a la casilla.
valvalnull en Checkbox; obligatorio en RadioEl valor que aporta esta opción.
validators, customClassigualnull
checked (solo Radio)false
slot por defectochildren

SingleCheckboxInputComponent

VueReactPor defecto
ididobligatorio en Vue
labellabel''
v-model:checkedchecked + onCheckedChange(marcado)false
valuevaluenull

MultiCheckboxInputComponent

VueReactPor defectoQué hace
optionsoptionsobligatorio en Vue[{ id, name }]. En Vue un validador exige las dos claves.
v-model:valuevalue + onChangeobligatorio en VueLos ids marcados.
idid''

React deriva la selección del valor. La versión Vue la recalcula con document.querySelectorAll, así que dos grupos con el mismo id se pisan.

SwitchComponent

VueReactQué hace
v-modelvalue + onChange(booleano)
evento change(evento)onToggle(evento)El evento del DOM.
el resto de propsVan al <input type="checkbox">.

StarsInputComponent

VueReactPor defecto
maxmax5
v-model (número)value + onChange0 en Vue
namename'rating'
char, inactiveCharigual'★', null
readonlyreadOnlyfalse
starsSizestarsSize'50px'
activeColor, inactiveColor, shadowColor, hoverColornull
slots activeLabel, inactiveLabel

ColorPickerInputComponent

VueReactPor defecto
labellabel, help, id''
clearableclearabletrue
colorscolors16 colores de Material
bottomBartrue
v-modelvalue + onChange'#607C8A' en Vue
name, validators'color', null

Sin lightvue, la versión Vue cae a <input type="color">, que abre el diálogo del sistema operativo y no se puede estilar ni probar. React usa react-colorful, de 2,8 kB y sin dependencias, y publica el valor en un input oculto con name y data-validators.

Compuestos

DynamicGroupInputComponent

Una lista de grupos de campos que se añaden, se quitan y se reordenan.

VueReactPor defectoQué hace
v-model (array)value + onChangeobligatorio en VueUn objeto por grupo.
inputsConfiginputsConfigobligatorio en VueUn campo por entrada, con key, type, label y options; en Vue, también attributes.
labellabel''
addButtonLabel, removeButtonLabel, itemLabeligual'Añadir', 'Eliminar', 'Item'
hasSufixtrue

En React, el asa de arrastre es un <button> alcanzable con el tabulador: @dnd-kit reordena también con el teclado. Un formulario que solo se reordena con el ratón no es accesible.

FqsInputComponent

Preguntas frecuentes: una lista de { question, answer }.

VueReactPor defecto
v-model (array)value + onChangeobligatorio en Vue
inputClassinputClass'fe-input '
labelslabels{ title: 'Add frequency asked questions', question: 'Question', answer: 'Answer', add: 'Add Question', remove: 'Remove question' }
uploadUrlnull
name'fqs'

PolymorphicInputComponent

Un solo componente que decide qué control pintar a partir de una configuración.

vue
<PolymorphicInputComponent
    :props="{ type: 'select', name: 'size', label: 'Talla', options: ['S', 'M', 'L'] }"
    v-model="answer"
    @save="guardar" />
jsx
<PolymorphicInputComponent
    config={{ type: 'select', name: 'size', label: 'Talla', options: ['S', 'M', 'L'] }}
    value={answer}
    onChange={setAnswer}
    onSave={guardar} />
Clave de la configuraciónUso
typeVue: text, number, date, time, url, email, textarea, radio, select, checkbox, file. React: los mismos, más switch y editor.
label, name, placeholder, validators, customClass, iconPasan al control.
minLength, maxLength, readonlyPasan al control de texto.
optionsPara radio, select y checkbox. En React, una opción de select puede ser { value, label }.
  • El prop. En Vue se llama literalmente props, y se conserva así por compatibilidad. En React se llama config y acepta props como alias.
  • Guardar. Tras un cambio aparece un botón que llama a save (onSave en React) con el valor.
  • Vue, tipo checkbox. El valor es un array en JSON y cada cambio emite save al momento.
  • Vue, tipo file. Sube con FileInputComponent y guarda el id del archivo.
  • React, tipo file. Usa SimpleFileInputComponent.

Editores

EditorInputComponent

Un editor de texto enriquecido con TinyMCE.

VueReactPor defecto (Vue / React)
id, nameigualobligatorios en Vue
label, helpigual'', null
heightheight400 / 300
disableddisabledfalse
initialValueinitialValue''
plugins, toolbarigualDistintos en cada paquete
menubar, inlinetrue, false
output'html' (o 'text')
tinymceCdntinymceScriptSrcVue carga TinyMCE 6.3.2 desde cdnjs
apiKey
uploadUrl, uri, file, onFileUploadSuccessnull, '/', false, null
showSpeechRecognitionfalse
extraConfigel resto de props{}
evento error
validators

TextEditorMonoStyleInputComponent

Los dos gemelos no son el mismo editor

  • Vue. Envuelve EditorInputComponent (TinyMCE) con dictado por voz. Sus props son label, name, id ('tmce'), placeholder, validators, min_length, max_length, defaultShowEditor (false), showSpeechRecognition (true), height (200), plugins, toolbar, menubar, inline, output, file, disabled, initialValue y customClass.
  • React. Es CodeMirrorComponent con language="html". Sus props son label, help, name, height ('400px'), readOnly, value y onChange.

CodeMirrorComponent

vue
<CodeMirrorComponent v-model="config" lang="json" label="Configuración del sitio" />
jsx
<CodeMirrorComponent
    language="json"
    label="Configuración del sitio"
    name="config"
    value={config}
    onChange={setConfig} />
VueReactPor defecto (Vue / React)Qué hace
langlanguage'html' / 'javascript'html, css, javascript o json, cargado bajo demanda.
themetheme'auto' / 'dark'Vue: auto, dark o light. React: dark o light.
autofocusfalseSi el editor pide el foco al montarse.
placeholder'Escriba su codigo aqui...'
labellabel, help''
height, readOnly'300px', falseEn Vue el alto es fijo: 400 px.
name, validatorsReact publica el valor en un input oculto con name y data-validators.
expone viewLa EditorView de CodeMirror.

En Vue la tabulación indenta y el tamaño de tabulación es 4. En React, el resto de props llega a @uiw/react-codemirror.

Qué cambió en 6.8.0 (Vue)

  • El lenguaje se carga bajo demanda.
    • Antes. El componente importaba de forma estática html, css, javascript, json y one-dark. En la aplicación base, un editor que solo edita JSON generó un chunk de 580 kB (200 kB con gzip).
    • Ahora. lang llega con import() y cada lenguaje es un chunk aparte. Una aplicación que solo monta el editor con lang="json" pasa de 637,3 kB (222,7 kB con gzip) a 498,5 kB (167,7 kB con gzip) en modo claro. En oscuro se suman 2,7 kB (1,2 kB con gzip) de one-dark.
  • Mientras llega el lenguaje, el editor funciona como texto plano. Cuando llega, se reconfigura sin rehacer la vista, así que no se pierden ni el cursor ni el historial.
  • Lo ya cargado se recuerda. Un editor que se vuelve a montar arranca directamente con su lenguaje; por ejemplo, dentro de un diálogo, cuyo contenido solo existe mientras está abierto.
  • Solo cuenta la última petición: un lenguaje lento no pisa al siguiente.
  • Un lang fuera de la lista cae a html sin lanzar. Antes, constructor o __proto__ encontraban algo en el mapa de lenguajes y rompían el editor.
  • El editor sigue el tema.
    • Antes. Se pintaba siempre con one-dark, también en una aplicación clara.
    • Ahora. theme="auto" aplica la misma regla que las variables de form-core: manda data-theme en <html> y, sin él, prefers-color-scheme. Cambia en caliente cuando cambia cualquiera de los dos. dark y light lo fuerzan. one-dark solo se descarga la primera vez que hay que pintar en oscuro.
  • autofocus vale false. Antes el editor pedía el foco al montarse, aunque estuviera al final de un formulario.
  • La etiqueta nombra al editor con aria-labelledby, y un clic en ella lo enfoca. Un <label for> no puede apuntar al contenteditable de CodeMirror. Antes, un lector de pantalla anunciaba un campo sin nombre. Sin texto de etiqueta no se añade el atributo, que nombraría al editor con nada.

Puede afectar a quien ya lo usa

  • El foco. Si una pantalla dependía de que el editor se llevara el foco al montarse, ahora hay que pedirlo:

    vue
    <CodeMirrorComponent v-model="html" autofocus />
  • El aspecto. Para recuperar el oscuro de siempre en una aplicación clara, fuérzalo con theme="dark".

Qué cambió en 3.8.0 (React)

  • El lenguaje también se carga bajo demanda. Una aplicación que solo monta el editor con language="json" pasa de 804,6 kB (270,9 kB con gzip) a 666,6 kB (215,9 kB con gzip). html, css y javascript quedan en chunks que nadie pide.
  • Mientras llega, texto plano. Un language fuera de la lista deja el editor en texto plano, sin lanzar.
  • theme sigue siendo dark (por defecto) o light. one-dark sigue en el bundle porque @uiw/react-codemirror lo importa y lo reexporta: cargarlo con import() desde este paquete no quitaría un solo byte.

En React la etiqueta aún no nombra al editor

En React, CodeMirrorComponent pinta la etiqueta sin asociarla al editor: un lector de pantalla lo anuncia sin nombre. La asociación con aria-labelledby solo existe en Vue.

Archivos

FileInputComponent

Soltar o elegir archivos, validarlos con describeFiles de form-core y subirlos a uploadUrl.

VueReactPor defecto
uploadUrluploadUrlobligatorio en Vue
methodmethod'POST'
autoUploadautoUploadfalse
namename'file'
visibilityvisibility'public'
maxSize, totalMaxSizeigual0 (sin límite)
maxFilesmaxFiles1
validMimesvalidMimesTexto, imágenes, audio, vídeo, PDF, Office y gzip
message, onDropMessage, onUploadMessage, maxFilesMessage, errorsTitlelabelsVue: textos sueltos. React: { drop, maxFiles, overTotal, upload, uploading, failed }
dropzoneClass, previewGridClass, hideOnMaxFilesReached, showTopPreview, showBottomPreview'drop-zone', 'fe-w-quarter', false, false, false
slots normalSlot, onDropSlot, onUploadSlot
eventos startUpload, updateFileList, endUploadonStartUpload, onFileListChange, onEndUpload

FileDropInputComponent

VueReactPor defecto
multiplemultiplefalse
mainText, subTextigual'Arrastra y suelta el archivo aqui', 'o haz clic para seleccionar los archivos.'
accept
evento change(archivos)onFilesChange(archivos)Un array de File.

SimpleFileInputComponent

VueReactPor defecto
inputNameinputName'file'
labellabel'Seleccionar archivo'
customClasscustomClassnull
accept
evento input(archivo)onInput(archivo)

AvatarInputComponent

VueReactPor defecto
avatarUrl, uploadUrligualobligatorios en Vue
uploadMethoduploadMethod'POST'
name'avatar'
evento upload(json)onUpload(json)La respuesta del servidor, ya en JSON.

En Vue, el archivo viaja en un FormData con el campo file. Un fallo solo se escribe en la consola.

Básicos

ButtonComponent

vue
<ButtonComponent value="Guardar" />
<ButtonComponent variant="secondary" type="button" value="Cancelar" @click="cerrar" />
jsx
<ButtonComponent value="Guardar" />
<ButtonComponent variant="secondary" type="button" value="Cancelar" onClick={cerrar} />
VueReactPor defectoQué hace
variantvariant'primary'secondary, danger o link eligen el token del tema; cualquier otro valor usa button.
valuevalue'Enviar' en VueEl texto.
typetype'submit'
disableddisabledfalse
customClasscustomClasssin valor
slot por defectochildren

IconComponent

VueReactPor defectoQué hace
namenameobligatorioUn nombre del mapa de iconos o uno de Iconify.
sizesizenullAncho y alto.
customClassclassNamenull

Es aria-hidden a propósito: un icono decorativo junto a su texto no debe leerse dos veces. Si el icono es la única pista, como en un botón sin texto, la etiqueta va en el aria-label del botón. Un setIcons() en caliente lo repinta.

InputErrorComponent

PropQué hace
errorsEl objeto de errores de un 422 de Laravel, { campo: [mensajes] }.
typeLa clave que mira este componente.

Pinta un párrafo por mensaje, con la clase del token error, y nada si no hay errores.

Piezas de escritorio

Se apoyan en lo que ya hace el navegador, sin librerías de interfaz:

  • <dialog> con showModal() pone la capa superior, el fondo inerte, el foco atrapado, Escape y la devolución del foco.
  • El atributo popover pone el cierre al pulsar fuera.
  • Floating UI coloca los menús.

Donde no hay showModal() o popover, como en jsdom o en un navegador antiguo, los componentes caen a los atributos open y hidden.

DialogComponent y DrawerComponent

vue
<DrawerComponent v-model:open="abierto" title="Nuevo producto">
    <CreateForm @submit="guardar" />

    <template #footer="{ close }">
        <ButtonComponent variant="secondary" type="button" value="Cancelar" @click="close" />
    </template>
</DrawerComponent>
jsx
<DrawerComponent
    open={abierto}
    onOpenChange={setAbierto}
    title="Nuevo producto"
    footer={({ close }) => (
        <ButtonComponent variant="secondary" type="button" value="Cancelar" onClick={close} />
    )}>
    <CreateForm onSubmit={guardar} />
</DrawerComponent>
VueReactPor defectoQué hace
v-model:openopen + onOpenChange(abierto)false
titletitlenullNombra el diálogo con aria-labelledby.
labellabelnullaria-label cuando no hay title.
size (solo diálogo)size'md'sm, md o lg.
side (solo drawer)side'end'end a la derecha, start a la izquierda.
dismissibledismissibletrueCon false no se cierra con Escape ni con el fondo, y no hay X.
closeLabelcloseLabel'Cerrar'El aria-label de la X.
evento closeonCloseSe emite junto a update:open con false.
slot headerheaderSustituye al título.
slot por defecto con { close }children (nodo o función ({ close }) => …)
slot footer con { close }footer (nodo o función)
  • Manda open. Escape, un clic en el fondo o la X solo piden cerrar con update:open / onOpenChange(false): cierra quien abrió. Si el estado lo guardara el <dialog>, se separaría del booleano de tu formulario.
  • El contenido solo existe mientras está abierto. Un formulario vuelve limpio cada vez, y dos copias del mismo formulario no se pisan los id en el DOM.
  • El foco al abrir. En Vue, usa el atributo autofocus de siempre. En React, marca el elemento con data-autofocus: React no escribe autofocus en el DOM.
vue
<MenuComponent
    :items="[
        { id: 'edit', label: 'Editar', icon: 'edit', action: editar },
        { separator: true },
        { id: 'delete', label: 'Eliminar', icon: 'delete', danger: true, disabled: ! puede, disabledReason: 'Sin permiso' },
    ]"
    :before-open="cargarPermisos"
    @select="(item) => console.log(item.id)" />
jsx
<MenuComponent
    items={[
        { id: 'edit', label: 'Editar', icon: 'edit', action: editar },
        { separator: true },
        { id: 'delete', label: 'Eliminar', icon: 'delete', danger: true, disabled: ! puede, disabledReason: 'Sin permiso' },
    ]}
    beforeOpen={cargarPermisos}
    onSelect={(item) => console.log(item.id)} />
VueReactPor defectoQué hace
itemsitems[]Los elementos (abajo).
labellabel'Acciones'Nombre del menú y del botón.
iconicon'more'El icono del botón por defecto.
placementplacement'bottom-end'La colocación de Floating UI.
beforeOpenbeforeOpennullFunción (puede ser asíncrona) que se espera antes de abrir.
eventos select, open, closeonSelect, onOpen, onClose
slot trigger con { toggle, open, loading, triggerProps }renderTrigger({ toggle, open, loading, triggerProps })Sustituye al botón. Pon triggerProps en tu botón: son los atributos ARIA.
expone open() y close()

Un elemento puede ser:

  • Una acción: { id, label, icon, shortcut, danger, disabled, disabledReason, action }. action(item) se llama al elegirlo, después de select.
  • Un separador: { separator: true }.
  • Un título de grupo: { group: 'Texto' }.

Y así se comporta:

  • beforeOpen se espera antes de abrir. Mientras tanto el botón está deshabilitado. Es para los permisos de una fila: abrir con todo deshabilitado y habilitarlo después hace que el usuario vea parpadear lo que no puede hacer.
  • Un elemento sin permiso no desaparece. Se ve deshabilitado (aria-disabled) y explica por qué en data-tooltip: quien no puede tiene que saber que la acción existe.
  • Teclado. Las flechas, Inicio y Fin recorren los elementos que se pueden usar. Al abrir, el foco va al primero; al cerrar, vuelve al botón.

CommandPaletteComponent

vue
<CommandPaletteComponent
    v-model:open="paleta"
    :items="[
        { id: 'products', label: 'Productos', group: 'Ir a', icon: 'box', action: irAProductos },
        { id: 'new', label: 'Nuevo producto', group: 'Crear', shortcut: 'N', keywords: ['alta'], action: nuevo },
    ]" />
jsx
<CommandPaletteComponent
    open={paleta}
    onOpenChange={setPaleta}
    items={[
        { id: 'products', label: 'Productos', group: 'Ir a', icon: 'box', action: irAProductos },
        { id: 'new', label: 'Nuevo producto', group: 'Crear', shortcut: 'N', keywords: ['alta'], action: nuevo },
    ]} />
VueReactPor defectoQué hace
v-model:openopen + onOpenChangefalse
itemsitems[]{ id, label, group, icon, shortcut, keywords, action }
placeholderplaceholder'Buscar…'
emptyTextemptyText'Sin resultados'
labellabel'Paleta de comandos'
hotkeyhotkey'k'Con Ctrl o Cmd abre y cierra la paleta. null quita el atajo.
evento selectonSelectSe emite antes de llamar a action(item).
  • El filtro no distingue mayúsculas ni acentos: quien escribe «configuracion» encuentra «Configuración». Busca en label, group y keywords.
  • Los grupos se muestran en el orden en que aparecen.
  • Teclado. Las flechas recorren la lista y Enter ejecuta. Al abrir, la búsqueda se vacía y el foco va al campo.

SkeletonComponent

vue
<SkeletonComponent :lines="3" />
<SkeletonComponent shape="circle" :width="40" :height="40" />
<SkeletonComponent shape="block" height="12rem" />
jsx
<SkeletonComponent lines={3} />
<SkeletonComponent shape="circle" width={40} height={40} />
<SkeletonComponent shape="block" height="12rem" />
PropPor defectoQué hace
shape'text'text, circle o block.
lines1Solo con text. Con varias, la última mide el 60 %: se lee como un párrafo y no como una tabla.
width, heightnullUn número son píxeles; una cadena, cualquier medida CSS.

Es aria-hidden: lo que anuncia la carga a un lector de pantalla es el aria-busy del contenedor que la espera, no cada rectángulo.

ClickToEditComponent

Un valor que se edita donde está: en la celda de una tabla o en una ficha.

vue
<ClickToEditComponent
    :value="producto.title"
    label="Título"
    :save="(title) => updateModel(producto.id, { title })" />
jsx
<ClickToEditComponent
    value={producto.title}
    label="Título"
    onSave={(title) => updateModel(producto.id, { title })} />
VueReactPor defectoQué hace
valuevalue''
typetype'text'El tipo del campo de edición.
placeholderplaceholder'—'Lo que se ve cuando el valor está vacío.
labellabel'Editar'El aria-label del botón y del campo.
saveonSavenullFunción que guarda; se espera.
evento inputonInputEl valor confirmado.
customClasscustomClasssin valorReemplaza la clase del campo.
expone start()Abre la edición.
  • Confirmar y cancelar. Enter o salir del campo confirman; Escape cancela.
  • Sin cambios, no pasa nada. Un valor igual al que había no guarda ni emite.
  • Con save, la confirmación espera a que termine. Mientras guarda, el campo está deshabilitado.
  • Si save lanza, la celda se queda abierta con el message del error, o «No se pudo guardar». Cerrarla mostraría un valor que no se guardó.
  • Antes, Vue y React no se comportaban igual: salir del campo lo dejaba abierto en Vue y lo confirmaba en React. Ahora es igual en los dos.

ToastRegionComponent y ConfirmHostComponent

vue
<!-- En la raíz de la aplicación, una sola vez -->
<ToastRegionComponent label="Avisos" close-label="Cerrar" />
<ConfirmHostComponent close-label="Cerrar" />
jsx
{/* En la raíz de la aplicación, una sola vez */}
<ToastRegionComponent label="Avisos" closeLabel="Cerrar" />
<ConfirmHostComponent closeLabel="Cerrar" />
ComponentePropsQué pinta
ToastRegionComponentlabel ('Avisos'), closeLabel ('Cerrar')La cola de notify() de form-core
ConfirmHostComponentcloseLabel ('Cerrar')La pregunta de confirmAction() de form-core
  • La región de avisos vive en la capa superior con popover="manual". Si no, un aviso que llega con un drawer abierto quedaría debajo del fondo inerte, que es justo donde se guarda un formulario. Si se abre un diálogo después, la región vuelve a mostrarse para quedar encima.
  • Roles. Un aviso de peligro tiene role="alert"; los demás, role="status".
  • La confirmación es un diálogo pequeño y empieza con el foco en cancelar: un Enter por inercia no debe borrar nada. Escape o un clic fuera cuentan como cancelar.

Qué se monta una sola vez

ComponenteUna sola vezPor qué
ToastRegionComponentSí, en la raízLa cola de avisos es una sola para toda la aplicación. Dos regiones pintarían cada aviso dos veces; ninguna, no pintaría nada, aunque notify() siga llenando la cola.
ConfirmHostComponentSí, en la raízTodos los anfitriones escuchan la misma confirmación: con dos, la pregunta se abre dos veces. Sin ninguno, confirmAction() cae a window.confirm.
CommandPaletteComponentEscucha el atajo en window al montarse. Dos paletas montadas responderían las dos a Ctrl+K.
DialogComponent, DrawerComponent, MenuComponentNoCada instancia es independiente; su contenido solo existe mientras está abierto.

Equivalencias Vue ↔ React

La forma general

VueReact
v-modelvalue + onChange(valor)
v-model:openopen + onOpenChange(abierto)
:custom-classcustomClass
min_length / max_lengthminLength / maxLength (se aceptan también los de guion bajo)
slot por defectochildren
slot con ámbito (footer, header con { close })prop con un nodo o una función ({ close }) => …
slot trigger de MenuComponentrenderTrigger({ toggle, open, loading, triggerProps })
@eventoonEvento
:mask-format (directiva v-format por dentro)maskFormat
atributo autofocus dentro de un diálogodata-autofocus
defineExpose (view, open(), close(), start())no existe en estos componentes
app.use(FormElements)no existe

Diferencias por componente

ComponenteVueReact
TextInputComponentreadonly, autofocus, autocompletereadOnly, autoFocus, autoComplete, id
SelectSearchInputComponentinputLabel y todas las props de vue-select; búsqueda remota con ajax; @search(respuesta)inputLabel, props de react-select; onSearch(término); input oculto con name
ModelSearchInputComponentgetOptionLabel (función), debounceTime, noOptionsTextoptionLabel (clave), debounce, minLength
SingleCheckboxInputComponentv-model:checkedchecked + onCheckedChange
MultiCheckboxInputComponentv-model:valuevalue + onChange
SwitchComponent@change(evento)onToggle(evento)
StarsInputComponentreadonly, colores y slotsreadOnly
CountrySelectInputComponent@change({ phone, country, isValid }) con phone nacionalonCountryChange({ phone, country, callingCode, national, isValid })
CodeInputComponent3 casillas de 56 px; @complete(booleano)6 casillas de 40 px; onComplete(código) al completarse
PolymorphicInputComponentprop propsprop config (acepta props); tipos switch y editor
EditorInputComponenttinymceCdn, subida de archivos, output, extraConfigtinymceScriptSrc, apiKey, validators
TextEditorMonoStyleInputComponentTinyMCE con dictadoCodeMirror en html
CodeMirrorComponentlang, theme auto/dark/light, autofocus, expone viewlanguage, theme dark/light, height, readOnly, name
FileInputComponenttextos sueltos y slots; @updateFileListlabels; onFileListChange
FileDropInputComponent, SimpleFileInputComponent@change, @inputonFilesChange, onInput, accept
AvatarInputComponent@uploadonUpload, name
IconComponentcustomClassclassName
ClickToEditComponent:save, @input, expone start()onSave, onInput
MenuComponent@select, @open, @close, expone open() y close()onSelect, onOpen, onClose

Por qué hay diferencias deliberadas

  • MultiCheckboxInputComponent. React deriva la selección del valor en vez de leer el DOM, así que dos grupos con el mismo id no se pisan.
  • Los componentes que envuelven una librería sin input propio publican el valor en un <input type="hidden"> con name y data-validators para que el validador lo encuentre. En React son SelectSearchInputComponent, ColorPickerInputComponent, CodeMirrorComponent y EditorInputComponent.
  • En React, pasar id cambia a la vez el del control y el for de su etiqueta. Sin eso, la etiqueta apuntaría a la nada.