Form controls
Uploader
Choose documents and keep upload state in the application.
Import
import { NeuUploaderComponent } from '@neural-ui/native/uploader';Examples
Import ExampleComponent into its parent and place <app-example /> inside your application’s root neu-theme. The preview supplies the device frame and safe areas; use the root setup from Installation in your app.
Everything your project needs
Choose a small file, run a local upload simulation, retry a deliberate failure and remove files. Connect the real transport shown below for your own application.
View and copy the complete example
import { Component, signal } from '@angular/core';
import { NeuSwitchComponent } from '@neural-ui/native/switch';
import { NeuUploaderComponent } from '@neural-ui/native/uploader';
import { View, Text, ScrollView } from '@ng-native/components';
import type { NeuUpload, NeuUploadItem } from '@neural-ui/native';
@Component({
selector: 'app-example',
imports: [NeuSwitchComponent, NeuUploaderComponent, 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">Everything your project needs</text><text class="product-caption">Keep the brief and supporting files together</text></view>
<view class="product-surface"><neu-uploader label="Project documents" chooseLabel="Choose documents" uploadLabel="Upload" removeLabel="Remove" [(files)]="files" [upload]="upload" [maxFiles]="3" [maxBytes]="5000000" [types]="types" /></view>
<neu-switch label="Fail the next simulated upload" [(checked)]="failNext" />
<text class="product-caption">Local simulation · No files are sent to a server. Remove an active file to cancel; retry after an error.</text></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 files = signal<readonly NeuUploadItem[]>([]);
readonly types = ['application/pdf', 'image/*', 'text/plain'];
readonly failNext = signal(false);
readonly upload: NeuUpload = async (_file, progress, signal) => {
const fail = this.failNext(); this.failNext.set(false);
for (const percent of [25, 60, 100]) {
await this.pause(signal);
if (fail && percent === 60) throw new Error('Simulated connection error. Try again.');
progress(percent);
}
};
private pause(signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
if (signal.aborted) { reject(new Error('Cancelled')); return; }
const abort = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); reject(new Error('Cancelled')); };
const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, 350);
signal.addEventListener('abort', abort, { once: true });
});
}
}Change the settings and try the result.
Device captures shown in English.


Connect a real upload
Choosing a file does not upload it. Supply upload to send it to your backend or object storage. This native example uses a short-lived PUT URL provided by your authenticated backend and Expo FileSystem to read the picked file.
npx expo install expo-file-systemCreate these two files, import ProjectFilesComponent into its parent and place it inside the root neu-theme. Replace api.example.com with your endpoint and add your app’s authentication. The backend must validate permissions, MIME type and size and return a file-specific HTTPS upload URL; client limits are only user feedback.
import { File } from 'expo-file-system';
import { fetch as expoFetch } from 'expo/fetch';
import type { NeuFile, NeuUpload } from '@neural-ui/native';
type UploadTarget = { url: string; headers?: Record<string, string> };
// Ask your authenticated backend for a short-lived, file-specific PUT URL.
type ResolveTarget = (file: NeuFile, signal: AbortSignal) => Promise<UploadTarget>;
export function createUpload(resolveTarget: ResolveTarget): NeuUpload {
return async (file, progress, signal) => {
if (signal.aborted) throw new Error('Upload cancelled');
progress(0);
const target = await resolveTarget(file, signal);
if (signal.aborted) throw new Error('Upload cancelled');
const response = await expoFetch(target.url, {
method: 'PUT', signal,
headers: { 'Content-Type': file.mimeType || 'application/octet-stream', ...target.headers },
body: new File(file.uri),
});
if (!response.ok) throw new Error('Could not upload. Try again.');
if (signal.aborted) throw new Error('Upload cancelled');
progress(100);
};
}import { Component, signal } from '@angular/core';
import { NeuUploaderComponent } from '@neural-ui/native/uploader';
import type { NeuUploadItem } from '@neural-ui/native';
import { createUpload } from './upload-transport';
@Component({
selector: 'app-project-files',
imports: [NeuUploaderComponent],
template: `<neu-uploader label="Project documents" chooseLabel="Choose documents"
uploadLabel="Upload" removeLabel="Remove" [(files)]="files"
[upload]="upload" [types]="types" [maxFiles]="3" [maxBytes]="5000000" />`,
})
export class ProjectFilesComponent {
readonly files = signal<readonly NeuUploadItem[]>([]);
readonly types = ['application/pdf', 'image/*'];
readonly upload = createUpload(async (file, signal) => {
// Replace this endpoint and add your application's authentication.
const response = await fetch('https://api.example.com/uploads/target', {
method: 'POST', signal,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: file.name, size: file.size, type: file.mimeType }),
});
if (!response.ok) throw new Error('Could not prepare the upload. Try again.');
const target: unknown = await response.json();
if (!target || typeof target !== 'object' || !('url' in target) || typeof target.url !== 'string') {
throw new Error('Invalid upload destination');
}
const url = new URL(target.url);
if (url.protocol !== 'https:') throw new Error('An HTTPS upload destination is required');
return { url: url.href };
});
}The upload callback receives the file, a progress callback and an AbortSignal. Resolve only after a successful server response; throw on failure so Uploader displays the error and offers retry. Removing an active file or destroying Uploader aborts the signal. Pass it to every network request.
This fetch transport reports 0 and 100; it does not measure bytes in flight. Use a transport with upload-progress events if you need intermediate percentages. Removing an already uploaded item only removes it from the UI: deleting the remote object requires your application’s API. The web demo simulates an upload locally and sends no file to an external service.
API reference
Inputs
| Name | Type | Default | Binding | Description |
|---|---|---|---|---|
| label | string | 'Files' | Input | Human-readable name for the control, action or group. |
| disabled | boolean | false | Input | Disables user interaction. Values can still be supplied by the application. |
| types | readonly string[] | ['*/*'] | Input | Accepted file MIME types passed to the picker and file acceptance checks. |
| multiple | boolean | true | Input | Allows multiple choices or files where supported; check the declared model type. |
| maxFiles | number | 10 | Input | Maximum number of files accepted by the uploader. |
| maxBytes | number | 10 * 1024 * 1024 | Input | Maximum accepted size per file, in bytes. |
| chooseLabel | string | 'Choose files' | Input | Text or accessible name used for choose files. It does not perform the action by itself. |
| uploadLabel | string | 'Upload' | Input | Text or accessible name used for upload. It does not perform the action by itself. |
| removeLabel | string | 'Remove' | Input | Text or accessible name used for remove. It does not perform the action by itself. |
| stateLabels | Readonly<Record<"selected" | "uploading" | "complete" | "error", string>> | { selected: 'File selected', uploading: 'Uploading', complete: 'Upload complete', error: 'Upload failed', } | Input | Visible messages for selected, uploading, complete and error. Supply all four keys in your app language; the state IDs in files remain unchanged. |
| upload | NeuUpload | undefined | undefined | Input | Application-owned upload callback. Receives the file, a progress reporter and an AbortSignal; selecting files does not require a transport. |
Models
| Name | Type | Default | Binding | Description |
|---|---|---|---|---|
| files | readonly NeuUploadItem[] | [] | Model · two-way | Current file entries, including their upload state and progress. Bind a writable signal with [(files)]. |
Outputs
| Name | Payload | When it emits |
|---|---|---|
| filesChange | readonly NeuUploadItem[] | Emits the updated files value. [(files)] binds this output together with its model input. |
| selected | readonly NeuFile[] | Emits the files accepted by the picker; this is separate from upload completion. |
| completed | NeuFile | Emits the file whose upload callback completed successfully. |
| rejected | string | Emits the rejection message for a file that did not pass acceptance checks. |
Model state
Bind the signal with [(modelName)] without calling it. Use modelName() to read its value.
import { signal } from '@angular/core';
import type { NeuUploadItem } from '@neural-ui/native';
// Fields inside your component class
readonly files = signal<readonly NeuUploadItem[]>([]);Types and data interfaces
Use the imports in your application. The definitions below are provided for reference.
NeuFile
import type { NeuFile } from '@neural-ui/native';
// Public definition for reference only. Import this type; do not redeclare it.
export interface NeuFile {
readonly uri: string;
readonly name: string;
readonly size?: number;
readonly mimeType?: string;
}| Field | Type | Requirement | Meaning |
|---|---|---|---|
| uri | string | Required | Native image or file URI. |
| name | string | Required | File name supplied by the picker. |
| size | number | Optional | File size in bytes, when reported by the picker. |
| mimeType | string | Optional | File MIME type, when available. |
NeuUploadItem
import type { NeuUploadItem } from '@neural-ui/native';
import type { NeuFile } from '@neural-ui/native';
// Public definition for reference only. Import this type; do not redeclare it.
export interface NeuUploadItem {
readonly id: string;
readonly file: NeuFile;
readonly state: 'selected' | 'uploading' | 'complete' | 'error';
readonly progress: number;
readonly error?: string;
}| Field | Type | Requirement | Meaning |
|---|---|---|---|
| id | string | Required | Stable, unique identifier used for tracking and selection. |
| file | NeuFile | Required | Selected file data using NeuFile. |
| state | 'selected' | 'uploading' | 'complete' | 'error' | Required | Upload stage: selected, uploading, complete or error. |
| progress | number | Required | Upload progress as a percentage. |
| error | string | Optional | Optional upload error message. |
NeuUpload
import type { NeuUpload } from '@neural-ui/native';
import type { NeuFile } from '@neural-ui/native';
// Public definition for reference only. Import this type; do not redeclare it.
export type NeuUpload = (file: NeuFile, progress: (percent: number) => void, signal: AbortSignal) => Promise<void>;