Superficies
Modal
Edita un valor en contexto y vuelve a la pantalla anterior.
Importación
import { NeuModalComponent } from '@neural-ui/native/modal';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.
Diálogo básico
Abre un mensaje breve y ciérralo con la acción o el control de cierre.
Ver y copiar el ejemplo completo
import { Component, signal } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuCardComponent, NeuModalComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-card title="Diálogo básico" variant="outlined"><view class="usage-section"><text class="usage-caption">Abre un mensaje breve y ciérralo con la acción o el control de cierre.</text><neu-button label="Acerca de este espacio" size="sm" (pressed)="open.set(true)" /></view></neu-card>
<neu-modal closeLabel="Cerrar diálogo" title="Espacio del producto" [(open)]="open">
<text>Un espacio compartido para los planes y documentos del equipo.</text>
<ng-template neuModalFooter><view class="dialog-actions"><neu-button label="Entendido" size="sm" variant="text" (pressed)="open.set(false)" /></view></ng-template>
</neu-modal>
</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; }
.dialog-actions { flex-direction: row; justify-content: flex-end; }
`,
})
export class ExampleComponent {
readonly open = signal(false);
}Reserva de desayuno
Personaliza un desayuno en un modal con selección, café opcional y acciones de confirmación. Confirmar actualiza la reserva local; cerrar o cancelar conserva el pedido anterior.
Ver y copiar el ejemplo completo
import { Component, signal, computed } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuSwitchComponent } from '@neural-ui/native/switch';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuBadgeComponent } from '@neural-ui/native/badge';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuRadioGroupComponent } from '@neural-ui/native/radio';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuSwitchComponent, NeuCardComponent, NeuBadgeComponent, NeuModalComponent, NeuRadioGroupComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="product-scroll" keyboardShouldPersistTaps="handled" [showsVerticalScrollIndicator]="true"><view class="product-stack"><view class="product-section"><text class="product-title" accessibilityRole="header">Un buen día empieza aquí</text><text class="product-caption">El desayuno del equipo, a tu gusto</text></view>
<neu-card title="Desayuno del viernes" [description]="saved()" variant="outlined"><view class="product-section"><neu-badge label="09:00 · Neural Studio" tone="info" /><view class="product-actions"><neu-button label="Elegir desayuno" size="sm" (pressed)="edit()" /></view></view></neu-card>
<neu-modal closeLabel="Cerrar diálogo" title="Tu desayuno" [(open)]="open"><view class="product-section"><neu-radio label="Opción de desayuno" [options]="choices" [(value)]="choice" /><neu-switch label="Añadir café" [(checked)]="coffee" /></view><ng-template neuModalFooter><view class="product-actions"><neu-button label="Confirmar desayuno" size="sm" (pressed)="confirm()" /><neu-button label="Cancelar" size="sm" variant="text" (pressed)="open.set(false)" /></view></ng-template></neu-modal></view></scroll-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 choices = [{ id: 'toast', label: 'Tostada de aguacate' }, { id: 'yogurt', label: 'Yogur y fruta' }];
readonly choice = signal('toast');
readonly coffee = signal(true);
readonly open = signal(false);
readonly savedChoice = signal('toast');
readonly savedCoffee = signal(true);
readonly saved = computed(() => (this.choices.find(item => item.id === this.savedChoice())?.label || '') + (this.savedCoffee() ? ' · Con café' : ''));
edit() { this.choice.set(this.savedChoice()); this.coffee.set(this.savedCoffee()); this.open.set(true); }
confirm() { this.savedChoice.set(this.choice()); this.savedCoffee.set(this.coffee()); this.open.set(false); }
}Formulario de perfil
Edita el perfil y consulta el nombre guardado después de cerrar el diálogo.
Ver y copiar el ejemplo completo
import { Component, signal } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuInputComponent } from '@neural-ui/native/input';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuInputComponent, NeuCardComponent, NeuModalComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-card title="Formulario de perfil" variant="outlined"><view class="usage-section"><text class="usage-caption">Edita el perfil y consulta el nombre guardado después de cerrar el diálogo.</text><neu-button label="Editar perfil" (pressed)="editing.set(true)" /></view></neu-card>
<neu-modal closeLabel="Cerrar diálogo" title="Editar perfil" [(open)]="editing">
<neu-input requiredLabel="Obligatorio" label="Nombre completo" [(value)]="name" />
<ng-template neuModalFooter><view class="dialog-actions"><neu-button label="Listo" size="sm" variant="text" (pressed)="editing.set(false)" /></view></ng-template>
</neu-modal>
<text class="usage-caption">Perfil actual: {{ name() || 'Sin nombre' }}</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; }
.dialog-actions { flex-direction: row; justify-content: flex-end; }
`,
})
export class ExampleComponent {
readonly editing = signal(false);
readonly name = signal('Alex Morgan');
}Contenido desplazable
Permite desplazar un documento largo dentro del diálogo y mantén disponible la acción de cierre.
Ver y copiar el ejemplo completo
import { Component, signal } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuCardComponent, NeuModalComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-card title="Contenido desplazable" variant="outlined"><view class="usage-section"><text class="usage-caption">Permite desplazar un documento largo dentro del diálogo y mantén disponible la acción de cierre.</text><neu-button label="Leer el resumen del proyecto" (pressed)="open.set(true)" /></view></neu-card>
<neu-modal closeLabel="Cerrar diálogo" title="Resumen del proyecto" [(open)]="open"><view class="modal-long-content">@for (part of briefSections; track part.title) {
<view class="usage-section"><text class="usage-heading">{{ part.title }}</text><text>{{ part.description }}</text></view>
}</view><ng-template neuModalFooter><view class="dialog-actions"><neu-button label="Listo" size="sm" variant="text" (pressed)="open.set(false)" /></view></ng-template></neu-modal>
</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; }
.modal-long-content { gap: 16px; } .dialog-actions { flex-direction: row; justify-content: flex-end; }
`,
})
export class ExampleComponent {
readonly open = signal(false);
readonly briefSections = [
{ title: 'Objetivo', description: 'Haz más clara la bienvenida móvil para los nuevos miembros.' },
{ title: 'Público', description: 'Personas que utilizan el espacio mientras viajan o entre reuniones.' },
{ title: 'Primera sesión', description: 'Presenta la siguiente acción y facilita el acceso a la información importante.' },
{ title: 'Contenido', description: 'Utiliza etiquetas breves, ayudas útiles y estados vacíos claros.' },
{ title: 'Revisión', description: 'Comprueba el flujo completo en una pantalla pequeña con el equipo.' },
{ title: 'Entrega', description: 'Comparte el plan final con las personas responsables del lanzamiento.' },
{ title: 'Acceso sin conexión', description: 'Explica qué cambios se guardan en el dispositivo y cuáles necesitan conexión. Conserva el borrador si se pierde la conexión y permite decidir cuándo volver a intentarlo.' },
{ title: 'Opciones de privacidad', description: 'Pide solo la información necesaria para el siguiente paso. Explica los permisos opcionales en su contexto y permite continuar cuando un permiso no sea esencial para la tarea.' },
{ title: 'Recorridos accesibles', description: 'Revisa el recorrido completo con un lector de pantalla, amplía el texto y comprueba el orden del foco. Cada acción necesita un nombre claro y cada error debe ofrecer una forma útil de recuperarse.' },
{ title: 'Comprobaciones del lanzamiento', description: 'Confirma con el equipo los textos, las traducciones y los estados vacíos. Prueba el guardado, la cancelación y el reintento en ambas plataformas antes de entregar la aplicación a quienes la revisarán.' },
];
}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 { NeuModalHeaderDirective, NeuModalFooterDirective } from '@neural-ui/native/modal';| Plantilla o región | Contexto | Uso |
|---|---|---|
ng-template[neuModalHeader] | — | Sustituye el encabezado; el control de cierre sigue disponible. title conserva el encabezado predeterminado; marca tu cabecera con accessibilityRole="header". |
ng-template[neuModalFooter] | — | Se muestra después del contenido desplazable. Úsalo para acciones que deban permanecer visibles. |
ng-content | — | El contenido entre las etiquetas del modal se muestra en su cuerpo desplazable. |
Referencia API
Inputs
| Nombre | Tipo | Predeterminado | Enlace | Descripción |
|---|---|---|---|---|
| title | string | Obligatorio | Input | Encabezado de la superficie, sección o presentación. |
| dismissible | boolean | true | Input | Permite cerrar la presentación desde la interacción del usuario. La app puede seguir cerrándola mediante open. |
| showCloseButton | boolean | true | Input | Muestra la acción de cierre en la cabecera si el diálogo se puede cerrar. Ocultarla no desactiva el cierre nativo mediante Atrás. |
| closeLabel | string | 'Close' | Input | Texto o nombre accesible utilizado para cerrar. No ejecuta la acción por sí solo. |
Models
| Nombre | Tipo | Predeterminado | Enlace | Descripción |
|---|---|---|---|---|
| open | boolean | false | Model · bidireccional | Indica si se solicita mantener abierta la presentación o mensaje. Enlaza una signal escribible mediante [(open)]. |
Outputs
| Nombre | Payload | Cuándo se emite |
|---|---|---|
| openChange | boolean | Emite el valor actualizado de open. [(open)] enlaza este output junto con su input model. |
| closed | "native" | "action" | "backdrop" | Motivo de la solicitud de cierre: native, action o backdrop. openChange sincroniza el model de visibilidad. |
| opened | void | Comunica que la presentación nativa se ha abierto. |
| dismissed | void | Comunica la finalización del cierre nativo. |
Estado de los models
Enlaza la signal con [(nombreModel)] sin llamarla. Utiliza nombreModel() para leer su valor.
import { signal } from '@angular/core';
// Fields inside your component class
readonly open = signal<boolean>(false);