Open veloii opened 3 months ago
@veloii This is definitely something we would be interested in, but internally do not have the bandwidth to build right now. If you are interested in putting together a PR, I am happy to review and provide feedback so that we can get something merged sooner!
Yes, I’d be happy to submit a PR, would you like a draft of the API before I start working on it?
Yeah, that is a good idea, just to make sure we are on the same page before you put the effort into implementing.
Sorry for the delay - what about something like this?
Function signatures:
import React, { ElementType } from "react";
import type { ErrorMessage } from "@uploadthing/shared";
import type { FileRouter } from "uploadthing/types";
import type { UploadthingComponentProps } from "../types";
import { createUploadthing } from "uploadthing/server";
type PrimitiveComponentCallbackValues = {
ready: boolean;
isUploading: boolean;
uploadProgress: number;
fileTypes: string[];
};
type PrimitiveComponentProps<T extends ElementType = "div"> = Omit<
React.ComponentPropsWithRef<T>,
"children"
> & {
children?:
| ((values: PrimitiveComponentCallbackValues) => React.ReactNode)
| React.ReactNode;
};
export type UploadPrimitiveProps<
TRouter extends FileRouter,
TEndpoint extends keyof TRouter,
TSkipPolling extends boolean = false,
> = UploadthingComponentProps<TRouter, TEndpoint, TSkipPolling> &
PrimitiveComponentProps;
declare function UploadPrimitive<
TRouter extends FileRouter,
TEndpoint extends keyof TRouter,
TSkipPolling extends boolean = false,
>(
props: FileRouter extends TRouter
? ErrorMessage<"You forgot to pass the generic">
: UploadPrimitiveProps<TRouter, TEndpoint, TSkipPolling>,
): React.JSX.Element;
declare namespace UploadPrimitive {
function Button(_: PrimitiveComponentProps<"button">): React.ReactNode;
function AllowedContent(_: PrimitiveComponentProps): React.ReactNode;
function ClearButton(_: PrimitiveComponentProps<"button">): React.ReactNode;
}
Demo:
const f = createUploadthing();
const router = {
exampleRoute: f(["image"])
.middleware(() => ({ foo: "bar" }))
.onUploadComplete(({ metadata }) => {
return { foo: "bar" as const };
}),
};
type Router = typeof router;
function Demo() {
return (
<>
<UploadPrimitive<Router, "exampleRoute">
endpoint="exampleRoute"
className="bg-blue-500"
>
<UploadPrimitive.Button className="ut-ready:bg-green-500">
Choose Files(s)
</UploadPrimitive.Button>
<UploadPrimitive.ClearButton className="disabled:hidden">
Clear files
</UploadPrimitive.ClearButton>
{/* Default content */}
<UploadPrimitive.AllowedContent />
{/* Custom content */}
<UploadPrimitive.AllowedContent>
{({ fileTypes }) => <div>Accepted: {fileTypes.join(", ")}</div>}
</UploadPrimitive.AllowedContent>
</UploadPrimitive>
<UploadPrimitive<Router, "exampleRoute">
endpoint="exampleRoute"
className="bg-blue-500"
>
{/* get values here instead */}
{({ fileTypes }) => (
<>
<UploadPrimitive.Button className="ut-ready:bg-green-500">
Choose Files(s)
</UploadPrimitive.Button>
<UploadPrimitive.ClearButton className="disabled:hidden">
Clear files
</UploadPrimitive.ClearButton>
<div>Accepted: {fileTypes.join(", ")}</div>
</>
)}
</UploadPrimitive>
</>
);
}
PS: I'm aware namespace is an anti-pattern but it's the only way I can think of declaring a function on a declared function in TS
For the actual implementation, I'd just copy the current Upload button and create a context with all the PrimitiveComponentCallbackValues
and have all the sub components read that.
Including @radix-ui/react-slot
would allow the use of any component library (MUI, Mantine, etc) and is only 1.1kb minified+gzipped.
<UploadPrimitive.Button asChild>
<YourComponentLibraryButton>
Choose Files(s)
</YourComponentLibraryButton>
</UploadPrimitive.Button>
I'm not 100% sure where this would go in the docs.
cc @markflorkowski
Sorry for the delay, didn't have time over the weekend to review. Overall looks good to me, though I would probably rename the components themselves a bit:
import * as UT from ...
<UT.Root<Router, "exampleRoute">
className="bg-blue-500"
>
<UT.Trigger className="ut-ready:bg-green-500">
Choose Files(s)
</UT.Trigger>
<UT.Clear className="disabled:hidden">
Clear files
</UT.Clear>
{/* Default content */}
<UT.AllowedContent />
{/* Custom content */}
<UT.AllowedContent>
{({ fileTypes }) => <div>Accepted: {fileTypes.join(", ")}</div>}
</UT.AllowedContent>
</UT.Root>;
I am ok with including @radix-ui/react-slot
for the ergonomic win
cc @juliusmarminge -- thoughts?
As for where to put it in the docs, right now I would say as a sub-section on the theming page, but me may want to break that page into multiple, as it is already quite large 🤔
I like the idea of unstyled very much.
API wise I've recently found HeadlessUI's <Parent as={Button}>
prop approach a bit nicer to work with than Radix's Parent asChild><Button>
but that's just personal preference. (Not sure if there's a package ready-to-use for this or not but feels like it should be fairly light weight to copy props?)
Same goes for render={({ types }) => UI}
prop instead of <>{({ types }) => UI}</>
children render function, but again that's personal preference and at the end of the day an implementation detail
Same goes for render={({ types }) => UI} prop instead of <>{({ types }) => UI}</> children render function, but again that's personal preference and at the end of the day an implementation detail
You prefer render={({ types }) => UI}
?
Yea. idk I guess just prefer not indenting stuff unnecessarily 😅
Another thing I think we need is for files
to be a controllable prop
Is this being worked on still? Would be a HUGE win imo. I have almost the exact same use case as OP -- except I just ended up making a messy workaround.
I'm willing to drop a PR over the weekend if no one's working on this.
Feel free!
One thing to note though is that https://github.com/pingdotgg/uploadthing/pull/886 is touching quite a bit in the component code. We'll try and get that merged before EOW so you don't have to resolve a bunch of conflicts later on
Yeah sorry, I've been really busy and haven't found the time to work on a PR. I can help with some of the other primitive components after your initial PR if you like. For example, multiple file support and inline images / videos.
import * as UT from ...
<UT.Root<Router, "exampleRoute">
className="bg-blue-500"
multiple
>
{({ files, removeFile }) => (
<>
<UT.Trigger className={files.length > 0 && "hidden"}>
Choose Files(s)
</UT.Trigger>
<div>
{files.map((file) => (
<div>
<span>{file.name}</span>
<button onClick={() => removeFile(file.id)}>Remove</button>
</div>
))}
</div>
<UT.Clear className="disabled:hidden">
Clear files
</UT.Clear>
</>
)}
</UT>;
or (this feels worst)
<UT.List>
{({ files, removeFile }) => files.map((file) => (
<div>
<span>{file.name}</span>
<button onClick={() => removeFile(file.id)}>Remove</button>
</div>
))}
</UT.List>
Could also be done through a controllable prop, but for most use cases it's worst DX.
Could also be done through a controllable prop, but for most use cases it's worst DX.
Perhaps, but it's also gonna be more powerful and allow much more usages (state might come from parent, "updater" might be on a different part of the app outside of <UT.Root>
etc etc
I completely agree. Sorry if I wasn't clear, I meant both controlled and non-controlled should be available but that I thought the DX is better if you aren't using it outside the component.
@juliusmarminge Working on a PR at the moment. How should I run my code locally to ensure it all works?
We don't have any UI tests atm so just spin up an example and test it manually. If you've made any logic changes to the internals our tests there are fairly comprehensive
Do you have an ideas on how the root component generics be generated? This is what I've got at the moment.
import { generateUploadRoot } from "@uploadthing/react";
import * as UT from "@uploadthing/react/primitive";
const UploadRoot = generateUploadRoot<OurFileRouter>();
<UploadRoot>
<UT.Button>
Upload
</UT.Button>
</UploadRoot>
but it's not very intuitive as we can't use UT.Root
without passing the generics manually.
It also breaks the react context because the context is used in @uploadthing/react/primitive
but the provider is created in @uploadthing/react
for generateUploadRoot
.
I thought about
import { generateUploadPrimitives } from "@uploadthing/react";
const UT = generateUploadPrimitives<OurFileRouter>();
<UT.Root>
<UT.Button>
Upload
</UT.Button>
</UT.Root>
but I'm pretty sure this will break tree shaking as we define all the components as one variable.
Do you have any other ideas?
cc @juliusmarminge
How many component parts are there? Is tree shaking something we need to worry too much about? It's gonna tree shake if you're not using any of this custom component at all, so the only case where there'll be more JS than necessary is if you're using the root and button but not the rest of them?
Alternatively, we can do some svelte-y thing and have a function that returns the props
const typehelper = gen<UploadRouter>()
function Comp() {
return <UT.Root {...typehelper("endpoint", { ... })}>...
How many component parts are there?
I'm working on Button
, AllowedContent
, ClearButton
and Dropzone
(4) at the moment. I'll try do another PR for InlineMedia
and FileList
(2). I'll implement it without tree-shaking for now and measure the bundle size to see if it's worth it.
I have a feeling each of those is not adding a lot on their own? It's the underlying core logic that eats most of the bytes and those need to be included regardless?
Yeah most of the components themselves are tiny. I've got a draft: #947.
Describe the feature you'd like to request
I'd like to request unstyled, composable components for uploadthing:
These would be bare-bones, allowing easy styling and flexible combinations. I've already had to build similar things for my own sites. This kind of stuff is gold for DX - saves time and headaches.
Describe the solution you'd like to see
I've already got an
UploadableInlineImage
component in my site at the moment. The other components would have a similar API. All the components can be customised with your current styling solution, for the examples I'm using Tailwind.Hierarchy:
Example Usage:
I'm not 100% sure on
UploadableInlineImageProgressOverlay
as that's currently not unstyled at the moment, but I think that could be worked out easily.Additional information
No response
👨👧👦 Contributing