-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(upload-atom): experimental
uploadAtom
and fileUpload
within a…
… list (#103) Preview version.
- Loading branch information
1 parent
a8e00b3
commit 3c12e39
Showing
10 changed files
with
319 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./list-atom"; | ||
export * from "./upload-atom"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./uploadAtom"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { fieldAtom } from "form-atoms"; | ||
import { Atom, atom } from "jotai"; | ||
|
||
import { ExtendFieldAtom, extendFieldAtom } from "../extendFieldAtom"; | ||
|
||
type UploadStatus = "loading" | "error" | "success"; | ||
|
||
export type UploadAtom<Value> = ExtendFieldAtom< | ||
Value, | ||
{ | ||
/** | ||
* A read-only atom containing the field's upload status. | ||
*/ | ||
uploadStatus: Atom<UploadStatus>; | ||
} | ||
>; | ||
|
||
type UploadAtomConfig = { | ||
name?: string; | ||
}; | ||
|
||
export const uploadAtom = | ||
<Value>(upload: (file: File) => Promise<Value>) => | ||
(file: File, config?: UploadAtomConfig): UploadAtom<Value> => { | ||
const requestAtom = atom(async () => upload(file)); | ||
|
||
const field = fieldAtom<Value | undefined>({ | ||
...config, | ||
value: undefined, | ||
validate: async ({ get, set, value }) => { | ||
if (value) { | ||
// the file was already uploaded, the value is the response | ||
return []; | ||
} | ||
|
||
try { | ||
const result = await get(requestAtom); | ||
|
||
set(get(field).value, result); | ||
|
||
return []; | ||
} catch (err) { | ||
if (typeof err !== "string") { | ||
console.warn( | ||
"uploadAtom: The error thrown from failed upload is not a string.", | ||
); | ||
return ["Failed to upload!"]; | ||
} else { | ||
return [err]; | ||
} | ||
} | ||
}, | ||
}); | ||
|
||
return extendFieldAtom(field, ({ validateStatus }) => ({ | ||
uploadStatus: atom<UploadStatus>((get) => { | ||
const status = get(validateStatus); | ||
|
||
if (status === "validating") { | ||
return "loading"; | ||
} else if (status === "valid") { | ||
return "success"; | ||
} else { | ||
return "error"; | ||
} | ||
}), | ||
})); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { useAtomValue, useSetAtom } from "jotai"; | ||
import { useEffect } from "react"; | ||
import { RenderProp } from "react-render-prop-type"; | ||
|
||
import { UploadAtom } from "../../atoms"; | ||
|
||
type ChildrenProps = { | ||
isLoading: boolean; | ||
isError: boolean; | ||
isSuccess: boolean; | ||
}; | ||
|
||
type Props<Value> = { field: UploadAtom<Value> } & RenderProp<ChildrenProps>; | ||
|
||
export function FileUpload<Value>({ field, children }: Props<Value>) { | ||
const atoms = useAtomValue(field); | ||
const validate = useSetAtom(atoms.validate); | ||
const status = useAtomValue(atoms.uploadStatus); | ||
|
||
// runs the upload | ||
useEffect(() => validate(), []); | ||
|
||
return children({ | ||
isLoading: status === "loading", | ||
isError: status === "error", | ||
isSuccess: status === "success", | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import { FieldAtom, fieldAtom, useFieldValue } from "form-atoms"; | ||
|
||
import { FileUpload } from "./FileUpload"; | ||
import { SwitchUploadAtom } from "./SwitchUploadAtom"; | ||
import { uploadAtom } from "../../atoms"; | ||
import { StringField, listField, stringField } from "../../fields"; | ||
import { PicoFieldErrors } from "../../scenarios/PicoFieldErrors"; | ||
import { formStory, meta } from "../../scenarios/StoryForm"; | ||
import { List, RemoveButtonProps } from "../list"; | ||
|
||
export default { | ||
...meta, | ||
title: "components/FileUpload", | ||
}; | ||
|
||
let id = 3; | ||
|
||
const fakeUploadAtom = uploadAtom( | ||
() => | ||
new Promise<string>((resolve, reject) => { | ||
setTimeout(() => { | ||
if (Math.random() > 0.3) { | ||
resolve(`https://picsum.photos/id/${id++}/100/100`); | ||
} else { | ||
reject("Simulated error message as passed to Promise.reject()."); | ||
} | ||
}, 2000); | ||
}), | ||
); | ||
|
||
const fileList = listField({ | ||
value: [ | ||
{ | ||
id: "1", | ||
url: "https://picsum.photos/id/1/100/100", | ||
}, | ||
{ | ||
id: "2", | ||
url: "https://picsum.photos/id/2/100/100", | ||
}, | ||
], | ||
builder: ({ url, id }) => ({ | ||
// the ID must be optional, to permit submit newly uploaded files to the server | ||
id: stringField({ value: id }).optional(), | ||
// the URL is only a fieldAtom, not zodField, as the uploadAtom extends only the fieldAtom | ||
url: fieldAtom({ value: url }), | ||
}), | ||
}); | ||
|
||
const Image = ({ url }: { url: FieldAtom<string> }) => { | ||
const value = useFieldValue(url); | ||
|
||
return ( | ||
<img width={100} height={100} style={{ marginRight: 20 }} src={value} /> | ||
); | ||
}; | ||
|
||
const IdMessage = ({ id }: { id: StringField }) => { | ||
const value = useFieldValue(id); | ||
|
||
return value ? ( | ||
<span> | ||
I'm already stored with <strong>id: {value}</strong> | ||
</span> | ||
) : ( | ||
<span>New url ready to be stored in DB (after form submit).</span> | ||
); | ||
}; | ||
|
||
export const FileUploadList = formStory({ | ||
parameters: { | ||
docs: { | ||
description: { | ||
story: | ||
"Here we use custom `uploadAtom()` which handles `File` upload from the client side. The form will eventually be submitted with the uploaded URL.", | ||
}, | ||
}, | ||
}, | ||
args: { | ||
fields: { | ||
fileList, | ||
}, | ||
children: ({ fields }) => ( | ||
<List | ||
field={fields.fileList} | ||
AddButton={({ add }) => ( | ||
<input | ||
type="file" | ||
multiple | ||
onChange={(event) => { | ||
const files = Array.from(event.currentTarget.files ?? []); | ||
|
||
files.forEach((file) => | ||
add({ | ||
id: stringField().optional(), | ||
url: fakeUploadAtom(file), | ||
}), | ||
); | ||
}} | ||
/> | ||
)} | ||
RemoveButton={RemoveButton} | ||
> | ||
{({ fields, RemoveButton }) => { | ||
return ( | ||
<div | ||
style={{ | ||
display: "grid", | ||
gridGap: 16, | ||
gridTemplateColumns: "auto min-content", | ||
}} | ||
> | ||
<SwitchUploadAtom field={fields.url}> | ||
{({ isUpload, field }) => { | ||
return isUpload ? ( | ||
<FileUpload field={field}> | ||
{({ isLoading, isSuccess, isError }) => ( | ||
<div> | ||
{isLoading ? ( | ||
<> | ||
<p> | ||
Please wait... <progress /> | ||
</p> | ||
</> | ||
) : isSuccess ? ( | ||
<p> | ||
<Image url={fields.url} /> | ||
<ins>Done. </ins> | ||
<IdMessage id={fields.id} /> | ||
</p> | ||
) : isError ? ( | ||
<> | ||
<p> | ||
Failed to upload. Use the{" "} | ||
<code>FieldErrors</code> component to display | ||
the reason thrown from your <code>upload</code>{" "} | ||
action: | ||
<PicoFieldErrors field={fields.url} /> | ||
</p> | ||
</> | ||
) : ( | ||
<></> | ||
)} | ||
</div> | ||
)} | ||
</FileUpload> | ||
) : ( | ||
<p> | ||
<Image url={fields.url} /> | ||
<IdMessage id={fields.id} /> | ||
</p> | ||
); | ||
}} | ||
</SwitchUploadAtom> | ||
<div> | ||
<RemoveButton /> | ||
</div> | ||
</div> | ||
); | ||
}} | ||
</List> | ||
), | ||
}, | ||
}); | ||
|
||
const RemoveButton = ({ remove }: RemoveButtonProps) => ( | ||
<button type="button" className="outline secondary" onClick={remove}> | ||
Remove | ||
</button> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { FieldAtom } from "form-atoms"; | ||
import { useAtomValue } from "jotai"; | ||
import { RenderProp } from "react-render-prop-type"; | ||
|
||
import { UploadAtom } from "../../atoms"; | ||
|
||
export function useIsUploadAtom( | ||
field: FieldAtom<any>, | ||
): field is UploadAtom<any> { | ||
const atoms = useAtomValue(field); | ||
|
||
return !!(atoms as any).uploadStatus; | ||
} | ||
|
||
type UploadProps<Value> = { isUpload: true; field: UploadAtom<Value> }; | ||
|
||
type RegularProps<Value> = { isUpload: false; field: FieldAtom<Value> }; | ||
|
||
export const SwitchUploadAtom = <Value,>({ | ||
field, | ||
children, | ||
}: { field: FieldAtom<Value> } & RenderProp< | ||
UploadProps<Value> | RegularProps<Value> | ||
>) => { | ||
if (useIsUploadAtom(field)) { | ||
return <>{children({ isUpload: true, field })}</>; | ||
} else { | ||
return <>{children({ isUpload: false, field })}</>; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./FileUpload"; | ||
export * from "./SwitchUploadAtom"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters