Elegir una fila de un catalogo que no cabe en memoria. Se teclea, la lista se reduce contra el servidor y lo que se elige es una fila completa, no un texto.
El kit tenia dos controles cerca y ninguno hacia las tres cosas a la vez:
| Control | Busca al teclear | Contra el servidor | Devuelve seleccion | Aguanta volumen |
|---|---|---|---|---|
lib-search |
si, con espera | si | no — emite una cadena | n/a |
lib-select con searchable |
si | no — filtra en el navegador | si | no: exige cargar el catalogo entero |
| Dialogo de seleccion | si, dentro de la ventana | si, paginado | si | si, pero interrumpiendo |
lib-autocomplete |
si | si, paginado | si | si |
Cuando NO usarlo. Un catalogo de decenas de filas se resuelve mejor con lib-select y searchable: carga una vez y filtra en el navegador, sin codigo nuevo. Este campo paga cuando el catalogo crece —terceros, plan de cuentas, centros de costo— o cuando el filtro tiene que vivir dentro de la barra de un listado, donde una ventana modal obliga a abrir, buscar, elegir y cerrar para ver el efecto.


| Paquete | nebula-ui-kit |
| Version | 0.18.0 |
| Registry de consumo | https://nexus.centricasoluciones.com/repository/centrica-frontend-all/ |
| Selector | lib-autocomplete |
| Componente | AutocompleteComponent<T> |
| Contrato de carga | NebulaAutocompleteLoadFn<T> · NebulaAutocompleteQuery · NebulaAutocompletePage<T> |
| Piel | NebulaAutocompleteApariencia — 'buscador' | 'campo' |
| Formularios | ControlValueAccessor: se enlaza con formControlName, [(value)] o [ngModel] |
| Angular | 21 · componente standalone, OnPush, signals |
| Contrato de datos | page-response del microservicio que sirve el catalogo |
| En DEV desde | 14 de Septiembre de 2026 |
No tiene
api-docs. Es una API de codigo, no un contrato HTTP: se consume importando el componente, no llamando una ruta. El contrato que si viaja por red es elpage-responsedel microservicio que sirve el catalogo.
El campo no toca la red. Pide una pagina llamando a la funcion que le entrega el consumidor, y esa funcion es la que sale al servidor:
lib-autocomplete compone {page, limit, search} (no toca la red)
| loadDataFn
la pantalla traduce y llama a su servicio (no toca la red)
| getPageRecord
el servicio POST /api/v1/.../page-response <-- aqui sale la peticion
Por que importa: el campo sirve igual a un catalogo de maestros que a uno de terceros, porque no sabe a quien se le pide. Para cambiar el origen se toca la funcion de carga; el componente no se toca.
page-responseLa funcion de carga debe resolver contra page-response, como todo listado del ERP. No por /read ni por /page. El campo pide de a una pagina precisamente para que el volumen del catalogo no entre en juego.
Ojo con el origen de las paginas. El campo cuenta desde 1 —igual que el dialogo de seleccion del ecosistema— y el contrato del backend cuenta desde 0. La traduccion la hace la funcion de carga.
loadDataFn recibe { page, limit, search } y devuelve { data, total }: la misma forma con la que se alimenta el dialogo de seleccion del ecosistema. Una pantalla que ya tenga esa funcion escrita la pasa a los dos sin duplicarla, y el ecosistema no gana un segundo contrato de carga paginada.
| Elemento | Quien lo decide |
|---|---|
| Por que campos se busca | La funcion de carga, al componer sus criterios |
| Que se ve de cada fila | displayFn o itemLabelKey |
| Que se guarda | itemIdKey: el valor del control es la CLAVE, no el objeto |
| Si el catalogo esta filtrado | La funcion de carga; el campo pinta lo que le devuelven y no vuelve a filtrar |
npm install nebula-ui-kit@0.18.0 --legacy-peer-deps
No necesita proveedores adicionales. Los dos textos del panel —el de espera y el del minimo de caracteres— salen de NEBULA_UI_CONFIG, con valor por defecto; la aplicacion que quiera otros los declara ahi:
{
provide: NEBULA_UI_CONFIG,
useValue: {
...DEFAULT_NEBULA_UI_CONFIG,
autocompleteLoadingMessage: 'Buscando...',
autocompleteMinCharsMessage: (min: number) => `Escriba al menos ${min} caracteres`,
},
}
export interface NebulaAutocompleteQuery {
/** Pagina solicitada, contada DESDE 1. */
page: number;
/** Tamaño de pagina. */
limit: number;
/** Termino teclado, ya recortado. Cadena vacia cuando no se ha teclado nada. */
search: string;
}
export interface NebulaAutocompletePage<T> {
data: T[];
total: number;
}
export type NebulaAutocompleteLoadFn<T> = (
query: NebulaAutocompleteQuery,
) => Promise<NebulaAutocompletePage<T>>;
El total no es decorativo: de el sale si quedan paginas por traer. Un total mal informado deja el desplazamiento sin mas candidatos aunque el servidor los tenga.
| Entrada | Por defecto | Que fija |
|---|---|---|
loadDataFn |
— | Trae una pagina de candidatos. Sin ella el campo no busca nada |
itemIdKey |
'id' |
Propiedad que hace de valor del control |
itemLabelKey |
'nombre' |
Propiedad que se muestra cuando no hay displayFn |
displayFn |
— | Como se pinta una fila. Prevalece sobre itemLabelKey |
selectedItem |
— | Fila ya elegida, para rotularla al abrir |
debounceMs |
300 |
Espera tras la ultima tecla antes de preguntar |
minChars |
0 |
Caracteres minimos para empezar a buscar |
pageSize |
10 |
Tamaño de pagina |
apariencia |
'buscador' |
Piel del campo |
placeholder, disabled, required, requiredMsg, errorMsg, panelClass |
Lo habitual de un campo |
| Salida | Que emite |
|---|---|
selectionChange |
La fila completa elegida, o null al limpiar |
value |
La CLAVE de la fila elegida (modelo de dos vias) |
El valor del control es la clave, no el objeto. Es lo que viaja al servidor y lo que un formulario guarda. Quien necesite otros campos de la fila los toma de selectionChange.
Y por eso existe selectedItem: al recibir una clave sin su fila, el campo no tiene con que construir la etiqueta y muestra la clave cruda. Quien ya conoce la fila la declara ahi y se evita un viaje.
El sistema separa dos familias de campo y el componente las declara:
apariencia |
Como se ve | Cuando |
|---|---|---|
'buscador' |
Pildora de fondo parejo, sin filete | Cuando va solo en la pantalla |
'campo' |
Esquinas redondeadas solo arriba y filete inferior que engorda al primario en foco | Cuando comparte barra con el buscador de un listado |
Por que importa. Un filtro y el buscador de la grilla hacen cosas distintas —uno decide QUE se ve y el otro ACOTA lo que ya se ve— y con la misma piel y el mismo texto se leen como dos cajas de busqueda iguales. La piel de campo y un rotulo propio los distinguen de un vistazo.
Los colores no se eligen en el componente: salen de los tokens del tema, los mismos con los que el kit pinta su buscador y sus campos.
Lo fija quien lo usa, por variables del host, sin ::ng-deep y sin tocar la libreria. Es el mismo mecanismo con el que lib-search deja ajustar el suyo.
| Variable | Por defecto |
|---|---|
--lib-autocomplete-width |
100% |
--lib-autocomplete-min-width |
0 |
--lib-autocomplete-max-width |
none |
--lib-autocomplete-height |
56px — la del buscador del listado |
--lib-autocomplete-font-size |
16px |
.mi-filtro {
lib-autocomplete {
--lib-autocomplete-height: 72px;
--lib-autocomplete-font-size: 18px;
}
}
El campo NO monta un mat-form-field. Es deliberado: la barra de filtros de la grilla compacta cualquier campo de Material que le proyecten, y con el campo de Material este control salia mas bajo y mas estrecho que el buscador que tiene al lado. Al maquetarse por su cuenta, la barra no puede deformarlo y el ancho lo decide la pantalla.
La pantalla de Valores del Maestro lleva el maestro como criterio obligatorio: sin el, el listado mezclaria los valores de todos los maestros. El filtro vive DENTRO de la ranura de filtros de la grilla.
/**
* Trae una pagina de maestros para el filtro, por `page-response`.
*
* El termino busca por codigo O por la descripcion CRUDA, que es la almacenada: la traducida se
* resuelve al presentar y el servidor no filtra por ella.
*/
protected readonly buscarMaestros = async (
query: NebulaAutocompleteQuery,
): Promise<{ data: TipoMaestro[]; total: number }> => {
const termino = query.search.trim();
const pagina = await firstValueFrom(
this.tipoMaestroService.getPageRecord({
// El campo cuenta las paginas desde 1 y el contrato desde 0.
page: Math.max(query.page - 1, 0),
size: query.limit,
searchsBy: termino
? [
{ key: 'codigo', operation: 'MATCH', value: termino, linkOperator: 'OR' },
{ key: 'descripcion', operation: 'MATCH', value: termino },
]
: [],
ordersBy: [{ column: 'codigo', direction: 'ASC' }],
}),
);
return { data: pagina?.content ?? [], total: pagina?.totalElements ?? 0 };
};
/** El codigo manda y la descripcion acompaña, que es como se lee un maestro. */
protected readonly rotularMaestro = (maestro: TipoMaestro): string =>
`${maestro.codigo} — ${maestro.descripcionTraducida ?? maestro.descripcion}`;
<lib-smart-table [config]="smartConfig()" ...>
<div nstFilters class="maestro-filter">
<lib-field-label [label]="'...FILTER.MASTER.LABEL' | translate" [required]="true" />
<lib-autocomplete
apariencia="campo"
[loadDataFn]="buscarMaestros"
itemIdKey="codigo"
[displayFn]="rotularMaestro"
[selectedItem]="maestroElegido()"
[placeholder]="'...FILTER.MASTER.PLACEHOLDER' | translate"
[pageSize]="10"
(selectionChange)="onMaestroElegido($event)"
></lib-autocomplete>
</div>
</lib-smart-table>
.maestro-filter {
/* En pixeles y NO en porcentaje: la ranura se dimensiona por su contenido. */
flex: 1 1 auto;
min-width: 420px;
max-width: 100%;
}
/**
* Sin maestro elegido NO se consulta: dejar pasar la consulta devolveria los valores de todos los
* maestros mezclados. La consulta de la grilla se guarda, para reemitirla al elegir uno.
*/
override onQueryChange(query: NebulaTableQuery): void {
if (!this.maestroCodigo()) {
this.consultaPendiente = query;
this.store.vaciar();
return;
}
this.consultaPendiente = null;
super.onQueryChange(query);
}
Su clave i18n es propia, no la del buscador del listado ni la del dialogo: mientras los tres dijeron «Buscar por codigo o descripcion», en pantalla se leian iguales.
| Comportamiento | Por que esta |
|---|---|
| Descarta las respuestas fuera de orden | Cada peticion lleva un numero de turno y solo se pinta la del turno vigente. Sin eso, teclear rapido deja en pantalla el resultado de una busqueda ya abandonada |
| Teclear despues de elegir PIERDE la eleccion, y lo avisa | Lo que hay en el campo ya no es la fila elegida; dejar el valor haria que el formulario guardase algo que el usuario no ve |
| Al salir del campo repone el texto de lo elegido | Sin eso queda un termino a medio teclear que no corresponde al valor guardado |
| Pagina al llegar al final del panel | El volumen del catalogo no entra en juego |
| Un fallo del servidor deja el panel vacio | No propaga: un catalogo que no responde no debe tumbar la pantalla |
| Sintoma | Causa |
|---|---|
| El campo no busca nada | Falta loadDataFn |
| Se pide siempre la misma pagina | La funcion de carga no traduce el origen: el campo cuenta desde 1 y el contrato desde 0 |
| El desplazamiento no trae mas | El total que devuelve la funcion de carga no refleja el del servidor |
| Al abrir muestra la clave en vez del texto | Llego el valor sin su fila: declarar selectedItem |
| Se ve igual que el buscador del listado | Falta apariencia="campo", o comparten el mismo texto de marcador |
| Sale mas bajo o mas estrecho que el buscador | El ancho se pidio en porcentaje dentro de la ranura de filtros, que se dimensiona por su contenido: pedirlo en pixeles |
| El listado sale sin el criterio obligatorio | La pantalla no corta la consulta cuando el filtro esta vacio |
| Los cambios del kit no se ven al recargar | El servidor de desarrollo preempaqueta las dependencias en .angular/cache: hay que borrarla y reiniciar |
| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-09-14 | Carlos Torres | Publicacion del campo de autocompletado contra el servidor: el contrato de carga, las dos pieles, el tamaño a demanda y el ejemplo del filtro del maestro dentro de la ranura de filtros de la grilla |