Skip to content

Commit b577afa

Browse files
authored
New children prop design (#155)
* Update package * Fix tests in chakra-ui package * Removed unused function: getState * Fix lint error * Add more tests * Updated renderer * Fix default form in chakra-ui * Fix tests * Support defaultValues * Add a test for defaultValues * Fix code comment * Add changeset * Add example to use defaultValues prop * Use update usecase * Add note for backward incompatibility * More example in release note
1 parent 6a7bb01 commit b577afa

26 files changed

+1529
-648
lines changed

.changeset/lemon-seas-trade.md

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
"@fabrix-framework/fabrix": minor
3+
---
4+
5+
This update contains the breaking changes specifically on functions that the children prop in `FabrixComponent` passes down to bring more flexibility in rendering the view.
6+
7+
See [#168](https://github.com/fabrix-framework/fabrix/issues/168) for more about the motivation about this change.
8+
9+
## Input and Output
10+
11+
The newly introduced functions in the children prop is `getInput` and `getOutput`.
12+
13+
* `getInput` is a function that plays a role as a form renderer, and also an accessor of the form context and form field control. Input fields are inferred from variable definitions in the corresponding GraphQL operation.
14+
* `getOutput` is a function that works as a result renderer (the behaviour of it depends on the component registered in the component registry), and also a direct accessor of the query data. Output fields are inferred from selected fields in the corresponding GraphQL operation.
15+
16+
Here is the example implementation that renders the form to get the search condition for `Query` operation alongside the result component like tables.
17+
18+
```tsx
19+
<FabrixComponent
20+
query={gql`
21+
query getTodos($input: GetTodoInput!) {
22+
getTodos(input: $input) {
23+
edges {
24+
node {
25+
name
26+
priority
27+
}
28+
}
29+
}
30+
}
31+
`}
32+
>
33+
{({ getInput, getOutput }) => (
34+
<>
35+
{/*
36+
* `getInput` renders the all form fields inferred from the variables
37+
* The rendered view by `getInput` without render props also has the button to execute the query.
38+
*/}
39+
{getInput()}
40+
41+
{/*
42+
* `getOuput` renders the result of the mutation
43+
* This example assumes that `getTodos` is rendered as a table component.
44+
*/}
45+
{getOutput("getTodos")}
46+
</>
47+
)}
48+
</FabrixComponent>
49+
```
50+
51+
The important point to mention is that `getOutput` and `getInput` work in the same way both for `Query` and `Mutation` by this update.
52+
53+
### `data` accessor
54+
55+
With this update, `data` accessor is accessible through `getOuput` function, since the data is tied from the query result (output).
56+
57+
```tsx
58+
<FabrixComponent
59+
query={gql`
60+
query getTodo {
61+
getTodo {
62+
name
63+
priority
64+
}
65+
}
66+
`}
67+
>
68+
{({ getOutput }) =>
69+
getOutput("getTodo", ({ data }) => (
70+
<div>Todo name: {data.name}</div>
71+
))
72+
}
73+
</FabrixComponent>
74+
```
75+
76+
## More customizable, layoutable form
77+
78+
Here is the complex example to create an update form to show the customizability and layoutability.
79+
80+
```tsx
81+
<FabrixComponent
82+
query={gql`
83+
mutation updateTodo($id: ID!, $input: CreateTodoInput!) {
84+
updateTodo(id: $id, input: $input) {
85+
id
86+
}
87+
}
88+
`}
89+
>
90+
{({ getInput }) =>
91+
/*
92+
* `getInput` is a function to render form view which can acess functions to build forms.
93+
* `Field` and `getAction` are the key functions (see their explanation below)
94+
*/
95+
getInput({
96+
/*
97+
* If the form is the one to update resource, set `defaultValues` here to prefill the form fields.
98+
* The data structure should be matched with the variables of query/mutation.
99+
*/
100+
defaultValues: {
101+
id: "user-id",
102+
input: {
103+
name: "John Doe"
104+
}
105+
}
106+
}, ({ Field, getAction }) => (
107+
{/*
108+
* `getAction` is expcted to be passed as an descructive props to `form` element.
109+
* It is an object that contains `onSubmit` function as a member that kicks off the query execution.
110+
*/}
111+
<form {...getAction()}>
112+
{/*
113+
* `Field` is a React component that renders the form field that autotimacally deciding
114+
* the corresponding component according to GraphQL type for the path specified in the `name` prop.
115+
*
116+
* `extraProps` is the prop to carry information to the form field.
117+
* In this example, I assume the component that is registered in the component registry
118+
* as the form field handles `label` to show it as a text content in the `label` element.
119+
*
120+
* The props for the `extraProps` should have more variety (e.g., `disabled`, `placeholder`, ...),
121+
* but I will work on adding them in other PRs later on.
122+
*/}
123+
<HStack>
124+
<Field name="input.name" extraProps={{ label: "Task Name" }} />
125+
<Field name="input.priority" extraProps={{ label: "Task Priority" }} />
126+
</HStack>
127+
<Button type="submit">Add</Button>
128+
</form>
129+
))
130+
}
131+
</FabrixComponent>
132+
```
133+
134+
Additionally, for more page-by-page customization for the form, `getInput` functions offers more functions in its render props, mostly powered by react-hook-form that fabrix internal uses.
135+
136+
### Field-level handler
137+
138+
In the case that the field component automatially decided by GraphQL type does not fit the requirement in the form, `getInput` function provides the another customizable point at the field level in the form.
139+
140+
`getField` function returns the value of `UseFormRegisterReturn` in react-hook-form. Users would be able to use the another input component on the spot with this.
141+
142+
```tsx
143+
<FabrixComponent query={`/* ... */`}>
144+
{({ getInput }) =>
145+
getInput({}, ({ Field, getAction, getField }) => {
146+
<form {...getAction()}>
147+
<Field name="input.name" />
148+
<Field name="input.priority" />
149+
<input {...getField("input.email")} type="text" />
150+
</form>
151+
})
152+
}
153+
</FabrixComponent>
154+
```
155+
156+
### Form context
157+
158+
The render props of `getInput` function also passes down `formContext` that is the react-hook-form context that the form rendered by `getInput` internally maintains.
159+
160+
This helps users create the flexible form-wide funcionality as they want by lerveraging the functionality of react-hook-form like inter-field interactibity.
161+
162+
```tsx
163+
import { UseFormReturn } from "react-hook-form";
164+
165+
const Page = () =>
166+
<FabrixComponent query={`/* ... */`}>
167+
{({ getInput }) =>
168+
getInput({}, ({ getAction, formContext }) => {
169+
<form {...getAction()}>
170+
<WatchingField formContext={formContext} />
171+
</form>
172+
})
173+
}
174+
</FabrixComponent>
175+
176+
const WatchingField = (props: {
177+
formContext: UseFormReturn,
178+
}) => {
179+
/*
180+
* Watches the value on the form field using `watch` method in the form context of react-hook-form
181+
*/
182+
const status = formContext.watch("input.priority");
183+
}
184+
```
185+
186+
## Backward incompatibility
187+
188+
The previous behaviour of `FabrixComponent` is that only the component for the result was rendered in `Query` and only the form for `Mutation` on the contrary.
189+
However, from this relelase, `FabrixComponent` will render both the form and the result of the component regardless of operation type.
190+
191+
If you would like to maintain the previous behaviour, use directives to guide the query render only the specific component that you want.
192+
193+
```tsx
194+
/*
195+
* `@fabrixForm` directive does not
196+
*/
197+
<FabrixComponent
198+
query={gql`
199+
mutation updateTodo($id: ID!, $input: CreateTodoInput!) {
200+
updateTodo(id: $id, input: $input) @fabrixForm {
201+
id
202+
}
203+
}
204+
`}
205+
/>
206+
```
207+
208+
`fabrixView` also works for `Query` operation in the same way.

examples/vite-todoapp/src/App.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Heading, Stack } from "@chakra-ui/react";
1+
import { Button, Heading, Stack } from "@chakra-ui/react";
22
import { FabrixComponent } from "@fabrix-framework/fabrix";
33
import { css } from "@emotion/css";
44
import { graphql } from "./graphql";
@@ -22,7 +22,17 @@ function App() {
2222
}
2323
}
2424
`)}
25-
/>
25+
>
26+
{({ getInput }) =>
27+
getInput({}, ({ Field, getAction }) => (
28+
<form {...getAction()}>
29+
<Field name="input.name" />
30+
<Field name="input.priority" />
31+
<Button type="submit">Add</Button>
32+
</form>
33+
))
34+
}
35+
</FabrixComponent>
2636
<FabrixComponent
2737
containerClassName={containerClassName}
2838
query={graphql(`

packages/chakra-ui/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@
3737
"chakra-react-select": "^6.0.1",
3838
"chroma-js": "^3.0.0",
3939
"create-color": "^2.0.6",
40+
"es-toolkit": "^1.30.1",
4041
"zod": "^3.23.8"
4142
},
4243
"peerDependencies": {
43-
"@emotion/react": "^11",
4444
"@chakra-ui/react": "^3",
45+
"@emotion/react": "^11",
4546
"react": ">=18",
4647
"react-dom": ">=18"
4748
},

packages/chakra-ui/src/components/default/form.tsx

+6-15
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,16 @@ import { Text, Input, Stack, Box, Button } from "@chakra-ui/react";
77
import { Switch } from "@components/ui/switch";
88
import { Select } from "chakra-react-select";
99
import { useController } from "@fabrix-framework/fabrix/rhf";
10+
import { get } from "es-toolkit/compat";
1011
import { LabelledHeading } from "./shared";
1112

1213
export const ChakraForm = (props: FormComponentProps) => {
1314
return (
1415
<Box className={props.className} rowGap={"20px"}>
15-
{props.renderFields()}
16-
{props.renderSubmit(({ submit, isSubmitting }) => (
17-
<Button
18-
className="col-12"
19-
colorScheme="blue"
20-
marginTop={2}
21-
// @ts-expect-error
22-
isDisabled={isSubmitting}
23-
onClick={() => submit()}
24-
>
25-
Submit
26-
</Button>
27-
))}
16+
<form {...props.getAction()}>
17+
{props.renderFields()}
18+
<Button type="submit">Submit</Button>
19+
</form>
2820
</Box>
2921
);
3022
};
@@ -61,7 +53,7 @@ const ErrorField = (props: FormFieldComponentProps) => {
6153
const { formState } = useController({
6254
name: props.name,
6355
});
64-
const error = formState.errors[props.name];
56+
const error = get(formState.errors, props.name);
6557

6658
return (
6759
error && (
@@ -157,7 +149,6 @@ const NumberFormField = (props: FormFieldComponentProps) => {
157149
const { className } = props.attributes;
158150
const { field } = useController({
159151
name: props.name,
160-
defaultValue: props.value ?? "",
161152
rules: {
162153
required: props.isRequired,
163154
},

packages/fabrix/__tests__/componentRegistry.test.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,9 @@ describe("getFabrixComponent", () => {
147147
}
148148
`}
149149
>
150-
{({ getComponent }) => {
151-
return (
152-
<div data-testid="component-wrapper">{getComponent("users")}</div>
153-
);
154-
}}
150+
{({ getOutput }) => (
151+
<div data-testid="component-wrapper">{getOutput("users")}</div>
152+
)}
155153
</CustomTable>,
156154
async () => {
157155
const wrapper = await screen.findByTestId("component-wrapper");

0 commit comments

Comments
 (0)