Skip to content

Campos custom

This content is not available in your language yet.

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().