Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/upload-solid-2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solid-primitives/upload": minor
---

Migrate to Solid.js 2.0 (beta.14)

- Updated peer dependencies to `solid-js@^2.0.0-beta.14` and `@solidjs/web@^2.0.0-beta.14`
- `isServer` is now imported from `@solidjs/web` (moved out of `solid-js/web`)
- `createDropzone`: now returns a `setRef` ref callback; event listeners are attached when the ref is assigned and cleaned up via `onCleanup` registered through `runWithOwner` back into the reactive owner scope
- `fileUploader`: replaced the `use:fileUploader` directive (removed in Solid 2.0) with a **ref callback factory** — use `ref={fileUploader(opts)}` instead of `use:fileUploader={opts}`
347 changes: 318 additions & 29 deletions packages/upload/README.md

Large diffs are not rendered by default.

126 changes: 0 additions & 126 deletions packages/upload/dev/index.tsx

This file was deleted.

16 changes: 11 additions & 5 deletions packages/upload/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/upload",
"version": "0.1.4",
"version": "0.2.0",
"description": "Primitives for uploading files.",
"author": "Rustam Ashurmatov <rr.ashurmatov.21@gmail.com>",
"license": "MIT",
Expand All @@ -13,8 +13,12 @@
"name": "upload",
"stage": 0,
"list": [
"createFilePicker",
"createFileUploader",
"createDropzone"
"fileSender",
"fileUploader",
"createDropzone",
"dropzone"
],
"category": "Browser APIs",
"gzip": 1035
Expand Down Expand Up @@ -50,13 +54,15 @@
"test:ssr": "pnpm run vitest --mode ssr"
},
"dependencies": {
"@solid-primitives/utils": "workspace:^"
"@solid-primitives/event-listener": "workspace:*"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.14",
"solid-js": "^2.0.0-beta.14"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.14",
"solid-js": "2.0.0-beta.14"
}
}
137 changes: 81 additions & 56 deletions packages/upload/src/createDropzone.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { createSignal, type JSX, onCleanup, onMount } from "solid-js";
import { isServer } from "solid-js/web";
import { noop } from "@solid-primitives/utils";
import { createSignal, flush } from "solid-js";
import { isServer } from "@solidjs/web";
import { createEventListenerMap } from "@solid-primitives/event-listener";
import { transformFiles } from "./helpers.js";
import type { UploadFile, Dropzone, DropzoneOptions } from "./types.js";

/**
* Primitive to make working with dropzones easier.
*
* @returns `setRef`
* @returns `ref`
* @returns `files`
* @returns `error` - Reactive error from the last drag callback, cleared on next drop
* @returns `isLoading` - True while the `onDrop` callback is pending
* @returns `isDragging`
* @returns `removeFile`
* @returns `clearFiles`
*
* @example
* ```ts
* // run async user callback
* const { setRef: dropzoneRef1, files: droppedFiles1 } = createDropzone({
* const { ref: dropzoneRef, files: droppedFiles, error } = createDropzone({
* onDrop: async files => {
* await doStuff(2);
* files.forEach(f => console.log(f));
* },
* onDragStart: files => console.log("drag start")
* onDragStart: files => files.forEach(f => console.log(f)),
* onDragOver: files => console.log("drag over")
* });
Expand All @@ -32,77 +32,84 @@ function createDropzone<T extends HTMLElement = HTMLElement>(
): Dropzone<T> {
if (isServer) {
return {
setRef: noop,
ref: () => {},
files: () => [],
error: () => null,
isLoading: () => false,
isDragging: () => false,
removeFile: noop,
clearFiles: noop,
removeFile: () => {},
clearFiles: () => {},
};
}
const [files, setFiles] = createSignal<UploadFile[]>([]);
const [error, setError] = createSignal<unknown>(null);
const [isLoading, setIsLoading] = createSignal(false);
const [isDragging, setIsDragging] = createSignal(false);

let ref: T | undefined = undefined;

const setRef = (r: T) => {
ref = r;
const runCallback = async (
callback: ((files: UploadFile[]) => void | Promise<void>) | undefined,
parsedFiles: UploadFile[],
) => {
try {
await callback?.(parsedFiles);
} catch (err) {
setError(err);
}
};

const onDragStart: JSX.EventHandler<T, DragEvent> = event => {
setIsDragging(true);
Promise.resolve(options?.onDragStart?.(transformFiles(event.dataTransfer?.files || null)));
const onDragStart = (event: DragEvent) => {
void runCallback(options?.onDragStart, transformFiles(event.dataTransfer?.files || null));
};
const onDragEnd: JSX.EventHandler<T, DragEvent> = event => {
setIsDragging(false);
Promise.resolve(options?.onDragEnd?.(transformFiles(event.dataTransfer?.files || null)));
const onDragEnd = (event: DragEvent) => {
void runCallback(options?.onDragEnd, transformFiles(event.dataTransfer?.files || null));
};

const onDragEnter: JSX.EventHandler<T, DragEvent> = event => {
Promise.resolve(options?.onDragEnter?.(transformFiles(event.dataTransfer?.files || null)));
const onDragEnter = (event: DragEvent) => {
setIsDragging(true);
void runCallback(options?.onDragEnter, transformFiles(event.dataTransfer?.files || null));
};
const onDragLeave: JSX.EventHandler<T, DragEvent> = event => {
Promise.resolve(options?.onDragLeave?.(transformFiles(event.dataTransfer?.files || null)));
const onDragLeave = (event: DragEvent) => {
setIsDragging(false);
void runCallback(options?.onDragLeave, transformFiles(event.dataTransfer?.files || null));
};
const onDragOver: JSX.EventHandler<T, DragEvent> = event => {
const onDragOver = (event: DragEvent) => {
event.preventDefault();
Promise.resolve(options?.onDragOver?.(transformFiles(event.dataTransfer?.files || null)));
void runCallback(options?.onDragOver, transformFiles(event.dataTransfer?.files || null));
};
const onDrag: JSX.EventHandler<T, DragEvent> = event => {
Promise.resolve(options?.onDrag?.(transformFiles(event.dataTransfer?.files || null)));
const onDrag = (event: DragEvent) => {
void runCallback(options?.onDrag, transformFiles(event.dataTransfer?.files || null));
};

const onDrop: JSX.EventHandler<T, DragEvent> = event => {
const onDrop = (event: DragEvent) => {
event.preventDefault();

setIsDragging(false);
const parsedFiles = transformFiles(event.dataTransfer?.files || null);
setFiles(parsedFiles);

Promise.resolve(options?.onDrop?.(parsedFiles));
setError(null);
setIsLoading(true);
void (async () => {
try {
await options?.onDrop?.(parsedFiles);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
})();
};
Comment thread
davedbase marked this conversation as resolved.

onMount(() => {
if (!ref) return;
const [refTarget, setRefTarget] = createSignal<T | undefined>(undefined);

// TODO: Should event.stopPropagation() or event.preventDefault() in handlers below?
ref.addEventListener("dragstart", onDragStart as any);
ref.addEventListener("dragenter", onDragEnter as any);
ref.addEventListener("dragend", onDragEnd as any);
ref.addEventListener("dragleave", onDragLeave as any);
ref.addEventListener("dragover", onDragOver as any);
ref.addEventListener("drag", onDrag as any);
ref.addEventListener("drop", onDrop as any);

onCleanup(() => {
ref?.removeEventListener("dragstart", onDragStart as any);
ref?.removeEventListener("dragenter", onDragEnter as any);
ref?.removeEventListener("dragend", onDragEnd as any);
ref?.removeEventListener("dragleave", onDragLeave as any);
ref?.removeEventListener("dragover", onDragOver as any);
ref?.removeEventListener("drag", onDrag as any);
ref?.removeEventListener("drop", onDrop as any);
});
createEventListenerMap(refTarget as () => T, {
dragstart: onDragStart,
dragenter: onDragEnter,
dragend: onDragEnd,
dragleave: onDragLeave,
dragover: onDragOver,
drag: onDrag,
drop: onDrop,
});

const ref = (el: T) => { setRefTarget(() => el); flush(); };

const removeFile = (fileName: string) => {
setFiles(prev => prev.filter(f => f.name !== fileName));
};
Expand All @@ -112,12 +119,30 @@ function createDropzone<T extends HTMLElement = HTMLElement>(
};

return {
setRef,
ref,
files,
error,
isLoading,
isDragging,
removeFile,
clearFiles,
};
}

export { createDropzone };
/**
* Ref callback factory for dropzone elements. Returns a single value that is
* both a ref callback and a reactive state object — use it directly as a `ref`
* while reading `.files`, `.isDragging`, etc. from the same reference.
*
* @example
* ```tsx
* const dz = dropzone({ onDrop: handleFiles });
* <div ref={dz} class={dz.isDragging() ? "dragging" : ""} />
* ```
*/
function dropzone<T extends HTMLElement = HTMLElement>(options?: DropzoneOptions) {
const { ref, ...state } = createDropzone<T>(options);
return Object.assign(ref, state);
}

export { createDropzone, dropzone };
Loading