|
| 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. |
0 commit comments