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

Feature: useLysForm hooks #7

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
73 changes: 73 additions & 0 deletions packages/lys/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
## 3.1.0

Introduce `useLysForm` hooks

### `useLysForm` example

```tsx
const slice = createSlice({
actions: {
async submit(x) {
const errors = validate(x.state)
if (errors) {
x.commit({ errors })
return
}

const success = await postUser(x.state)
x.commit({ done: true })
}
}
}, () => ({
form: {
name: '',
isYes: null as File | null
gender: '',
},
done: false,
errors: null,
}))


// in form component
const Form = () => {
const [state, actions] = useLysSliceRoot(slice)
const { bind } = useLysForm(state, actions, 'form')

return (
<>
<div>
Name:
<input type="text" {...bind('name')} />
</div>
<div>
Avatar:
<input type="file" {...bind('file')} />
</div>
<div>
Gender:
<label>
<input type="checkbox" value="yes" {...bind('isYes')} />
Yes
</label>
<label>
<input type="checkbox" value="no" {...bind('isYes')} />
No
</label>

<div>
Websites:
{state.form.urls.map((url, idx) => (
<input type="text" {...bind(`websites.${idx}`)} />
))}
</div>

<button onClick={() => actions.submit()}>
Submit
</button>
</div>
</>
)
}
```

## 3.0.0

Breaking API Changes for simplify state update method.
Expand Down
12 changes: 6 additions & 6 deletions packages/lys/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fleur/lys",
"version": "3.0.0",
"version": "3.1.0-beta.1",
"description": "lys is an micro statement manager for '21s react",
"main": "dist/index.js",
"module": "dist/index.esm.js",
Expand All @@ -17,15 +17,15 @@
"prepublishOnly": "rm -rf ./dist && yarn build:declaration && yarn build"
},
"devDependencies": {
"@testing-library/react": "^11.1.2",
"@testing-library/react": "^13.0.0",
"@testing-library/react-hooks": "^3.4.2",
"@types/react": "^16.9.56",
"@types/react-dom": "^16.9.9",
"@types/react": "^17.0.43",
"@types/react-dom": "^17.0.14",
"bili": "^5.0.5",
"jest": "^26.6.3",
"prettier": "^2.1.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-test-renderer": "^17.0.1",
"rollup-plugin-typescript2": "^0.29.0",
"ts-jest": "^26.4.4",
Expand Down
8 changes: 7 additions & 1 deletion packages/lys/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { createLysContext, LysProvider as LysContext } from "./LysContext";
export { createSlice, instantiateSlice, StateOfSlice } from "./slice";
export {
createSlice,
instantiateSlice,
SliceActions,
StateOfSlice,
} from "./slice";
export { useLysSliceRoot, useLysSlice } from "./useSlice";
export { mockSlice } from "./mockSlice";
export { useLysForm } from "./useLysForm";
17 changes: 10 additions & 7 deletions packages/lys/src/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,27 @@ export type StateOfSlice<T extends Slice<any, any>> = T extends Slice<

export type SliceInstance<S extends Slice<any, any>> = {
state: { readonly current: StateOfSlice<S> & SliceToComputeds<S> };
actions: SliceToActions<S>;
actions: SliceActions<S>;
dispose: () => void;
};

type ExtraArgs<T> = T extends (_: any, ...args: infer R) => any ? R : never;

export type SliceActions<S extends Slice<any, any>> = SliceToActions<S>;
export type DefaultSliceActions<S extends Slice<any, any>> = {
/** @param applier Shallow merging object or modifier function */
set(applier: ObjectPatcher<Draft<StateOfSlice<S>>>): void;
/** @param k Field name to reset to initial state, no specified to reset all fields */
reset(k?: keyof StateOfSlice<S>): void;
};

// prettier-ignore
export type SliceToActions<S extends Slice<any, any>> = {
[K in keyof S["actions"]]:
ReturnType<S["actions"][K]> extends void | undefined ? (...args: ExtraArgs<S['actions'][K]>) => void
: ReturnType<S["actions"][K]> extends Promise<any> ? (...args: ExtraArgs<S['actions'][K]>) => Promise<void>
: never;
} & {
/** @param applier Shallow merging object or modifier function */
set(applier: ObjectPatcher<Draft<StateOfSlice<S>>>): void;
/** @param k Field name to reset to initial state, no specified to reset all fields */
reset(k?: keyof StateOfSlice<S>): void;
};
} & DefaultSliceActions<S>

export type SliceToComputeds<S extends Slice<any, any>> = {
[K in keyof S["computables"]]: ReturnType<S["computables"][K]>;
Expand Down
87 changes: 87 additions & 0 deletions packages/lys/src/useLysForm.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react";
import { renderHook } from "@testing-library/react-hooks";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { useLysForm } from "./bindInput";
import { createSlice, instantiateSlice, SliceActions } from "./slice";
import {
createLysContext,
LysContext,
LysProvider,
LysReactContext,
} from "./LysContext";
import { ReactElement } from "react";

describe("useLysForm", () => {
const slice = createSlice(
{
actions: {},
computed: {},
},
(): { form: { file: File | null; files: []; name: string } } => ({
form: {
file: null,
files: [],
name: "",
},
})
);

const mockFile = new File(["🐿️"], "test.txt");

function setup({
context,
actions,
elements,
}: {
context: LysContext;
actions: SliceActions<any>;
elements: (bind: any) => ReactElement;
}) {
const App = () => {
const { bind } = useLysForm(actions, "form");
return elements(bind);
};

return render(
<LysReactContext.Provider value={context}>
<App />
</LysReactContext.Provider>
);
}

it("should be defined", async () => {
const context = createLysContext();
const { state, actions } = context.createSliceInstance(slice);

const result = setup({
context,
actions,
elements: (bind) => (
<>
<input type="text" {...bind("name")} data-testid="name" />
<input type="file" {...bind("file")} data-testid="file" />
<input type="file" {...bind("files")} multiple data-testid="files" />
</>
),
});

await waitFor(() => {
const input = result.getByTestId("name");
fireEvent.change(input, { target: { value: "test" } });

const fileInput = result.getByTestId("file");
fireEvent.change(fileInput, {
target: { files: [mockFile] },
});

const filesInput = result.getByTestId("files");
fireEvent.change(filesInput, {
target: { files: [mockFile, mockFile] },
});
});

expect(state.current.form.name).toBe("test");
expect(state.current.form.file).toBe(mockFile);
expect(state.current.form.files).toEqual([mockFile, mockFile]);
});
});
Loading