Ir al contenido

Campos de referencia

Los ref fields son el tipo de campo más usado en aplicaciones de Prolibu: apuntan a entidades de otro modelo del backend. Se renderizan como <co-ref-field> (un autocomplete con búsqueda, paginación y prefetch).

const dealSchema = {
company: {
type: 'ObjectId',
ref: 'Company', // nombre del modelo backend
displayName: 'name', // qué campo mostrar en la UI
label: 'Empresa',
required: true,
},
owner: {
type: 'ObjectId',
ref: 'User',
displayName: 'fullName',
label: 'Responsable',
},
};
AtributoTipoDescripción
type'ObjectId' | 'String'El tipo del ID. ObjectId es el típico de Mongo.
refstringNombre del modelo. Se pasa al refResolver para que sepa qué endpoint llamar.
displayNamestringCampo del documento referenciado que se muestra (default: 'name').
multiplebooleanSi true, el campo guarda un array de IDs.

Es la función que tu app define para resolver qué opciones mostrar. Recibe la query del usuario y retorna las opciones a renderizar.

type RefResolver = (params: {
model: string; // el ref del schema
query?: string; // texto que escribió el usuario
page?: number; // página solicitada (1-based)
pageSize?: number; // default 20
ids?: string[]; // si está presente, debes resolver POR estos IDs
}) => Promise<{
options: Array<{
value: string; // el _id
label: string; // el displayName resuelto
meta?: any; // info opcional adicional
}>;
total?: number;
hasMore?: boolean;
}>;
const refResolver = async ({ model, query, page = 1, pageSize = 20, ids }) => {
// Caso 1: hydration — el form abre con un _id existente y necesita traer su label
if (ids?.length) {
const res = await api.get(`/${model.toLowerCase()}/byIds`, {
params: { ids: ids.join(',') },
});
return {
options: res.data.map((doc) => ({
value: doc._id,
label: doc.name, // o el displayName
meta: doc,
})),
};
}
// Caso 2: búsqueda o paginación
const res = await api.get(`/${model.toLowerCase()}`, {
params: { search: query, page, pageSize },
});
return {
options: res.data.items.map((doc) => ({
value: doc._id,
label: doc.name,
meta: doc,
})),
total: res.data.total,
hasMore: page * pageSize < res.data.total,
};
};
<CoFormRenderer :form="form" :ref-resolver="refResolver" />
document.querySelector('co-form').refResolver = refResolver;

Si abres un form con un valor _id ya seteado en initialValues:

const form = useForm(() => ({
schema: { modelSchema: dealSchema, locale: 'es' },
initialValues: {
company: '64a7f12e9d3b88a01c1234ef', // ID, no objeto
},
}));

<co-ref-field> automáticamente:

  1. Detecta que el value es un string (no un objeto con label).
  2. Llama a refResolver({ model: 'Company', ids: ['64a7f12...'] }).
  3. Espera la respuesta y renderiza el label resuelto en el input.

No tienes que hacer nada — solo asegúrate de que tu resolver maneje el parámetro ids.

const dealSchema = {
tags: {
type: 'ObjectId',
ref: 'Tag',
multiple: true,
label: 'Etiquetas',
},
};

El valor entonces es string[]. La UI muestra chips removibles.

// initialValues
{ tags: ['id1', 'id2', 'id3'] }
// El refResolver recibe ids = ['id1', 'id2', 'id3']

<co-ref-field> ya hace debounce internamente (300ms por default). Tu refResolver solo se llama una vez que el usuario para de escribir, así no saturas la API.

Si necesitas más control:

<CoFormRenderer :form="form" :ref-resolver="resolveWithCache" />
const cache = new Map();
const inflight = new Map();
const resolveWithCache = async (params) => {
const key = JSON.stringify(params);
if (cache.has(key)) return cache.get(key);
if (inflight.has(key)) return inflight.get(key);
const promise = realResolver(params).then((result) => {
cache.set(key, result);
inflight.delete(key);
return result;
});
inflight.set(key, promise);
return promise;
};

Por defecto, un ref field con required: true valida que el valor no sea null/undefined. La validación de “el ID realmente existe en backend” es responsabilidad del backend al hacer submit — no se hace round-trip al validar.

Si necesitas validación adicional (ej. “este Company debe estar activo”), hazla en el submit:

const onSubmit = async (values) => {
const company = await api.get(`/company/${values.company}`);
if (!company.active) {
form.setError('company', ['Esta empresa está desactivada']);
return;
}
await api.createDeal(values);
};

En una app con muchos forms, conviene tener UN solo refResolver para toda la app:

composables/useGlobalRefResolver.js
import { useAuth } from '@/composables/useAuth';
export function useGlobalRefResolver() {
const { token } = useAuth();
return async ({ model, query, page, pageSize, ids }) => {
const headers = { Authorization: `Bearer ${token.value}` };
const baseUrl = `/api/${model.toLowerCase()}`;
if (ids?.length) {
const { data } = await fetch(`${baseUrl}/byIds?ids=${ids.join(',')}`, { headers })
.then((r) => r.json());
return { options: data.map(toOption) };
}
const params = new URLSearchParams({ q: query || '', page, pageSize });
const { data, total } = await fetch(`${baseUrl}?${params}`, { headers })
.then((r) => r.json());
return {
options: data.map(toOption),
total,
hasMore: page * pageSize < total,
};
};
}
function toOption(doc) {
return {
value: doc._id,
label: doc.name || doc.fullName || doc.title || doc._id,
meta: doc,
};
}

Y en cualquier componente:

<script setup>
const refResolver = useGlobalRefResolver();
const form = useForm(() => ({ /* ... */ }));
</script>
<template>
<CoFormRenderer :form="form" :ref-resolver="refResolver" />
</template>