Skip to content

Commit e687cb5

Browse files
author
Larry Botha
authored
Merge pull request #102 from tjinauyeung/feat/add-form-component-context-prop
2 parents 29aeea0 + 1de3f93 commit e687cb5

File tree

4 files changed

+166
-83
lines changed

4 files changed

+166
-83
lines changed

lib/components/Form.svelte

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@
66
export let initialValues = {};
77
export let validate = null;
88
export let validationSchema = null;
9-
export let onSubmit;
9+
export let onSubmit = () => {
10+
throw new Error(
11+
'onSubmit is a required property in <Form /> when using the fallback context',
12+
);
13+
};
14+
export let context = createForm({
15+
initialValues,
16+
onSubmit,
17+
validate,
18+
validationSchema,
19+
});
1020
1121
const {
1222
form,
@@ -20,12 +30,7 @@
2030
updateTouched,
2131
updateValidateField,
2232
validateField,
23-
} = createForm({
24-
initialValues,
25-
validationSchema,
26-
validate,
27-
onSubmit,
28-
});
33+
} = context;
2934
3035
setContext(key, {
3136
form,

test/specs/components/fixtures/input.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
export let field;
66
77
const context = getContext(key);
8-
const form = context.form;
8+
const {form} = context;
99
</script>
1010

1111
<label for={field}>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
import {getContext} from 'svelte';
3+
import {key} from '../../../../lib';
4+
5+
const context = getContext(key);
6+
7+
export let storeProp;
8+
9+
const store = context[storeProp];
10+
</script>
11+
12+
<div {...$$restProps}>
13+
{JSON.stringify($store)}
14+
</div>

test/specs/components/form.spec.js

Lines changed: 139 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,157 @@
11
import {render, fireEvent, waitFor} from '@testing-library/svelte';
2-
import {writable, get as svelteGet} from 'svelte/store';
32

43
import Input from './fixtures/input.svelte';
4+
import StoreContextInspector from './fixtures/store-context-inspector.svelte';
55

66
import Form from 'lib/components/Form.svelte';
7+
import {createForm} from 'lib/index';
78

89
const defaultProps = {
910
initialValues: {foo: ''},
1011
onSubmit() {},
1112
};
1213

13-
const get = (store) => {
14-
/**
15-
* not sure why we need to invoke `get` twice, but we do
16-
*/
17-
return svelteGet(svelteGet(store));
14+
const contextType = {
15+
userDefined: 'user',
16+
fallback: 'fallback',
1817
};
1918

2019
describe('Form', () => {
21-
test('-> exposes `form` prop via slot properties', async () => {
22-
const inputId = 'foo';
23-
const props = {
24-
...defaultProps,
25-
initialValues: {[inputId]: ''},
20+
describe.each`
21+
contextName | type
22+
${'fallback'} | ${contextType.fallback}
23+
${'user-defined'} | ${contextType.userDefined}
24+
`('-> with $contextName context', ({type}) => {
25+
const getForm = (props = defaultProps) => {
26+
const form =
27+
type === contextType.userDefined ? createForm(props) : undefined;
28+
const resultProps = type === contextType.userDefined ? {} : props;
29+
30+
return [resultProps, form];
2631
};
27-
let form = writable({});
28-
const {getByLabelText} = render(
29-
<Form {...props} let_form={form}>
30-
<Input field={inputId} />
31-
</Form>,
32-
);
33-
const input = getByLabelText(inputId);
3432

35-
expect(get(form)).toHaveProperty('foo', '');
36-
37-
const value = 'bar';
38-
39-
await fireEvent.input(input, {target: {value}});
40-
41-
expect(get(form)).toHaveProperty('foo', value);
42-
});
43-
44-
test('-> exposes `state` prop via slot properties', async () => {
45-
const inputId = 'foo';
46-
const props = {
47-
...defaultProps,
48-
initialValues: {[inputId]: ''},
49-
};
50-
let state = writable({});
51-
const {getByLabelText} = render(
52-
<Form {...props} let_state={state}>
53-
<Input field={inputId} />
54-
</Form>,
55-
);
56-
const input = getByLabelText(inputId);
57-
58-
expect(get(state)).toHaveProperty('isModified', false);
59-
60-
const value = 'bar';
61-
62-
await fireEvent.input(input, {target: {value}});
63-
64-
expect(get(state)).toHaveProperty('isModified', true);
65-
});
66-
67-
test.each`
68-
methodName
69-
${'handleChange'}
70-
${'handleSubmit'}
71-
${'updateField'}
72-
${'updateInitialValues'}
73-
${'updateTouched'}
74-
${'updateValidateField'}
75-
${'validateField'}
76-
`('-> exposes $methodName via slot properties', ({methodName}) => {
77-
const inputId = 'foo';
78-
const methodSetter = jest.fn();
79-
const props = {
80-
...defaultProps,
81-
initialValues: {[inputId]: ''},
82-
[`let_${methodName}`]: methodSetter,
83-
};
84-
render(
85-
<Form {...props}>
86-
<Input field={inputId} />
87-
</Form>,
88-
);
89-
90-
expect(methodSetter.mock.calls[0][0]).toBeInstanceOf(Function);
91-
expect(methodSetter.mock.calls[0][0]).toHaveProperty('name', methodName);
33+
test('-> exposes `form` store to children via context', async () => {
34+
const inputId = 'foo';
35+
const baseProps = {...defaultProps, initialValues: {[inputId]: ''}};
36+
const [props, form] = getForm(baseProps);
37+
const {getByLabelText, getByTestId} = render(
38+
<Form {...props} context={form}>
39+
<Input field={inputId} />
40+
41+
<StoreContextInspector data-testid="values" storeProp="form" />
42+
</Form>,
43+
);
44+
const input = getByLabelText(inputId);
45+
const valuesEl = getByTestId('values');
46+
let values = JSON.parse(valuesEl.textContent);
47+
48+
expect(values[inputId]).toBe(baseProps.initialValues[inputId]);
49+
expect(input.value).toBe(values[inputId]);
50+
51+
const value = 'bar';
52+
53+
await fireEvent.input(input, {target: {value}});
54+
55+
values = JSON.parse(valuesEl.textContent);
56+
57+
expect(values[inputId]).toBe(value);
58+
expect(input.value).toBe(value);
59+
});
60+
61+
test('-> exposes `state` store to children via context', async () => {
62+
const inputId = 'foo';
63+
const baseProps = {...defaultProps, initialValues: {[inputId]: ''}};
64+
const [props, form] = getForm(baseProps);
65+
const {getByLabelText, getByTestId} = render(
66+
<Form {...props} context={form}>
67+
<Input field={inputId} />
68+
69+
<StoreContextInspector data-testid="state" storeProp="state" />
70+
</Form>,
71+
);
72+
const input = getByLabelText(inputId);
73+
const stateEl = getByTestId('state');
74+
let state = JSON.parse(stateEl.textContent);
75+
76+
expect(state).toHaveProperty('isModified', false);
77+
78+
const value = 'bar';
79+
80+
await fireEvent.input(input, {target: {value}});
81+
82+
state = JSON.parse(stateEl.textContent);
83+
expect(state).toHaveProperty('isModified', true);
84+
});
85+
86+
test('-> exposes `errors` store to children via context', async () => {
87+
const inputId = 'foo';
88+
const validationError = `${inputId} is required`;
89+
const baseProps = {
90+
...defaultProps,
91+
initialValues: {[inputId]: ''},
92+
validate: (values) => {
93+
if (!values[inputId]) {
94+
return {[inputId]: validationError};
95+
}
96+
},
97+
};
98+
const [props, form] = getForm(baseProps);
99+
const {getByTestId, getByLabelText, getByRole} = render(
100+
<Form {...props} context={form}>
101+
<Input field={inputId} />
102+
<StoreContextInspector data-testid="error" storeProp="errors" />
103+
104+
<button type="submit">submit</button>
105+
</Form>,
106+
);
107+
const input = getByLabelText(inputId);
108+
const button = getByRole('button');
109+
const errorEl = getByTestId('error');
110+
let errors = JSON.parse(errorEl.textContent);
111+
112+
expect(errors).toHaveProperty(inputId, '');
113+
114+
await fireEvent.click(button);
115+
116+
await waitFor(() => {
117+
errors = JSON.parse(errorEl.textContent);
118+
expect(errors).toHaveProperty(inputId, validationError);
119+
});
120+
121+
const value = 'bar';
122+
123+
await fireEvent.input(input, {target: {value}});
124+
await fireEvent.click(button);
125+
126+
await waitFor(() => {
127+
errors = JSON.parse(errorEl.textContent);
128+
expect(errors).toHaveProperty(inputId, '');
129+
});
130+
});
131+
132+
test.each`
133+
methodName
134+
${'handleChange'}
135+
${'handleSubmit'}
136+
${'updateField'}
137+
${'updateInitialValues'}
138+
${'updateTouched'}
139+
${'updateValidateField'}
140+
${'validateField'}
141+
`('-> exposes $methodName via slot properties', ({methodName}) => {
142+
const inputId = 'foo';
143+
const methodSetter = jest.fn();
144+
const baseProps = {...defaultProps, initialValues: {[inputId]: ''}};
145+
const methodProps = {[`let_${methodName}`]: methodSetter};
146+
const [props, form] = getForm(baseProps);
147+
render(
148+
<Form {...props} {...methodProps} context={form}>
149+
<Input field={inputId} />
150+
</Form>,
151+
);
152+
153+
expect(methodSetter.mock.calls[0][0]).toBeInstanceOf(Function);
154+
expect(methodSetter.mock.calls[0][0]).toHaveProperty('name', methodName);
155+
});
92156
});
93157
});

0 commit comments

Comments
 (0)