Foundations
State ownership
Your application owns the data; controls communicate edits and action requests.
Models, inputs and outputs
An input configures a control, such as its label or disabled state. An output reports something that happened, such as a button press. A model shares an editable value in both directions: the application provides it and receives the user’s changes. The API tables identify which kind each property uses.
| Binding | Meaning |
|---|---|
| [(value)]="name" | Shares the editable value with a writable signal. |
| [(checked)]="notifications" | Shares the boolean state of a Switch or Checkbox. |
| [disabled]="!canSave()" | Passes a value from the application into the control. |
| (pressed)="save()" | Requests an action; the application decides what it does. |
Use the writable signal itself in a two-way model binding: [(value)]="name". Read it with parentheses in an expression: name(). A computed signal is derived state and is suitable for inputs such as disabled; it is not an editable model.
A screen owns the editing state
Place the following component inside the root neu-theme of your application.
import { Component, computed, signal } from '@angular/core';
import { Text, View } from '@ng-native/components';
import { NeuInputComponent } from '@neural-ui/native/input';
import { NeuSwitchComponent } from '@neural-ui/native/switch';
import { NeuButtonComponent } from '@neural-ui/native/button';
@Component({
selector: 'app-preferences',
imports: [Text, View, NeuInputComponent, NeuSwitchComponent, NeuButtonComponent],
template: `
<view class="content">
<neu-input label="Full name" [(value)]="name" hint="Shown to your team" />
<neu-switch label="Team notifications" [(checked)]="notifications" />
<neu-button label="Save preferences" [disabled]="!canSave()" (pressed)="save()" />
<neu-button label="Reset" variant="text" (pressed)="reset()" />
<text class="feedback">{{ feedback() }}</text>
</view>
`,
styles: `
.content { padding: 24px; gap: 16px; }
.feedback { color: var(--neu-text); }
`,
})
export class PreferencesComponent {
readonly name = signal('Ada Lovelace');
readonly notifications = signal(true);
readonly canSave = computed(() => this.name().trim().length > 0);
readonly feedback = signal('');
save(): void {
if (!this.canSave()) return;
const preferences = { name: this.name().trim(), notifications: this.notifications() };
this.feedback.set('Preferences ready for ' + preferences.name);
// Pass preferences to your application service to persist them.
}
reset(): void {
this.name.set('Ada Lovelace');
this.notifications.set(true);
this.feedback.set('');
}
}The name and notifications signals store edits. The canSave value derives from the name, and reset restores the screen state. Your save handler performs any persistence the application needs.
Local edits, saved data and asynchronous work
For cancellable editing, copy the saved value into a draft when opening the editor. Update the saved value only after confirmation. Do not bind an unsaved editor directly to shared persisted state if Cancel must discard the changes.
This complete editor keeps edits after a failed request, prevents duplicate submissions and aborts the request when the screen is destroyed. Replace the example endpoint and supply your application’s authentication. The server must validate and persist the data; a successful local action alone does not save anything remotely.
Place the following component inside the root neu-theme of your application.
import { Component, DestroyRef, computed, inject, signal } from '@angular/core';
import { Text, View } from '@ng-native/components';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuInputComponent } from '@neural-ui/native/input';
@Component({
selector: 'app-profile-editor',
imports: [View, Text, NeuInputComponent, NeuButtonComponent],
template: `
<view class="form">
<neu-input label="Display name" [(value)]="draft" [disabled]="saving()" />
@if (error()) { <text accessibilityRole="alert">{{ error() }}</text> }
@if (saved()) { <text accessibilityLiveRegion="polite">Saved: {{ saved() }}</text> }
<view class="actions">
<neu-button [label]="error() ? 'Retry' : 'Save'" [loading]="saving()"
[disabled]="!canSave()" (pressed)="save()" />
<neu-button label="Cancel edits" variant="text" [disabled]="saving()"
(pressed)="reset()" />
</view>
</view>
`,
styles: '.form { gap: 16px; } .actions { gap: 8px; align-items: flex-start; }',
})
export class ProfileEditorComponent {
readonly draft = signal('Alex Morgan');
readonly saved = signal('');
readonly saving = signal(false);
readonly error = signal('');
readonly canSave = computed(() => !!this.draft().trim() && !this.saving());
private readonly lifetime = inject(DestroyRef);
private request?: AbortController;
constructor() {
this.lifetime.onDestroy(() => this.request?.abort());
}
reset() {
this.draft.set(this.saved() || 'Alex Morgan');
this.error.set('');
}
async save() {
if (!this.canSave()) return;
const name = this.draft().trim();
const request = new AbortController();
this.request = request;
this.saving.set(true);
this.error.set('');
try {
// Replace with your authenticated API; the server validates and persists.
const response = await fetch('https://api.example.com/profile', {
method: 'PATCH', signal: request.signal,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (!response.ok) throw new Error('Save failed');
if (!this.lifetime.destroyed) this.saved.set(name);
} catch {
if (!this.lifetime.destroyed && !request.signal.aborted) {
this.error.set('Could not save. Your edits are still here; try again.');
}
} finally {
if (!this.lifetime.destroyed) this.saving.set(false);
if (this.request === request) this.request = undefined;
}
}
}loading belongs to the screen and is restored in finally. The button receives it; it does not start or persist a request automatically. For object or array signals, replace the value with set or update instead of silently mutating it.