Angular File Picker Service


Description

This is a simple file picker service that can be used to pick files from the user. fast and easy. without the need to create a file input element in the dom.


// file-picker.service.ts

import { wait } from '@lib/common/fun';

export class FilePicker {
  static #ref: HTMLInputElement;

  static #getFileInput(): HTMLInputElement {
    if (this.#ref) return this.#ref;
    const input = document.createElement('input');
    input.type = 'file';
    this.#ref = input;
    return this.#ref;
  }

  public static async pick(opt: { multiple?: boolean; accept?: string } = {}): Promise<File[]> {
    const input = this.#getFileInput();
    input.multiple = opt.multiple;
    input.accept = opt.accept ?? '*/*';
    const onWindowFocusP = new Promise((res) => window.addEventListener('focus', res, { once: true }));
    input.click();
    await onWindowFocusP;
    await wait(100);
    const files = Array.from(input.files ?? []);
    input.value = '';
    return files;
  }
}

// to use it in angular
@Injectable({ providedIn: 'root' })
export class FilePickerService {
  readonly #ngZone = inject(NgZone);

  pick(opt: { multiple?: boolean; accept?: string } = {}): Promise<File[]> {
    return this.#ngZone.runOutsideAngular(() => FilePicker.pick(opt));
  }
}

definition of wait function

export const wait = (x: number, signal?: AbortSignal): Promise<number> => {
  return new Promise((s, f) => {
    const id = setTimeout(s, x, x);
    signal?.addEventListener('abort', () => {
      clearTimeout(id);
      f('AbortError');
    });
  });
};

Setup in Angular

Import the FilePickerService in your app module/component providers

Note: runOutsideAngular is used to prevent angular from running change detection when the user selects a file. so that it doesn’t cause any performance issues.

Usage

For plain typescript/javascript

import { FilePicker } from './file-picker';

const [file] = await FilePicker.pick();
console.log(file);

// select multiple files
const files = await FilePicker.pick({ multiple: true });
console.log(files);

// select only images
const files = await FilePicker.pick({ accept: 'image/*' });
console.log(files);

For Angular

import { Component, inject } from '@angular/core';
import { FilePickerService } from './file-picker.service';

@Component({
  selector: 'test-page',
  standalone: true,
  imports: [CommonModule],
  template: `<button (click)="pick()">Pick</button>`,
})
export class TestComponent implements OnInit {
  readonly #filePickerService = inject(FilePickerService);

  async pick(id: number) {
    // single file
    const [file] = await this.#filePickerService.pick();
    console.log(file);
    // multiple files
    const files = await this.#filePickerService.pick({ multiple: true });
    console.log(files);
    // only images
    const files = await this.#filePickerService.pick({ accept: 'image/*' });
    console.log(files);
  }
}