Campos de referencia
This content is not available in your language yet.
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).
Definición en el schema
Sección titulada «Definición en el schema»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', },};| Atributo | Tipo | Descripción |
|---|---|---|
type | 'ObjectId' | 'String' | El tipo del ID. ObjectId es el típico de Mongo. |
ref | string | Nombre del modelo. Se pasa al refResolver para que sepa qué endpoint llamar. |
displayName | string | Campo del documento referenciado que se muestra (default: 'name'). |
multiple | boolean | Si true, el campo guarda un array de IDs. |
El refResolver
Sección titulada «El refResolver»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;}>;Implementación típica
Sección titulada «Implementación típica»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, };};Conectarlo en Vue
Sección titulada «Conectarlo en Vue»<CoFormRenderer :form="form" :ref-resolver="refResolver" />Conectarlo en Web Component
Sección titulada «Conectarlo en Web Component»document.querySelector('co-form').refResolver = refResolver;Ciclo de vida: hydration
Sección titulada «Ciclo de vida: hydration»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:
- Detecta que el value es un string (no un objeto con label).
- Llama a
refResolver({ model: 'Company', ids: ['64a7f12...'] }). - 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.
Multiple refs
Sección titulada «Multiple refs»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']Búsqueda con debounce
Sección titulada «Búsqueda con debounce»<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;};Validación
Sección titulada «Validación»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);};Pattern: resolver global compartido
Sección titulada «Pattern: resolver global compartido»En una app con muchos forms, conviene tener UN solo refResolver para toda la app:
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>