Skip to content

Commit a9f4c65

Browse files
authored
feat: 🎸 add Watch component to subscribe to fields (#21)
1 parent 91d3969 commit a9f4c65

File tree

8 files changed

+352
-55
lines changed

8 files changed

+352
-55
lines changed

.changeset/cool-gorillas-work.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@deep-state/react-form': patch
3+
---
4+
5+
Add Watch component to subscribe to fields

.changeset/fast-cycles-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@deep-state/react-form': patch
3+
---
4+
5+
Add custom render prop for Field component

packages/deep-state-react-form/demo/index.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,17 @@ function App() {
186186
cond: (data) => parseInt(data.fieldB.value as string) === 100,
187187
effects: { value: 'disable D', disabled: true },
188188
}),
189+
build({
190+
keys: ['_meta'],
191+
cond: (data) =>
192+
!data._meta.isValid && data._meta.errors.fieldA === '',
193+
effects: {},
194+
}),
189195
],
190196
},
191197
}}
192198
>
193-
{({ Field, Show }) => (
199+
{({ Field, Show, Watch }) => (
194200
<>
195201
<Field field="randomizer" />
196202
<Field field="reset" />
@@ -212,6 +218,16 @@ function App() {
212218
</div>
213219
)}
214220
</Field>
221+
<Watch keys={['fieldA', 'fieldB', '_meta']}>
222+
{(props) => (
223+
<>
224+
<div>Watcher!</div>
225+
<div>{props.fieldA.value}</div>
226+
<div>{props.fieldB.value}</div>
227+
<div>Is Valid: {props._meta.isValid.toString()}</div>
228+
</>
229+
)}
230+
</Watch>
215231
</>
216232
)}
217233
</Form>

packages/deep-state-react-form/src/__tests__/DeepState.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,69 @@ describe('Show', () => {
211211
</Form>,
212212
),
213213
).toThrowErrorMatchingInlineSnapshot(
214-
'"To use the Show component. the Field key fake-field must be set in the <FormProvider> fields prop."',
214+
'"To use the Show component, the field key `fake-field` must be set in the <FormProvider> fields prop."',
215+
);
216+
});
217+
});
218+
219+
describe('Watch', () => {
220+
it('should provide the child elements with access to the props for all keys', async () => {
221+
render(
222+
<Form
223+
fields={{ name: { type: 'input', props: { value: 'I am an input!' } } }}
224+
>
225+
{({ Watch }) => (
226+
<Watch keys={['name']}>{(props) => <>{props.name.value}</>}</Watch>
227+
)}
228+
</Form>,
229+
);
230+
231+
expect(await screen.findByText(/i am an input!/i)).toBeInTheDocument();
232+
});
233+
234+
it('should provide the child elements with access to the _meta key', async () => {
235+
const { user } = render(
236+
<Form
237+
validateOnChange
238+
validate={() => ({
239+
isValid: false,
240+
errors: { name: 'Name has an error!' },
241+
})}
242+
fields={{ name: { type: 'input', props: { label: 'Name' } } }}
243+
>
244+
{({ Field, Watch }) => (
245+
<>
246+
<Field field="name" />
247+
<Watch keys={['_meta']}>
248+
{(props) => (
249+
<>{!props._meta.isValid && props._meta.errors.name}</>
250+
)}
251+
</Watch>
252+
</>
253+
)}
254+
</Form>,
255+
);
256+
257+
// Change input to trigger validation
258+
await user.type(await screen.findByLabelText(/name/i), 'change');
259+
expect(await screen.findByText(/name has an error!/i)).toBeInTheDocument();
260+
});
261+
262+
it("should throw if a key that's not listed in the fields is used", () => {
263+
Logger.suppressLogging();
264+
265+
expect(() =>
266+
render(
267+
<Form fields={{}}>
268+
{({ Watch }) => (
269+
// Disabled to test the error state
270+
// @ts-expect-error
271+
<Watch keys={['fake-field']}>{() => <></>}</Watch>
272+
)}
273+
</Form>,
274+
),
275+
).toThrowErrorMatchingInlineSnapshot(
276+
'"To use the Watch component, the field key `fake-field` must be set in the <FormProvider> fields prop."',
215277
);
216278
});
217279
});

packages/deep-state-react-form/src/components.tsx

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,67 @@ export type DeepStateShowComponent<
2929
) => boolean;
3030
}) => React.ReactElement<any, any> | null;
3131

32+
export type DeepStateWatchComponent<
33+
FormFieldTypes extends InferForm<Form<any>> = InferForm<Form<any>>,
34+
GraphTypes extends Record<keyof GraphTypes, keyof FormFieldTypes> = Record<
35+
string,
36+
keyof FormFieldTypes
37+
>,
38+
> = <DependencyKeys extends Array<keyof GraphTypes | '_meta'>>(props: {
39+
keys: DependencyKeys;
40+
children: (
41+
data: DependencyData<FormFieldTypes, GraphTypes, DependencyKeys>,
42+
) => React.ReactElement<any, any> | null;
43+
}) => React.ReactElement<any, any> | null;
44+
3245
export const buildComponents = <Config extends Form<Record<string, any>>>(
3346
formConfig: Config,
3447
) => {
48+
const validateKeys = (
49+
type: 'Field' | 'Show' | 'Watch',
50+
keys: Array<string>,
51+
keyToTypeMap: Record<string, string>,
52+
) => {
53+
for (const key of keys) {
54+
if (key === '_meta') continue;
55+
56+
if (!keyToTypeMap[key]) {
57+
throw new Error(
58+
`To use the ${type} component, the field key \`${key}\` must be set in the <FormProvider> fields prop.`,
59+
);
60+
}
61+
}
62+
};
63+
64+
const useWatchFields = <Keys extends Array<string | number | symbol>>(
65+
keys: Keys,
66+
) => {
67+
const selector = useDeepStateSelector(
68+
...keys.map<DeepStateSelector>((key) => {
69+
return (state) => state[key as keyof typeof state];
70+
}),
71+
);
72+
73+
const { data, config } = useDeepState({ selector });
74+
75+
type DataMap = DependencyData<
76+
InferForm<Form<any>>,
77+
Record<string, keyof InferForm<Form<any>>>,
78+
Keys
79+
>;
80+
81+
const dataMap = keys.reduce<DataMap>(
82+
(acc, key, index) => ({ ...acc, [key]: data[index] }),
83+
{} as DataMap,
84+
);
85+
86+
return { dataMap, config };
87+
};
88+
3589
const DeepStateField: DeepStateFieldComponent = ({ field, children }) => {
36-
const {
37-
data: props,
38-
config: { keyToTypeMap },
39-
} = useDeepState({
40-
selector: (state) => state[field],
41-
});
42-
43-
const fieldType = keyToTypeMap[field];
90+
const { dataMap, config } = useWatchFields([field]);
91+
92+
const fieldType = config.keyToTypeMap[field];
4493
if (!fieldType) {
4594
throw new Error(
4695
`Field key ${field} must be set in the <FormProvider> fields prop.`,
@@ -50,42 +99,25 @@ export const buildComponents = <Config extends Form<Record<string, any>>>(
5099
const Component = formConfig._fields[fieldType]._component;
51100

52101
return typeof children === 'function' ? (
53-
children(props)
102+
children(dataMap[field as keyof typeof dataMap])
54103
) : (
55104
// TODO: Fix the type for Fields/BaseConfigs
56105
// StoreSnapshot is wrong
57-
<Component {...(props as any)} />
106+
<Component {...dataMap[field as keyof typeof dataMap]} />
58107
);
59108
};
60109

61110
const DeepStateShow: DeepStateShowComponent = ({ children, keys, when }) => {
62-
const selector = useDeepStateSelector(
63-
...keys.map<DeepStateSelector>((key) => {
64-
return (state) => state[key as keyof typeof state];
65-
}),
66-
);
67-
68-
const {
69-
data,
70-
config: { keyToTypeMap },
71-
} = useDeepState({ selector });
72-
73-
for (const key of keys) {
74-
if (key === '_meta') continue;
75-
76-
if (!keyToTypeMap[key]) {
77-
throw new Error(
78-
`To use the Show component. the Field key ${key} must be set in the <FormProvider> fields prop.`,
79-
);
80-
}
81-
}
82-
83-
const dataMap = keys.reduce<
84-
DependencyData<Record<string, any>, Record<string, any>, Array<any>>
85-
>((acc, key, index) => ({ ...acc, [key]: data[index] }), {});
86-
111+
const { dataMap, config } = useWatchFields(keys);
112+
validateKeys('Show', keys, config.keyToTypeMap);
87113
return when(dataMap) ? children : null;
88114
};
89115

90-
return { Field: DeepStateField, Show: DeepStateShow };
116+
const DeepStateWatch: DeepStateWatchComponent = ({ children, keys }) => {
117+
const { dataMap, config } = useWatchFields(keys);
118+
validateKeys('Watch', keys, config.keyToTypeMap);
119+
return children(dataMap);
120+
};
121+
122+
return { Field: DeepStateField, Show: DeepStateShow, Watch: DeepStateWatch };
91123
};

packages/deep-state-react-form/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ export const Builder = {
293293
GraphTypes
294294
>,
295295
Show: components.Show,
296+
Watch: components.Watch,
296297
})}
297298
</FormWrapper>
298299
</DeepStateContext.Provider>

0 commit comments

Comments
 (0)