Skip to content
Draft
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.7)

- Updated peer dependencies to `solid-js@^2.0.0-beta.7` and `@solidjs/web@^2.0.0-beta.7`
- `isServer` is now imported from `@solidjs/web` (moved out of `solid-js/web`)
- `createDropzone`: replaced `onMount`/`onCleanup` with `onSettled` (returns cleanup function) per Solid 2.0 lifecycle API
- `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}`
4 changes: 2 additions & 2 deletions packages/audio/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const App: Component = () => {
<div class="flex flex-col items-center">
<div class="flex items-center justify-center space-x-4 rounded-full bg-white p-1 shadow">
<button
class="scale-200 flex cursor-pointer border-none bg-transparent"
class="flex scale-200 cursor-pointer border-none bg-transparent"
disabled={audio.state == AudioState.ERROR}
onClick={() => setPlaying(audio.state == AudioState.PLAYING ? false : true)}
>
Expand All @@ -97,7 +97,7 @@ const App: Component = () => {
step="0.1"
max={audio.duration}
value={audio.currentTime}
class="form-range w-40 cursor-pointer appearance-none rounded-3xl bg-gray-200 transition hover:bg-gray-300 focus:outline-none focus:ring-0"
class="form-range w-40 cursor-pointer appearance-none rounded-3xl bg-gray-200 transition hover:bg-gray-300 focus:ring-0 focus:outline-none"
/>
<div class="flex px-2">
<Icon class="w-6 text-blue-600" path={speakerWave} />
Expand Down
2 changes: 1 addition & 1 deletion packages/geolocation/dev/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const Client: Component = () => {
</div>
</div>
</Show>
<div class="w-100 h-100" ref={el => (ref = el)} />
<div class="h-100 w-100" ref={el => (ref = el)} />
<div class="p-4">
{location()?.latitude}, {location()?.longitude}
</div>
Expand Down
109 changes: 93 additions & 16 deletions packages/upload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,56 +16,133 @@ Primitive to make uploading files and making dropzones easier.
npm install @solid-primitives/upload
# or
yarn add @solid-primitives/upload
# or
pnpm add @solid-primitives/upload
```

> **Requires Solid.js 2.0 and `@solidjs/web` 2.0.**

## How to use it

### [createFileUploader](#createfileuploader)

A reactive primitive that opens the OS file-picker dialog and exposes the selected files as a signal.

```ts
// single files
// single file
const { files, selectFiles } = createFileUploader();
selectFiles([file] => console.log(file));
selectFiles(([file]) => console.log(file));

// multiple files
// multiple files with accept filter
const { files, selectFiles } = createFileUploader({ multiple: true, accept: "image/*" });
selectFiles(files => files.forEach(file => console.log(file)));
```

### use:fileUploader directive
**Returns:**

```ts
| Name | Type | Description |
| ------------- | ----------------------------------- | --------------------------------------------------------------- |
| `files` | `Accessor<UploadFile[]>` | Reactive list of selected files |
| `error` | `Accessor<unknown>` | Error thrown by the last `selectFiles` callback; `null` if none |
| `selectFiles` | `(callback?: UserCallback) => void` | Opens file-picker and runs optional callback |
| `removeFile` | `(fileName: string) => void` | Removes a file by name from the list |
| `clearFiles` | `() => void` | Clears all selected files |

### [fileUploader](#fileuploader-ref-callback)

A **ref callback factory** for `<input type="file">` elements (replaces the Solid 1.x `use:fileUploader` directive).

```tsx
const [files, setFiles] = createSignal<UploadFile[]>([]);
const [uploadError, setUploadError] = createSignal<unknown>(null);

<input
type="file"
multiple
use:fileUploader={{
ref={fileUploader({
userCallback: fs => fs.forEach(f => console.log(f)),
setFiles,
}}
onError: err => setUploadError(err),
})}
/>;
```

If `onError` is omitted, a rejection from `userCallback` propagates as an unhandled promise rejection.

> **Migration note (Solid 2.0):** The `use:fileUploader` directive syntax has been removed.
> Replace `use:fileUploader={opts}` with `ref={fileUploader(opts)}`.

### [createDropzone](#createdropzone)

```html
<div
ref={dropzoneRef}
style={{ width: "100px", height: "100px", background: "red" }}>
Dropzone
</div>
```
A reactive primitive for drag-and-drop file targets.

```ts
const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
```tsx
const {
setRef: dropzoneRef,
files: droppedFiles,
isDragging,
} = createDropzone({
onDrop: async files => {
await doStuff(2);
files.forEach(f => console.log(f));
},
onDragStart: files => files.forEach(f => console.log(f)),
onDragOver: files => console.log("drag over"),
});

<div
ref={dropzoneRef}
style={{ width: "100px", height: "100px", background: isDragging() ? "green" : "red" }}
>
Dropzone
</div>;
```

**Returns:**

| Name | Type | Description |
| ------------ | ---------------------------- | ------------------------------------------------------ |
| `setRef` | `(el: T) => void` | Ref callback — pass to the `ref` prop of an element |
| `files` | `Accessor<UploadFile[]>` | Reactive list of dropped files |
| `error` | `Accessor<unknown>` | Error thrown by the last drag callback; `null` if none |
| `isDragging` | `Accessor<boolean>` | `true` while a drag is in progress |
| `removeFile` | `(fileName: string) => void` | Removes a file by name from the list |
| `clearFiles` | `() => void` | Clears all dropped files |

**DropzoneOptions:**

| Callback | Type | Description |
| ------------- | -------------- | -------------------------------------- |
| `onDrop` | `UserCallback` | Fired when files are dropped |
| `onDragStart` | `UserCallback` | Fired when a drag starts |
| `onDragEnter` | `UserCallback` | Fired when dragged item enters element |
| `onDragEnd` | `UserCallback` | Fired when drag ends |
| `onDragLeave` | `UserCallback` | Fired when dragged item leaves element |
| `onDragOver` | `UserCallback` | Fired continuously while dragging over |
| `onDrag` | `UserCallback` | Fired on drag events |

## Types

```ts
type UploadFile = {
source: string; // blob URL (URL.createObjectURL)
name: string;
size: number;
file: File;
};

type UserCallback = (files: UploadFile[]) => void | Promise<void>;

type FileUploaderOptions = {
accept?: string;
multiple?: boolean;
};

type FileUploaderDirective = {
userCallback: UserCallback;
setFiles: Setter<UploadFile[]>;
onError?: (error: unknown) => void;
};
```

## Demo
Expand Down
6 changes: 2 additions & 4 deletions packages/upload/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { doStuff } from "../src/helpers.js";

import type { UploadFile } from "../src/types.js";

fileUploader;

const SingleFileUpload: Component = () => {
const { files, selectFiles } = createFileUploader();
const { files: filesAsync, selectFiles: selectFilesAsync } = createFileUploader();
Expand Down Expand Up @@ -99,10 +97,10 @@ const FileUploaderDirective: Component = () => {
<input
type="file"
multiple
use:fileUploader={{
ref={fileUploader({
userCallback: fs => fs.forEach(f => console.log(f)),
setFiles,
}}
})}
/>
<For each={files()}>{file => <p>{file.name}</p>}</For>
</div>
Expand Down
12 changes: 6 additions & 6 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 Down Expand Up @@ -48,14 +48,14 @@
"test": "pnpm run vitest",
"test:ssr": "pnpm run vitest --mode ssr"
},
"dependencies": {
"@solid-primitives/utils": "workspace:^"
},
"dependencies": {},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.7",
"solid-js": "^2.0.0-beta.7"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.7",
"solid-js": "2.0.0-beta.7"
}
}
79 changes: 43 additions & 36 deletions packages/upload/src/createDropzone.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createSignal, type JSX, onCleanup, onMount } from "solid-js";
import { isServer } from "solid-js/web";
import { noop } from "@solid-primitives/utils";
import { createSignal, type JSX, getOwner, onCleanup, runWithOwner } from "solid-js";
import { isServer } from "@solidjs/web";
import { transformFiles } from "./helpers.js";
import type { UploadFile, Dropzone, DropzoneOptions } from "./types.js";

Expand All @@ -9,19 +8,18 @@ import type { UploadFile, Dropzone, DropzoneOptions } from "./types.js";
*
* @returns `setRef`
* @returns `files`
* @returns `error` - Reactive error from the last drag callback, cleared on next drop
* @returns `isDragging`
* @returns `removeFile`
* @returns `clearFiles`
*
* @example
* ```ts
* // run async user callback
* const { setRef: dropzoneRef1, files: droppedFiles1 } = createDropzone({
* const { setRef: 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,58 +30,64 @@ function createDropzone<T extends HTMLElement = HTMLElement>(
): Dropzone<T> {
if (isServer) {
return {
setRef: noop,
setRef: () => {},
files: () => [],
error: () => null,
isDragging: () => false,
removeFile: noop,
clearFiles: noop,
removeFile: () => {},
clearFiles: () => {},
};
}
const [files, setFiles] = createSignal<UploadFile[]>([]);
const [error, setError] = createSignal<unknown>(null);
const [isDragging, setIsDragging] = createSignal(false);

let ref: T | undefined = undefined;
// Capture owner in the Phase-1 (owned) scope so cleanup can be registered later
const owner = getOwner();

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)));
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)));
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)));
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)));
void runCallback(options?.onDragLeave, transformFiles(event.dataTransfer?.files || null));
};
const onDragOver: JSX.EventHandler<T, DragEvent> = event => {
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)));
void runCallback(options?.onDrag, transformFiles(event.dataTransfer?.files || null));
};

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

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

Promise.resolve(options?.onDrop?.(parsedFiles));
setError(null);
void runCallback(options?.onDrop, parsedFiles);
};

onMount(() => {
if (!ref) return;

// TODO: Should event.stopPropagation() or event.preventDefault() in handlers below?
// setRef is the Phase-2 ref callback: called synchronously when the element is created.
// Listeners are attached immediately; cleanup is registered back in the owner scope.
const setRef = (ref: T) => {
ref.addEventListener("dragstart", onDragStart as any);
ref.addEventListener("dragenter", onDragEnter as any);
ref.addEventListener("dragend", onDragEnd as any);
Expand All @@ -92,16 +96,18 @@ function createDropzone<T extends HTMLElement = HTMLElement>(
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);
runWithOwner(owner, () => {
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);
});
});
});
};

const removeFile = (fileName: string) => {
setFiles(prev => prev.filter(f => f.name !== fileName));
Expand All @@ -114,6 +120,7 @@ function createDropzone<T extends HTMLElement = HTMLElement>(
return {
setRef,
files,
error,
isDragging,
removeFile,
clearFiles,
Expand Down
Loading
Loading