Custom rendering
React
<DocumentExplorer
renderers={{
file: (item) => <MyFileRow file={item} />,
folder: (item) => <MyFolderRow folder={item} />,
row: (item) => <MyEntireRow item={item} />, // replaces the whole row
icon: (item) => <MyIcon type={item.mimeType} />,
empty: () => <MyEmptyState />,
loading: () => <MySkeleton />,
error: (error) => <MyError error={error} />,
toolbar: () => <button onClick={upload}>Upload</button>,
}}
/>Anything you do not provide falls back to the default rendering.
For a single column, render on a ColumnDef is usually simpler than replacing the whole row — see avatars and other cell content below.
Angular
Angular's equivalent is ng-template slots:
<doc-explorer [data]="documents">
<ng-template #deFile let-item>
<my-file-row [file]="item" />
</ng-template>
<ng-template #deFolder let-item>
<my-folder-row [folder]="item" />
</ng-template>
<ng-template #deEmpty>
<my-empty-state />
</ng-template>
<ng-template #deToolbar>
<button (click)="upload()">Upload</button>
</ng-template>
</doc-explorer>The item is available both as let-item and as the implicit value (let-item / let-x).
Cell content
To change one column rather than a whole row — an avatar beside an owner, a status pill, a badge — render just that cell.
React
const ownerColumn: ColumnDef = {
key: 'metadata.owner',
label: 'Owner',
sortable: true,
minWidth: 120,
render: (item) => {
const owner = item.metadata?.owner as string | undefined;
if (!owner) return null;
return (
<>
<span className="de-avatar" aria-hidden="true">{initials(owner)}</span>
<span className="de-avatar-label">{owner}</span>
</>
);
},
};
<DocumentExplorer columns={['name', ownerColumn, 'size']} />Angular
Angular matches templates to columns with the deColumn directive:
import { DeColumnTemplateDirective } from '@document-explorer/angular';<doc-explorer [data]="documents" [columns]="columns">
<ng-template deColumn="metadata.owner" let-item let-value="value">
@if (value) {
<span class="de-avatar" aria-hidden="true">{{ initials(value) }}</span>
<span class="de-avatar-label">{{ value }}</span>
}
</ng-template>
</doc-explorer>The template context gives you both the whole item and value — the column's already-formatted text — so you rarely have to re-derive it.
Badges
A status, a state, a category — built in, with defaults you can throw away.
import { badgeColumn } from '@document-explorer/react';
<DocumentExplorer columns={['name', badgeColumn({ key: 'metadata.status' }), 'size']} />readonly status = badgeColumn({ key: 'metadata.status' });<ng-template deColumn="metadata.status" let-item>
<de-badge-cell [item]="item" columnKey="metadata.status" [options]="status.options" />
</ng-template>Defaults, not decisions
Common values get a conventional tone with no configuration at all:
| Value | Tone |
|---|---|
approved, active, complete, published, done | success |
review, in_review, pending | warning |
rejected, failed, error, expired | danger |
draft, archived | neutral |
new | info |
Matching is case-insensitive, and labels are humanised — in_review renders as “In review”. Anything unrecognised is neutral.
Overriding all of it
badgeColumn({ key: 'metadata.status', tones: { draft: 'info' } }) // remap one value
badgeColumn({ key: 'metadata.status', tones: (v) => myTone(v) }) // decide it yourself
badgeColumn({ key: 'metadata.status', format: (v) => v.toUpperCase() }) // your own label
badgeColumn({ key: 'metadata.status', variant: 'pill' }) // filled pill
badgeColumn({ key: 'metadata.status', fallbackTone: 'info' }) // different defaultA tones function wins over the map, which wins over the conventions — so you can override one value, or take the decision away entirely.
Colours
The five tones read from theme tokens, so recolouring every badge in the app is a stylesheet change rather than a prop:
.my-explorer {
--de-tone-success: #15803d;
--de-tone-success-soft: #f0fdf4; /* pill background */
}Rows with no value render an empty cell, not a meaningless grey pill.
If none of that fits, a plain render (React) or deColumn template (Angular) puts the cell entirely in your hands — see cell content.
Avatars
Showing a person in a column is common enough that it is built in.
React
import { DocumentExplorer, avatarColumn } from '@document-explorer/react';
<DocumentExplorer
columns={['name', avatarColumn({ key: 'metadata.owner' }), 'size']}
/>Angular
readonly owner = avatarColumn({ key: 'metadata.owner' });
readonly columns = ['name', this.owner.column, 'size'];<doc-explorer [data]="documents" [columns]="columns">
<ng-template deColumn="metadata.owner" let-item>
<de-avatar-cell [item]="item" columnKey="metadata.owner" [options]="owner.options" />
</ng-template>
</doc-explorer>Angular cannot return markup from a plain object the way React's render can, so avatarColumn hands back the column and the options to feed <de-avatar-cell>.
Two independent settings
This is the part worth reading slowly, because the two are easy to conflate:
display— how much of the person to show: the avatar, the name, or both.image— what the avatar is: a photo, or generated initials.
They compose freely. A photo still shows the name unless you also ask for display: 'avatar':
display: 'both' (default) | display: 'avatar' | display: 'name' | |
|---|---|---|---|
no image | initials + name | initials, name on hover | name only |
with image | photo + name | photo, name on hover | name only (no avatar drawn) |
avatarColumn({ key: 'metadata.owner' })
// initials + name
avatarColumn({ key: 'metadata.owner', display: 'avatar' })
// initials alone, name on hover
avatarColumn({ key: 'metadata.owner', image: 'metadata.pic' })
// photo + name
avatarColumn({ key: 'metadata.owner', image: 'metadata.pic', display: 'avatar' })
// photo alone, name on hover ← the compact onedisplay: 'avatar' is for narrow columns: the name moves to a tooltip and to screen-reader-only text, so hovering reveals it and assistive technology still reads it. An avatar with no way to recover the name is a puzzle, not a feature, so the tooltip is on by default in that mode and off when the name is already visible. Force it either way with tooltip: true | false.
display: 'name' draws no avatar at all, so an image alongside it has nothing to render into — the combination is harmless, just inert.
An image falls back to initials if it fails to load, so a broken URL degrades to something readable rather than an empty circle.
See it
The Playground in the React example has display and image as separate controls, so you can flip between all six combinations and copy the resulting props.
Options
| Option | Meaning |
|---|---|
key | Where the name lives. Also the column key. |
name | Read the name from somewhere else — dot path or function. |
image | Image URL — dot path or function. |
display | 'both' (default), 'avatar', 'name'. |
tooltip | Force the hover name on or off. |
color | A fixed colour, or a function of the name. |
fallback | Label for rows with no name, e.g. 'Unassigned'. |
Anything a ColumnDef accepts — label, width, minWidth, sortable, resizable — passes straight through:
avatarColumn({ key: 'metadata.owner', label: 'Assignee', width: '220px', display: 'avatar' })Rows with no name and no image render an empty cell, rather than an anonymous grey circle on every row.
Colours
Each name gets a stable swatch from six theme tokens, so the same person is the same colour everywhere and in both frameworks — and the palette follows light and dark:
.my-explorer {
--de-avatar-1: #0f766e;
--de-avatar-size: 28px;
--de-avatar-foreground: #fff;
}Pass color to override entirely.
Rolling your own
avatarColumn is only a ColumnDef with a render, so nothing stops you writing the cell by hand — see cell content above. These stylesheet helpers are available either way:
| Class | What it does |
|---|---|
.de-avatar | Circle sized by --de-avatar-size, crops any <img> inside |
.de-avatar-label | Truncates the name with an ellipsis |
.de-avatar-stack | Overlaps several avatars, for a shared-with column |
A cell with custom content is laid out as a flex row rather than a text run, so an icon and a label sit side by side without extra wrapping.
Rebuilding the shell entirely
Every sub-component is exported, so you can keep the engine and write your own chrome:
import {
useDocumentExplorer,
Breadcrumbs,
SearchInput,
ListView,
} from '@document-explorer/react';
function MyExplorer({ data }) {
const { store, snapshot } = useDocumentExplorer({ data });
return (
<div>
<MyOwnHeader />
<Breadcrumbs items={snapshot.breadcrumbs} onNavigate={store.actions.openFolder} />
<ListView items={snapshot.items} /* … */ />
</div>
);
}Or drop the UI package altogether and drive the core store yourself.