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>
const props = defineProps({
field: Object, // FieldDescriptor
value: String,
setValue: Function,
error: String,
touched: Boolean,
});
const canvas = ref(null);
const isEmpty = ref(true);
const clear = () => {
canvas.value.clear();
props.setValue('');
isEmpty.value = true;
};
const save = () => {
const dataUrl = canvas.value.toDataURL();
props.setValue(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="error && touched" class="error">{{ error }}</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, error, touched }">
<SignaturePad
:value="value"
:error="touched && error"
@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
interface FieldRendererProps {
field: FieldDescriptor; // metadata del campo desde el schema
value: any; // valor actual
setValue: (v: any) => void; // setter
touch: () => void; // marca como touched
error: string | undefined; // primer error (si touched)
errors: string[]; // todos los errores
touched: boolean;
disabled: boolean; // disabled global o del campo
readOnly: boolean;
}

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
setValue: Function,
error: String,
touched: Boolean,
});
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;
})
);
props.setValue([...(props.value || []), ...uploaded]);
uploading.value = false;
};
const removeAt = (idx) => {
props.setValue(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="error && touched" class="error">{{ error }}</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().