Ir al contenido

Campos custom

Cobalt cubre los casos comunes (text, number, select, boolean, date, ref, textarea). Para casos específicos del dominio — firma digital, color picker, file uploader, JSON editor, mapa — necesitas un campo custom.

Hay dos formas de hacerlo, según el caso.

Declara el componente en el schema con uiCom:

const schema = {
signature: {
type: 'String',
uiCom: 'signature', // string libre que identifica tu componente
label: 'Firma',
required: true,
},
accentColor: {
type: 'String',
uiCom: 'color-picker',
default: '#02a270',
label: 'Color de marca',
},
};

Crea el componente custom:

SignatureField.vue
<script setup>
import { ref } from 'vue';
const props = defineProps({
field: Object, // FieldDescriptor
value: String,
errors: Array, // string[]
disabled: Boolean,
});
const emit = defineEmits(['update:value', 'blur']);
const canvas = ref(null);
const isEmpty = ref(true);
const clear = () => {
canvas.value.clear();
emit('update:value', '');
isEmpty.value = true;
};
const save = () => {
const dataUrl = canvas.value.toDataURL();
emit('update:value', dataUrl);
isEmpty.value = false;
};
</script>
<template>
<div class="signature-field">
<label>{{ field.label }}</label>
<SignaturePad ref="canvas" @end="save" />
<co-button label="Limpiar" variant="ghost" size="s" @click="clear" />
<p v-if="errors.length" class="error">{{ errors[0] }}</p>
</div>
</template>

Y pásalo a CoFormRenderer vía fieldComponents:

<script setup>
import SignatureField from './SignatureField.vue';
import ColorPickerField from './ColorPickerField.vue';
const customFields = {
'signature': SignatureField,
'color-picker': ColorPickerField,
};
</script>
<template>
<CoFormRenderer :form="form" :field-components="customFields" />
</template>

CoFormRenderer ahora usa tu componente cada vez que encuentra un campo con uiCom: 'signature'.

Si solo necesitas customizar UN campo en UN form (no reutilizable), usa el slot directamente:

<CoFormRenderer :form="form">
<template #field:signature="{ value, setValue, errors }">
<SignaturePad
:value="value"
:error="errors[0]"
@change="setValue"
/>
</template>
</CoFormRenderer>

No necesitas uiCom en el schema porque el slot machea por nombre del campo, no por kind.

CasoOpción recomendada
Componente reutilizable en muchos formsuiCom + fieldComponents
Override puntual de UN campo en UN formSlot field:<name>
Mismo tipo de campo en muchos schemasuiCom + fieldComponents
Diferentes overrides en cada form (ej. password con eye toggle solo aquí)Slot

Un componente registrado vía fieldComponents recibe el valor como prop y lo actualiza emitiendo update:value (no hay un prop setValue):

// Props
interface FieldRendererProps {
field: FieldDescriptor; // metadata del campo desde el schema
value: any; // valor actual
errors: string[]; // errores del campo (vacío mientras no esté touched)
disabled: boolean; // disabled global o del campo
locale?: string; // locale actual (BCP-47), para campos locale-aware
}
// Eventos
// emit('update:value', v) ← el setter
// emit('blur') ← marca el campo como touched

FieldDescriptor te da acceso a TODOS los atributos del schema:

interface FieldDescriptor {
name: string;
kind: FieldKind;
label: string;
placeholder: string;
required: boolean;
disabled: boolean;
hidden: boolean;
fullWidth: boolean;
helperText: string;
originalAttrs: Record<string, any>; // ← TU `uiCom`, `description`, etc.
ajvProperty: Record<string, any>;
}

Si pusiste uiCom: 'signature' en el schema, lo encuentras en field.originalAttrs.uiCom.

FileUploadField.vue
<script setup>
import { ref } from 'vue';
const props = defineProps({
field: Object,
value: Array, // array de URLs subidas
errors: Array, // string[]
disabled: Boolean,
});
const emit = defineEmits(['update:value', 'blur']);
const uploading = ref(false);
const onChange = async (e) => {
const files = Array.from(e.target.files);
uploading.value = true;
const uploaded = await Promise.all(
files.map(async (file) => {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const { url } = await res.json();
return url;
})
);
emit('update:value', [...(props.value || []), ...uploaded]);
uploading.value = false;
};
const removeAt = (idx) => {
emit('update:value', props.value.filter((_, i) => i !== idx));
};
</script>
<template>
<div class="file-field">
<label>{{ field.label }}</label>
<input type="file" multiple @change="onChange" :disabled="uploading" />
<ul>
<li v-for="(url, i) in value" :key="url">
{{ url }}
<button @click="removeAt(i)">x</button>
</li>
</ul>
<p v-if="uploading">Subiendo…</p>
<p v-if="errors.length" class="error">{{ errors[0] }}</p>
</div>
</template>

Schema:

{
attachments: {
type: 'String', // o 'Mixed', solo importa que AJV lo valide
uiCom: 'file-upload',
multiple: true,
label: 'Adjuntos',
},
}

Y en el form:

<CoFormRenderer
:form="form"
:field-components="{ 'file-upload': FileUploadField }"
/>

Custom kinds (override del FieldKind built-in)

Sección titulada «Custom kinds (override del FieldKind built-in)»

También puedes reemplazar los kinds estándar pasando la misma key como en FieldKind:

const customFields = {
'text': MyFancyTextField, // reemplaza TODOS los text fields
'date': DatePickerWithPresets,
};

Esto es útil si quieres un look-and-feel propio diferente al de Cobalt en TODA tu app.

Para <co-form> (Web Component), los custom fields se pasan vía slots field:<name>:

<co-form schema='...'>
<div slot="field:signature">
<signature-pad id="sig"></signature-pad>
</div>
</co-form>
<script>
const form = document.querySelector('co-form');
const pad = document.getElementById('sig');
pad.addEventListener('change', async (e) => {
await form.setValue('signature', e.detail.dataUrl);
});
</script>

Es más manual que la versión Vue: tú escuchas el evento del componente custom y propagas el valor al form via setValue().