Packages

React inside Phoenix LiveView — async code splitting, client-side props diffing, and zero-config component discovery.

Retired package: Release invalid - Not production-ready

Current section

Files

Jump to
react_phx assets useLiveUpload.ts
Raw

assets/useLiveUpload.ts

import { useCallback, useMemo } from "react";
import { useLiveReact } from "./context";
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
export interface UploadEntry {
ref: string;
name: string;
size: number;
progress: number;
valid: boolean;
errors: string[];
}
export interface UseLiveUploadReturn {
entries: UploadEntry[];
addFiles: (files: File[]) => void;
showFilePicker: () => void;
submit: () => void;
cancel: (ref: string) => void;
clear: () => void;
progress: number;
isValid: boolean;
}
// ---------------------------------------------------------------------------
// Server upload config shape (encoded @uploads.avatar prop)
// ---------------------------------------------------------------------------
interface UploadConfig {
ref: string;
name: string;
accept: string[] | string;
max_entries: number;
max_file_size: number;
entries: UploadEntry[];
errors: string[];
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
export function useLiveUpload(
uploadConfig: UploadConfig,
options?: { submitEvent?: string },
): UseLiveUploadReturn {
const { pushEvent, upload } = useLiveReact();
const submitEvent = options?.submitEvent ?? "save";
const entries: UploadEntry[] = uploadConfig?.entries ?? [];
const progress = useMemo(() => {
if (entries.length === 0) return 0;
const total = entries.reduce((sum, e) => sum + e.progress, 0);
return total / entries.length;
}, [entries]);
const isValid = useMemo(() => {
const configErrors = uploadConfig?.errors ?? [];
if (configErrors.length > 0) return false;
return entries.every((e) => e.valid);
}, [entries, uploadConfig?.errors]);
const addFiles = useCallback(
(files: File[]) => {
upload(uploadConfig.name, files);
},
[upload, uploadConfig?.name],
);
const showFilePicker = useCallback(() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = (uploadConfig?.max_entries ?? 1) > 1;
const accept = Array.isArray(uploadConfig?.accept)
? uploadConfig.accept.join(",")
: uploadConfig?.accept || "";
if (accept) {
input.accept = accept;
}
input.onchange = () => {
if (input.files && input.files.length > 0) {
addFiles(Array.from(input.files));
}
};
input.click();
}, [addFiles, uploadConfig?.max_entries, uploadConfig?.accept]);
const cancel = useCallback(
(ref: string) => {
pushEvent("cancel-upload", { ref });
},
[pushEvent],
);
const clear = useCallback(() => {
for (const entry of entries) {
pushEvent("cancel-upload", { ref: entry.ref });
}
}, [entries, pushEvent]);
const submit = useCallback(() => {
pushEvent(submitEvent, {});
}, [pushEvent, submitEvent]);
return {
entries,
addFiles,
showFilePicker,
submit,
cancel,
clear,
progress,
isValid,
};
}