Datos y multimedia
Virtual List
Explora una colección larga en una vista nativa de altura acotada.
Importación
import { NeuVirtualListComponent } from '@neural-ui/native/virtual-list';Ejemplos
Importa ExampleComponent en su padre y coloca <app-example /> dentro del neu-theme raíz de tu aplicación. El preview aporta el marco y las zonas seguras; utiliza la estructura raíz de Instalación en tu app.
Todos los proyectos
Recorre una colección larga con filas reutilizadas. Selecciona un proyecto para mostrar su resumen bajo la lista.
Ver y copiar el ejemplo completo
import { Component, signal, computed } from '@angular/core';
import { NeuAvatarComponent } from '@neural-ui/native/avatar';
import { NeuVirtualListComponent } from '@neural-ui/native/virtual-list';
import { View, Text } from '@ng-native/components';
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';
import type { NeuOption } from '@neural-ui/native';
@Component({
selector: 'app-example',
imports: [NeuAvatarComponent, NeuVirtualListComponent, NeuVirtualListItemDirective, View, Text],
template: `
<view class="product-section"><view class="product-section"><text class="product-title" accessibilityRole="header">Todos los proyectos</text><text class="product-caption">100 proyectos · Un espacio</text></view>
<neu-virtual-list emptyLabel="No hay proyectos" [items]="projects" [height]="320" [estimatedItemHeight]="100" (selected)="selected.set($event)" (endReached)="reachedEnd.set(true)">
<ng-template neuVirtualListItem let-project><view class="product-row"><neu-avatar [label]="project.label" size="sm" /><view class="product-copy"><text class="product-heading">{{ project.label }}</text><text class="product-caption">{{ project.description }}</text></view></view></ng-template>
</neu-virtual-list>
@if (selectedProject(); as project) { <view class="product-surface"><text class="product-heading">{{ project.label }}</text><text class="product-caption">{{ project.description }}</text></view> }
<text class="product-caption" accessibilityLiveRegion="polite">{{ reachedEnd() ? 'Has llegado al último proyecto' : 'Selecciona un proyecto para ver su resumen' }}</text></view>
`,
styles: `
.actions { gap: 12px; align-items: flex-start; } text { color: var(--neu-text, #172033); font-size: 16px; }
:host { flex: 1; flex-shrink: 1; min-height: 0; }
.product-scroll { flex: 1; flex-shrink: 1; height: 0px; min-height: 0; }
.product-stack { gap: 24px; padding-bottom: 20px; }
.product-section { gap: 12px; }
.product-row { flex-direction: row; align-items: center; gap: 12px; }
.product-copy { flex: 1; min-width: 0; gap: 4px; }
.product-title { color: var(--neu-text, #172033); font-size: 24px; line-height: 30px; font-weight: 600; }
.product-heading { color: var(--neu-text, #172033); font-size: 17px; line-height: 23px; font-weight: 600; }
.product-caption { color: var(--neu-muted, #596477); font-size: 14px; line-height: 20px; }
.product-actions { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; }
.product-surface { gap: 16px; padding: 16px; border-radius: 16px; background-color: var(--neu-surface, #ffffff); border-width: 1px; border-color: var(--neu-border, #a5afbf); }
`,
})
export class ExampleComponent {
readonly projects: readonly NeuOption[] = Array.from({ length: 100 }, (_, index) => ({
id: 'project-' + index,
label: ['Lanzamiento móvil', 'Renovación de la web', 'Investigación con clientes', 'Sistema de diseño', 'Bienvenida al equipo', 'Revisión de accesibilidad', 'Notas del lanzamiento', 'Portal de clientes', 'Espacio sin conexión', 'Analítica del producto'][index % 10] + ' · ' + ['Descubrimiento', 'Prototipo', 'Piloto', 'Lanzamiento', 'Crecimiento', 'Conclusiones', 'Acabado', 'Despliegue', 'Comentarios', 'Siguiente etapa'][Math.floor(index / 10)],
description: ['Equipo de producto · En curso', 'Equipo de diseño · En revisión', 'Equipo de clientes · Planificación'][index % 3],
}));
readonly selected = signal('');
readonly selectedProject = computed(() => this.projects.find(item => item.id === this.selected()));
readonly reachedEnd = signal(false);
}Filas recicladas propias
Desplaza filas personalizadas con cabecera y pie. Cada plantilla recibe el índice original del elemento aunque la lista recicle sus vistas.
Ver y copiar el ejemplo completo
import { Component, signal, computed } from '@angular/core';
import { NeuAvatarComponent } from '@neural-ui/native/avatar';
import { NeuVirtualListComponent } from '@neural-ui/native/virtual-list';
import { View, Text, ScrollView } from '@ng-native/components';
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';
@Component({
selector: 'app-example',
imports: [NeuAvatarComponent, NeuVirtualListComponent, NeuVirtualListItemDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-virtual-list emptyLabel="No hay proyectos" [items]="members" [height]="360" [estimatedItemHeight]="80" (selected)="selected.set($event)">
<view listHeader class="list-heading"><text class="usage-heading">Directorio del espacio</text><text class="usage-caption">24 miembros del equipo</text></view>
<ng-template neuVirtualListItem let-member let-index="index" let-disabled="disabled">
<view class="directory-row"><neu-avatar [label]="member.label" size="md" /><view class="directory-copy"><text class="usage-heading">{{ member.label }}</text><text class="usage-caption">{{ disabled ? 'No disponible' : member.description }}</text></view><text class="usage-caption">{{ index + 1 }}</text></view>
</ng-template>
<view listFooter class="list-heading"><text class="usage-caption">Fin del directorio</text></view>
</neu-virtual-list>
<text class="usage-caption">{{ selectedMember() ? 'Persona seleccionada: ' + selectedMember()?.label : 'Selecciona un miembro' }}</text>
</view></scroll-view>
`,
styles: `
.actions { gap: 12px; align-items: flex-start; } text { color: var(--neu-text, #172033); font-size: 16px; }
.usage-scroll { flex: 1; }
.usage-stack { gap: 20px; padding-bottom: 20px; }
.usage-section { gap: 12px; }
.usage-heading { color: var(--neu-text, #172033); font-size: 16px; font-weight: 600; }
.usage-caption { color: var(--neu-muted, #64748b); font-size: 14px; }
.usage-row { flex-direction: row; flex-wrap: wrap; gap: 12px; align-items: center; }
.usage-actions { flex-direction: row; flex-wrap: wrap; gap: 8px; align-items: center; }
.directory-row { flex-direction: row; gap: 12px; align-items: center; } .directory-copy { flex: 1; min-width: 0; gap: 3px; } .list-heading { gap: 4px; padding: 12px 0; }
`,
})
export class ExampleComponent {
readonly members = Array.from({ length: 24 }, (_, index) => ({ id: 'member-' + index, label: ['Alex', 'Sam', 'Jordan', 'Taylor', 'Robin', 'Casey'][index % 6] + ' ' + ['Morgan', 'Rivera', 'Chen', 'Patel'][Math.floor(index / 6)], description: ['Diseñadora de producto', 'Desarrollador móvil', 'Ingeniero de diseño', 'Responsable de producto'][index % 4], disabled: index === 3 }));
readonly selected = signal('');
readonly selectedMember = computed(() => this.members.find(member => member.id === this.selected()));
}Ajusta la configuración y prueba el resultado.
Capturas del dispositivo en inglés.


Personalización
Importa estas directivas junto con el componente y añádelas a Component.imports. Los ejemplos anteriores incluyen la configuración completa.
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';| Plantilla o región | Contexto | Uso |
|---|---|---|
ng-template[neuVirtualListItem] | $implicit: NeuOption; index: number; disabled: boolean | Contenido visual de cada fila. La lista conserva el reciclado, la medición y la selección. index es la posición del elemento, no la del slot reciclado; guarda el estado por item.id. |
[listHeader] | — | View nativa antes de las filas desplazables; no necesita una directiva Neural adicional. |
[listFooter] | — | View nativa después de las filas desplazables; no necesita una directiva Neural adicional. |
Contextos de las plantillas
import type { NeuVirtualListItemContext } from '@neural-ui/native/virtual-list';let-item recibe $implicit; los campos con nombre utilizan let-index="index". Usa los tipos exportados al declarar un TemplateRef reutilizable. Mantén el contenido interno como presentación: el padre conserva la acción accesible y el comportamiento de selección.
Referencia API
Inputs
| Nombre | Tipo | Predeterminado | Enlace | Descripción |
|---|---|---|---|---|
| items | readonly NeuOption[] | [] | Input | Datos ordenados del componente. Utiliza el contrato siguiente e identificadores estables y únicos. |
| disabled | boolean | false | Input | Desactiva la interacción del usuario. La aplicación puede seguir proporcionando valores. |
| estimatedItemHeight | number | 84 | Input | Altura estimada de fila utilizada para calcular la ventana virtualizada. |
| height | number | 400 | Input | Altura en unidades de layout nativo del viewport o contenido, según el componente. |
| emptyLabel | string | 'No items' | Input | Texto o nombre accesible utilizado para estado vacío. No ejecuta la acción por sí solo. |
Models
Este componente no declara models bidireccionales.
Outputs
| Nombre | Payload | Cuándo se emite |
|---|---|---|
| selected | string | Emite el ID del elemento seleccionado. |
| endReached | void | Comunica que se alcanza el final de la lista visible. Evita cargas simultáneas en la aplicación. |
Tipos e interfaces de datos
Utiliza los imports en tu aplicación. Las definiciones siguientes se muestran como referencia.
NeuOption
import type { NeuOption } from '@neural-ui/native';
// Public definition for reference only. Import this type; do not redeclare it.
export interface NeuOption {
readonly id: string;
readonly label: string;
readonly description?: string;
readonly disabled?: boolean;
}| Campo | Tipo | Requisito | Significado |
|---|---|---|---|
| id | string | Obligatorio | Identificador estable y único para seguimiento y selección. |
| label | string | Obligatorio | Nombre legible del elemento. |
| description | string | Opcional | Texto de apoyo opcional; su presentación depende del control. |
| disabled | boolean | Opcional | Impide activar o seleccionar este elemento. |