Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: clear file input value via atomEffect #47

Merged
merged 1 commit into from
Oct 16, 2023
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"happy-dom": "^8.9.0",
"jotai": "^2.1.1",
"jotai-devtools": "^0.5.3",
"jotai-effect": "0.1.0",
"jsdom": "^21.1.0",
"lodash.shuffle": "^4.2.0",
"prettier": "2.8.4",
Expand Down Expand Up @@ -130,6 +131,7 @@
"peerDependencies": {
"form-atoms": "^3",
"jotai": "^2",
"jotai-effect": "^0",
"react": ">=16.8",
"zod": "^3"
}
Expand Down
15 changes: 7 additions & 8 deletions src/Intro.contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@

### Hooks

| | |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| [useClearInputAction](?path=/docs/hooks-useclearinputaction--docs) | Hook providing action to clear input value via its ref. |
| [useClearFileInputEffect](?path=/docs/hooks-useclearfileinputeffect--docs) | An effect hook to clear file input when its field is reset. |
| [useOptions](?path=/docs/hooks-useoptions--docs) | A data hook to evaluate which of option(s) is(are) active with respect to a field. |
| [useRequiredProps](?path=/docs/hooks-userequiredprops--docs) | Provides the `required` prop for input based on field optionality. |
| [useSelectFieldProps](?path=/docs/hooks-useselectfieldprops--docs) | A generic hook to manage a field holding active option from primitive options. |
| [useSelectOptions](?path=/docs/hooks-useselectoptions--docs) | An extension of `useOptions` hook which returns `<option>` elements instead of raw data. |
| | |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| [useClearInputAction](?path=/docs/hooks-useclearinputaction--docs) | Hook providing action to clear input value via its ref. |
| [useOptions](?path=/docs/hooks-useoptions--docs) | A data hook to evaluate which of option(s) is(are) active with respect to a field. |
| [useRequiredProps](?path=/docs/hooks-userequiredprops--docs) | Provides the `required` prop for input based on field optionality. |
| [useSelectFieldProps](?path=/docs/hooks-useselectfieldprops--docs) | A generic hook to manage a field holding active option from primitive options. |
| [useSelectOptions](?path=/docs/hooks-useselectoptions--docs) | An extension of `useOptions` hook which returns `<option>` elements instead of raw data. |
8 changes: 6 additions & 2 deletions src/fields/array-field/arrayField.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ZodArray, z } from "zod";

import { ZodFieldConfig, zodField } from "..";
import { ZodParams, defaultParams } from "../zod-field";
import {
ZodFieldConfig,
ZodParams,
defaultParams,
zodField,
} from "../zod-field";

export type ArrayFieldParams<ElementSchema extends z.Schema> = Partial<
ZodFieldConfig<
Expand Down
7 changes: 5 additions & 2 deletions src/fields/files-field/Docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const attachments = filesField().optional();

A hook providing props to control a file input.

### Features

- ✅ Clears the file input, when the form is reset.

```tsx
const FileInput = ({
field,
Expand All @@ -41,8 +45,7 @@ const FileInput = ({
field: FilesFieldAtom;
label: ReactNode;
}) => {
// the `value` is the FileList which cannot be passed to the input element, so we drop it
const { value, ...props } = useFilesFieldProps(field);
const props = useFilesFieldProps(field);

return (
<div style={{ margin: "20px 0" }}>
Expand Down
5 changes: 1 addition & 4 deletions src/fields/files-field/FilesInput.mock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { InputHTMLAttributes, ReactNode } from "react";
import { FilesField } from "./filesField";
import { useFilesFieldProps } from "./useFilesFieldProps";
import { FieldErrors, FieldLabel } from "../../components";
import { useClearFileInputEffect } from "../../hooks";

export const FilesInput = ({
field,
Expand All @@ -13,9 +12,7 @@ export const FilesInput = ({
field: FilesField;
label: ReactNode;
} & InputHTMLAttributes<HTMLInputElement>) => {
const { value, ...props } = useFilesFieldProps(field);

useClearFileInputEffect(field);
const props = useFilesFieldProps(field);

return (
<div style={{ margin: "20px 0" }}>
Expand Down
1 change: 0 additions & 1 deletion src/fields/files-field/filesField.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { expectTypeOf } from "vitest";

import { filesField } from "./filesField";
import { FormSubmitValues } from "../zod-field/zodField";

test("required filesField has '[File, ...File[]]' submit value", () => {
const form = formAtom({
field: filesField(),
Expand Down
55 changes: 52 additions & 3 deletions src/fields/files-field/filesField.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExtractAtomValue } from "jotai";
import { ExtractAtomValue, atom } from "jotai";
import { atomEffect } from "jotai-effect";
import { z } from "zod";

import { ArrayFieldParams, arrayField } from "..";
Expand All @@ -14,10 +15,58 @@ const isServer = typeof window === "undefined";
// the File constructor does not exist in node, so we must prevent getting reference error
const elementSchema = isServer ? z.never() : z.instanceof(File);

export const filesArrayField = arrayField({ elementSchema });

const makeClearInputEffect = (atom: typeof filesArrayField) => {
const effect = atomEffect((get) => {
const field = get(atom);
const value = get(field.value);
const ref = get(field.ref);

if (value.length === 0) {
if (ref) {
ref.value = "";
}
}
});

effect.debugPrivate = true;

return effect;
};

export const filesField = (
params: Partial<ArrayFieldParams<typeof elementSchema>> = {}
) =>
arrayField({
) => {
const fieldAtom = arrayField({
elementSchema,
...params,
});

const clearInputEffect = makeClearInputEffect(fieldAtom);

const filesField = atom((get) => {
const field = get(fieldAtom);
get(clearInputEffect); // mount effect

return { ...field, clearInputEffect };
});

return Object.assign(filesField, {
optional: () => {
const optionalFieldAtom = fieldAtom.optional();
const clearInputEffect = makeClearInputEffect(optionalFieldAtom);

const optionalFilesField = atom((get) => {
const field = get(optionalFieldAtom);
get(clearInputEffect); // mount effect

return { ...field, clearInputEffect };
});

return Object.assign(optionalFilesField, {
optional: () => optionalFilesField,
});
},
}) as unknown as typeof filesArrayField;
};
65 changes: 65 additions & 0 deletions src/fields/files-field/useFilesFieldProps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { act, render, renderHook, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { formAtom, useFormActions } from "form-atoms";
import { describe, expect, it } from "vitest";

import { filesField } from "./filesField";
import { useFilesFieldProps } from "./useFilesFieldProps";

describe("useFilesFieldProps", () => {
describe("with required field", () => {
it("clears input when the form is reset", async () => {
const field = filesField();
const form = formAtom({ field });
const { result: props } = renderHook(() => useFilesFieldProps(field));
const { result: formActions } = renderHook(() => useFormActions(form));

render(<input type="file" data-testid="fileInput" {...props.current} />);

const input = screen.getByTestId("fileInput");

await act(() =>
userEvent.upload(
input,
new File(["content"], "file-name.png", {
type: "image/png",
})
)
);

expect(input).toHaveValue("C:\\fakepath\\file-name.png");

await act(() => formActions.current.reset());

expect(input).toHaveValue("");
});
});

describe("with optional field", () => {
it("clears input when the form is reset", async () => {
const field = filesField().optional();
const form = formAtom({ field });
const { result: props } = renderHook(() => useFilesFieldProps(field));
const { result: formActions } = renderHook(() => useFormActions(form));

render(<input type="file" data-testid="fileInput" {...props.current} />);

const input = screen.getByTestId("fileInput");

await act(() =>
userEvent.upload(
input,
new File(["content"], "file-name.png", {
type: "image/png",
})
)
);

expect(input).toHaveValue("C:\\fakepath\\file-name.png");

await act(() => formActions.current.reset());

expect(input).toHaveValue("");
});
});
});
8 changes: 6 additions & 2 deletions src/fields/files-field/useFilesFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ export type FileFieldProps = FieldProps<FilesField>;
const getFiles = (event: ChangeEvent<HTMLInputElement>) =>
event.target.files ? Array.from(event.target.files) : [];

export const useFilesFieldProps = (field: FilesField) =>
useFieldProps(field, getFiles);
export const useFilesFieldProps = (field: FilesField) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { value, ...props } = useFieldProps(field, getFiles);

return props;
};
1 change: 0 additions & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./use-clear-file-input-effect";
export * from "./use-clear-input-action";
export * from "./use-field-error";
export * from "./use-field-props";
Expand Down
79 changes: 0 additions & 79 deletions src/hooks/use-clear-file-input-effect/Docs.mdx

This file was deleted.

1 change: 0 additions & 1 deletion src/hooks/use-clear-file-input-effect/index.ts

This file was deleted.

This file was deleted.

Loading