Skip to content

Quickstart

A complete, working explorer in about five minutes. Copy each block in order — nothing is elided, and the result runs.

We'll start from a realistic API response, not tidy sample data.

1. Install

bash
npm install @document-explorer/react
bash
npm install @document-explorer/angular
bash
npm install @document-explorer/core @document-explorer/theme

2. The data you already have

Say your endpoint returns this. Note the shape: snake_case keys, numeric ids, nested children, a kind field instead of type.

json
[
  {
    "document_id": 1,
    "document_title": "Engineering",
    "kind": "DIRECTORY",
    "folder_id": null,
    "children": [
      {
        "document_id": 100,
        "document_title": "system-design.pdf",
        "kind": "FILE",
        "folder_id": 1,
        "file_size": 2450000,
        "modified": "2026-08-22T10:00:00Z",
        "download": "/api/documents/100/download",
        "extra": { "owner": "Ada Lovelace", "status": "approved" }
      }
    ]
  }
]

You do not transform this. You describe it once.

3. Describe the shape

ts
// mapper.ts
import type { DocumentMapper } from '@document-explorer/core';

export interface ApiDocument {
  document_id: number;
  document_title: string;
  kind: 'DIRECTORY' | 'FILE';
  folder_id: number | null;
  file_size?: number;
  modified?: string;
  download?: string;
  extra?: { owner?: string; status?: string };
  children?: ApiDocument[];
}

export const mapper: DocumentMapper<ApiDocument> = {
  id: 'document_id',          // numbers are coerced to strings for you
  name: 'document_title',
  type: 'kind',               // 'DIRECTORY' is recognised as a folder
  parentId: 'folder_id',
  size: 'file_size',
  updatedAt: 'modified',
  downloadUrl: 'download',
  metadata: 'extra',          // anything else you want to show
};

Every field takes a key, a dot path ('audit.created_by.name'), or a function ((d) => d.kind === 'FILE' ? 'file' : 'folder'). Strings are enough here, and a string-only mapper has a bonus: it can cross a React Server Components boundary. See compatibility.

4. Render it

tsx
import { DocumentExplorer } from '@document-explorer/react';
import '@document-explorer/react/styles.css';
import { mapper, type ApiDocument } from './mapper';

export function Documents({ data }: { data: ApiDocument[] }) {
  return (
    <DocumentExplorer<ApiDocument>
      data={data}
      mapper={mapper}
      className="documents"
    />
  );
}
ts
import { Component, input } from '@angular/core';
import { DocumentExplorerComponent } from '@document-explorer/angular';
import { mapper, type ApiDocument } from './mapper';

@Component({
  selector: 'app-documents',
  standalone: true,
  imports: [DocumentExplorerComponent],
  template: `<doc-explorer [data]="data()" [mapper]="mapper" class="documents" />`,
})
export class DocumentsComponent {
  readonly data = input.required<ApiDocument[]>();
  protected readonly mapper = mapper;
}

Angular needs the stylesheet registered once, in angular.json:

json
"styles": ["@document-explorer/angular/styles.css", "src/styles.css"]

5. Give it a height

The explorer fills its container and scrolls internally, so it needs one:

css
.documents {
  height: 480px;
}

That is a working explorer — folder navigation, breadcrumbs, sorting, file type icons, formatted sizes and dates.

6. Turn on what you need

Nothing below is required; add only what your product actually has.

tsx
<DocumentExplorer<ApiDocument>
  data={data}
  mapper={mapper}
  className="documents"

  title="Documents"
  search={{ scope: 'global', searchableFields: ['metadata.owner'] }}
  selection={{ mode: 'multiple', folders: false }}
  actions={{ view: true, download: true }}
  views={['list', 'grid', 'tree']}
  resizableColumns

  onDocumentOpen={(item) => window.open(item.viewUrl)}
  onDownload={(item) => download(item.downloadUrl!)}
  onSelectionChange={setSelected}
/>
html
<doc-explorer
  [data]="data()"
  [mapper]="mapper"
  class="documents"

  title="Documents"
  [search]="search"
  [selection]="selection"
  [actions]="actions"
  [views]="views"
  [resizableColumns]="true"

  (documentOpen)="open($event)"
  (documentDownload)="download($event)"
  (selectionChange)="selected.set($event)"
/>

Angular: declare object inputs as fields

An object literal written inline in a template is a new object on every change detection pass. Put search, selection and actions on the component.

7. Show your own columns

tsx
import { avatarColumn, badgeColumn } from '@document-explorer/react';

<DocumentExplorer
  columns={[
    'name',
    avatarColumn({ key: 'metadata.owner', label: 'Owner' }),
    badgeColumn({ key: 'metadata.status', label: 'Status' }),
    'size',
    'updatedAt',
  ]}
/>

avatarColumn renders a person — initials or a photo, name beside it or on hover. badgeColumn gives approved a green tone with no configuration. Both are ordinary ColumnDefs, so anything they do you can also write by hand with render.

See custom rendering for the full picture.

8. Wire up the actions

The library never performs an action — it renders the menu and tells you what was clicked:

tsx
<DocumentExplorer
  actions={[
    { id: 'view', label: 'View' },
    { id: 'download', label: 'Download' },
    { id: 'share', label: 'Share' },
  ]}
  onAction={(actionId, item) => {
    if (actionId === 'view') openViewer(item);
    if (actionId === 'download') download(item.downloadUrl!);
    if (actionId === 'share') openShareDialog(item);
  }}
/>

share means whatever you decide. The library needs no idea.

9. Make it yours

Override tokens; you should never need a rule against a .de-* class:

css
.documents {
  --de-accent: #7c3aed;
  --de-radius: 4px;
  --de-row-height: 48px;
  --de-font-size: 13px;
}

Dark mode follows prefers-color-scheme on its own. For a toggle, set data-de-theme="dark" on any ancestor. Full list in theming.

Handling the messy parts

Loading and errors. The explorer fetches nothing, so tell it what yours is doing:

tsx
<DocumentExplorer data={data ?? []} loading={isLoading} error={error} />

Bad rows. Duplicate ids, missing parents and cycles are repaired and reported rather than thrown. Inspect them when you want to know about upstream problems:

ts
import { normalize } from '@document-explorer/core';

const { warnings } = normalize(data, { mapper });
warnings.forEach((w) => console.warn(w.code, w.message));

Where to go next

Released under the MIT License.