| Documents | β | 5/4/2026 | Folder | |
| Music | β | 4/22/2026 | Folder | |
| Pictures | β | 5/8/2026 | Folder | |
| Projects | β | 5/14/2026 | Folder |
Enterprise file-browser composed from mp-splitter, mp-treeview, and mp-datatable Lit web components. Tree on the left, file list on the right, breadcrumb above, toolbar with search, view-mode toggle, and file operations. The live demo below is fully interactive: drag-drop files from your OS, rename inline, multi-select, cut/copy/paste, switch the language, simulate read-only permissions, or toggle on the lazy-load tree mode.
| Documents | β | 5/4/2026 | Folder | |
| Music | β | 4/22/2026 | Folder | |
| Pictures | β | 5/8/2026 | Folder | |
| Projects | β | 5/14/2026 | Folder |
(nodeOpen) for a filemultiple; Shift+click only ever adds to the selection, it never trims it.The component is intentionally event-driven: it renders the UX and emits intents, leaving every consumer to wire the backend, dialog style, and toast system of their own product. The recipes below cover the seven integration points a typical admin portal needs.
Every mutating action fires (operation) with a discriminated-union payload. The component is fire-and-forget β your handler runs the backend call, optionally marks the affected rows as pending, and reports failures through reportError() (which re-fires (errorReported) for your toast bus).
// Mark rows busy while the backend processes the change, then clear
// on completion. Failures route through reportError() β (errorReported).
onOperation(op: OperationEventDetail, fm: BsFileManagerComponent) {
if (op.kind === 'rename') {
fm.markPending(op.nodeId, 'rename');
this.api.renameNode(op.nodeId, op.newName).then(
() => { /* update local nodes signal */ },
err => fm.reportError(err.message, op.nodeId),
).finally(() => fm.clearPending(op.nodeId));
}
// β¦handle 'delete', 'new-folder', 'paste' the same way.
}
Drag-drop and the touch upload button both fire (uploadRequest) carrying pre-registered UploadEntry IDs. Drive your real upload (XHR / fetch / S3 SDK), then push percentage updates into the component via reportUploadProgress(id, pct, status?). The uploads signal on the component reflects the in-flight set so you can render a progress UI wherever you like (the live demo above renders it directly below the file-manager).
// The (uploadRequest) event carries pre-registered UploadEntry IDs.
// Drive your real XHR / fetch upload, then push progress into the WC
// so it can render the in-flight bar wherever your app prefers.
onUpload(req: UploadRequestEventDetail, fm: BsFileManagerComponent) {
for (const entry of req.uploads) {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.addEventListener('progress', e =>
fm.reportUploadProgress(entry.id, Math.round((e.loaded / e.total) * 100), 'uploading'));
xhr.addEventListener('load', () =>
fm.reportUploadProgress(entry.id, 100, 'done'));
xhr.addEventListener('error', () =>
fm.reportUploadProgress(entry.id, 0, 'error', 'Network error'));
const fd = new FormData();
fd.append('file', entry.file);
xhr.send(fd);
}
}
Most enterprise apps want their own modal styling instead of the browser dialogs. Wire a DialogResolver to your modal service (typically a thin wrapper around bs-modal). When unset, the component falls back to the native dialogs so basic usage still works.
// Replace the native window.confirm / window.prompt with a bs-modal
// (or your design system's dialog). The resolver returns a Promise
// resolving to a boolean (for confirm) or string|null (for prompt).
dialogResolver: DialogResolver = req => {
if (req.kind === 'confirm') {
return this.modalService.confirm({ message: req.message }); // your wrapper
}
return this.modalService.prompt({ label: req.label, defaultValue: req.defaultValue });
};
// template:
// <bs-file-manager [dialogResolver]="dialogResolver" ...></bs-file-manager>
When a paste or upload would overwrite a same-name entry in the target folder, the component calls conflictResolver once per conflict. Returning 'skip' filters the source out of the operation; 'rename' includes the consumer's chosen newName in the resulting (operation) / (uploadRequest) payload. Unset = silent replace (preserves the v1 behaviour).
// Asked once per same-name overlap when the user pastes or drops
// files. Returning 'skip' filters that source out of the operation.
conflictResolver: ConflictResolver = async req => {
const choice = await this.modalService.open(ConflictDialogComponent, {
existingNode: req.existingNode,
incomingName: req.incomingName,
});
// choice is 'replace' | 'skip' | { action: 'rename', newName: string }
return choice;
};
Real file systems aren't materialisable up front. Bind a loadChildren async callback and seed [nodes] with only the top-level entries. The component marks every unloaded folder with a chevron, shows a spinner during the fetch, and merges the returned children back into its store β no consumer-side coordination beyond returning the right data.
// Lazy tree: seed only the top level; load children on first expand.
// The component merges loaded nodes into its local store and re-renders.
nodes = signal<FileSystemNode[]>(this.api.getRoots());
loadChildren = (parentId: string) =>
this.api.listChildren(parentId); // returns Promise<FileSystemNode[]>
// template:
// <bs-file-manager [nodes]="nodes()" [loadChildren]="loadChildren"></bs-file-manager>
Every visible string + ARIA label routes through FileManagerMessages. Pass a partial overrides object via [messages]; unset keys fall back to DEFAULT_FILE_MANAGER_MESSAGES (English). Locale switching works at runtime β the demo above uses three locales.
// Overrides are partial β unset keys fall back to English defaults.
messages = computed<Partial<FileManagerMessages>>(() => ({
home: this.t('fm.home'),
newFolder: this.t('fm.new_folder'),
delete: this.t('fm.delete'),
deleteConfirm: n => this.t('fm.delete_confirm', { count: n }),
// β¦all keys your locale needs
}));
Beyond the global [allowOperations] input, every FileSystemNode can carry its own allowOperations partial. The toolbar / context-menu buttons gate against the worst case across the current selection (a multi-row delete is disabled if any selected row is locked).
// Mark a folder read-only by setting allowOperations on the node itself.
// Each falsy key disables that operation for the node; unset keys fall
// back to the global [allowOperations] input.
const node: FileSystemNode = {
id: 'shared/marketing',
parentId: null,
name: 'Marketing (read-only)',
type: 'folder',
allowOperations: { rename: false, delete: false, cut: false, newFolder: false },
};
Failures from your backend never need to reach into the component's UI directly. Call fm.reportError(message, nodeId?) from your handler and listen on (errorReported) at the template level β that's where you wire the toast / snackbar.
// Surface errors from your backend through the (errorReported) event.
// The component fires this whenever reportError() is called β wire
// it to your toast / snackbar service.
onError(detail: { kind: string; message: string; nodeId?: string }) {
this.toastService.danger(detail.message);
}
// template:
// <bs-file-manager (errorReported)="onError($event)"></bs-file-manager>
kind in onOperation + the upload pipeline.(errorReported); route to your snackbar.dialogResolver to bs-modal if you don't want native browser dialogs.conflictResolver if "silent overwrite" isn't your spec.allowOperations from your auth layer.[messages].[loadChildren] if folder cardinality is unbounded.