Skip to main content
Neural Nativev0.1.0-beta.1Documentation

Import

component.ts
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
basic.component.ts
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 });
    });
  }
}
Interactive playground

Change the settings and try the result.

Device captures shown in English.

Uploader · android · native capture
AndroidNative screenshot
Uploader · ios · native capture
iOSNative screenshot

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.

terminal
npx expo install expo-file-system

Create 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.

src/app/upload-transport.ts
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);
  };
}
src/app/project-files.component.ts
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.

Expo: upload files with fetch ↗

API reference

Inputs

NameTypeDefaultBindingDescription
labelstring'Files'InputHuman-readable name for the control, action or group.
disabledbooleanfalseInputDisables user interaction. Values can still be supplied by the application.
typesreadonly string[]['*/*']InputAccepted file MIME types passed to the picker and file acceptance checks.
multiplebooleantrueInputAllows multiple choices or files where supported; check the declared model type.
maxFilesnumber10InputMaximum number of files accepted by the uploader.
maxBytesnumber10 * 1024 * 1024InputMaximum accepted size per file, in bytes.
chooseLabelstring'Choose files'InputText or accessible name used for choose files. It does not perform the action by itself.
uploadLabelstring'Upload'InputText or accessible name used for upload. It does not perform the action by itself.
removeLabelstring'Remove'InputText or accessible name used for remove. It does not perform the action by itself.
stateLabelsReadonly<Record<"selected" | "uploading" | "complete" | "error", string>>{ selected: 'File selected', uploading: 'Uploading', complete: 'Upload complete', error: 'Upload failed', }InputVisible messages for selected, uploading, complete and error. Supply all four keys in your app language; the state IDs in files remain unchanged.
uploadNeuUpload | undefinedundefinedInputApplication-owned upload callback. Receives the file, a progress reporter and an AbortSignal; selecting files does not require a transport.

Models

NameTypeDefaultBindingDescription
filesreadonly NeuUploadItem[][]Model · two-wayCurrent file entries, including their upload state and progress. Bind a writable signal with [(files)].

Outputs

NamePayloadWhen it emits
filesChangereadonly NeuUploadItem[]Emits the updated files value. [(files)] binds this output together with its model input.
selectedreadonly NeuFile[]Emits the files accepted by the picker; this is separate from upload completion.
completedNeuFileEmits the file whose upload callback completed successfully.
rejectedstringEmits 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.

model-fields.ts
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

NeuFile.ts
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;
}
FieldTypeRequirementMeaning
uristringRequiredNative image or file URI.
namestringRequiredFile name supplied by the picker.
sizenumberOptionalFile size in bytes, when reported by the picker.
mimeTypestringOptionalFile MIME type, when available.

NeuUploadItem

NeuUploadItem.ts
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;
}
FieldTypeRequirementMeaning
idstringRequiredStable, unique identifier used for tracking and selection.
fileNeuFileRequiredSelected file data using NeuFile.
state'selected' | 'uploading' | 'complete' | 'error'RequiredUpload stage: selected, uploading, complete or error.
progressnumberRequiredUpload progress as a percentage.
errorstringOptionalOptional upload error message.

NeuUpload

NeuUpload.ts
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>;