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

Feat/emitter #61

Merged
merged 3 commits into from Oct 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions __tests__/helpers/deriveKeys.test.ts
@@ -0,0 +1,18 @@
import { deriveKeys } from '../../src/helpers/deriveKeys';

describe('deriveKeys', () => {
it('should correctly derive keys', () => {
const result = deriveKeys({ a: 'x', b: 'y' });
expect(result).toEqual(['a', 'b']);
});

it('should correctly derive keys for nested objects', () => {
const result = deriveKeys({ a: 'x', b: 'y', c: { x: 'y', d: { h: 'y' } } });
expect(result).toEqual(['a', 'b', 'c.x', 'c.d.h']);
});

it('should correctly derive keys for arrays', () => {
const result = deriveKeys({ a: 'x', b: 'y', e: [{ x: 'y', d: { h: 'y' } }], d: [{ freakz: 'hi' }] });
expect(result).toEqual(['a', 'b', 'e[0].x', 'e[0].d.h', 'd[0].freakz']);
});
});
File renamed without changes.
Expand Up @@ -18,32 +18,25 @@ const StringField = ({ fieldId }: { fieldId: string }) => {
)
}

const ArrayContainer = ({ fieldId }: { fieldId: string }) => {
const [{ add, remove, swap, insert, move, replace }, { value }] = useFieldArray(fieldId);
return (
<React.Fragment>
{value.map((val: object, i: number) => (
<React.Fragment key={fieldId + i}>
<StringField fieldId={`${fieldId}[${i}].name`} />
<button data-testid={`remove-element-${i}`} onClick={() => remove(val)}>Delete</button>
</React.Fragment>
))}
<button data-testid="add-element" onClick={() => add({ name: `${value.length}` })}>Add</button>
<button data-testid="insert-element" onClick={() => insert(1, { name: `${value.length}` })}>Insert</button>
<button data-testid="swap-element" onClick={() => swap(0, 1)}>Swap</button>
<button data-testid="move-element" onClick={() => move(0, 1)}>Move</button>
<button data-testid="replace-element" onClick={() => replace(1, { name: `hi` })}>Move</button>
</React.Fragment>
);
}

const makeHookedForm = (HookedFormOptions?: object, props?: object) => {
let injectedProps: any;
const TestHookedForm = () => {
injectedProps = useFormConnect();
const fieldId = 'friends';
const [{ add, remove, swap, insert, move, replace }, { value }] = useFieldArray(fieldId);
injectedProps = { ...useFormConnect(), value }
return (
<React.Fragment>
<ArrayContainer fieldId="friends" />
{value.map((val: object, i: number) => (
<React.Fragment key={fieldId + i}>
<StringField fieldId={`${fieldId}[${i}].name`} />
<button data-testid={`remove-element-${i}`} onClick={() => remove(val)}>Delete</button>
</React.Fragment>
))}
<button data-testid="add-element" onClick={() => add({ name: `${value.length}` })}>Add</button>
<button data-testid="insert-element" onClick={() => insert(1, { name: `${value.length}` })}>Insert</button>
<button data-testid="swap-element" onClick={() => swap(0, 1)}>Swap</button>
<button data-testid="move-element" onClick={() => move(0, 1)}>Move</button>
<button data-testid="replace-element" onClick={() => replace(1, { name: `hi` })}>Move</button>
</React.Fragment>
)
}
Expand Down
47 changes: 0 additions & 47 deletions __tests__/performance/ErrorMessage.test.tsx

This file was deleted.

Expand Up @@ -36,7 +36,7 @@ describe('ErorrMessage', () => {

expect(renders).toBe(1);
act(() => {
setFieldValue('name', 'j');
setFieldValue('age', '2');
});
expect(renders).toBe(1);
act(() => {
Expand Down
8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -50,11 +50,11 @@
"cross-env": "5.2.0",
"jest": "24.9.0",
"jest-cli": "24.9.0",
"microbundle": "^0.12.0-next.3",
"react": "16.10.2",
"react-dom": "16.10.2",
"microbundle": "0.12.0-next.6",
"react": "16.11.0",
"react-dom": "16.11.0",
"rimraf": "2.6.3",
"rollup": "1.23.1",
"rollup": "1.25.2",
"rollup-plugin-filesize": "6.1.1",
"rollup-plugin-node-resolve": "5.2.0",
"rollup-plugin-replace": "2.2.0",
Expand Down
26 changes: 21 additions & 5 deletions src/Form.tsx
@@ -1,8 +1,11 @@
import * as React from 'react';
import { formContext } from './helpers/context';
import { emit } from './context/emitter';
import { deriveInitial } from './helpers/deriveInitial';
import { deriveKeys } from './helpers/deriveKeys';
import useState from './helpers/useState';
import { Errors, InitialValues, Touched } from './types';
import { Errors, FormHookContext, InitialValues, Touched } from './types';

export const formContext = React.createContext<FormHookContext>(null as any, () => 0);

export interface SuccessBag {
resetForm: () => void;
Expand Down Expand Up @@ -62,12 +65,14 @@ const Form = <Values extends object>({
const validateForm = React.useCallback(() => {
const validationErrors = validate ? validate(values) : EMPTY_OBJ;
setErrorState(validationErrors);
emit(deriveKeys(Object.assign({}, validationErrors, formErrors)));
return validationErrors;
}, [values]);

// Provide a way to reset the full form to the initialValues.
const resetForm = React.useCallback(() => {
isDirty.current = false;
emit(deriveKeys(Object.assign({}, initialValues, values)));
setValuesState(initialValues || EMPTY_OBJ);
setTouchedState(EMPTY_OBJ);
setErrorState(EMPTY_OBJ);
Expand All @@ -80,18 +85,26 @@ const Form = <Values extends object>({
const errors = validateForm();
setTouchedState(deriveInitial(errors, true));
if (!shouldSubmitWhenInvalid && Object.keys(errors).length > 0) {
return setSubmitting(false);
setSubmitting(false);
return emit('submitting');
}

const setFormErr = (err: string) => {
setFormError(err);
emit('formError');
};

return new Promise(resolve => resolve(
onSubmit(values, { setErrors: setErrorState, setFormError })))
onSubmit(values, { setErrors: setErrorState, setFormError: setFormErr })))
.then((result: any) => {
setSubmitting(false);
emit('submitting');
if (onSuccess) onSuccess(result, { resetForm });
})
.catch((e: any) => {
setSubmitting(false);
if (onError) onError(e, { setErrors: setErrorState, setFormError });
emit('submitting');
if (onError) onError(e, { setErrors: setErrorState, setFormError: setFormErr });
});
},
[values],
Expand All @@ -116,11 +129,13 @@ const Form = <Values extends object>({
const onChange = React.useCallback((fieldId: string, value: any) => {
isDirty.current = true;
setFieldValue(fieldId, value);
emit(fieldId);
}, []);

const submit = React.useCallback((e?: React.SyntheticEvent) => {
if (e && e.preventDefault) e.preventDefault();
setSubmitting(() => true);
emit('submitting');
}, []);

const providerValue = React.useMemo(
Expand All @@ -131,6 +146,7 @@ const Form = <Values extends object>({
isSubmitting,
resetForm,
setFieldTouched: (fieldId: string, value?: boolean) => {
emit(fieldId);
touch(fieldId, value == null ? true : value);
},
setFieldValue: onChange,
Expand Down
24 changes: 14 additions & 10 deletions src/FormHoc.tsx
@@ -1,7 +1,6 @@
import * as React from 'react';
import { useSelector } from './context/useSelector';
import Form, { FormOptions } from './Form';
import useFormConnect from './useFormConnect';
import { on } from './context/emitter';
import Form, { formContext, FormOptions } from './Form';

const OptionsContainer = <Values extends object>({
enableReinitialize,
Expand All @@ -18,15 +17,20 @@ const OptionsContainer = <Values extends object>({

return function FormOuterWrapper(Component: React.ComponentType<any> | React.FC<any>) {
const NewComponent = (props: any) => {
const form = useFormConnect();
const ctx = React.useContext(formContext);
const state = React.useReducer(c => !c, false);
on(['formError', 'isSubmitting', 'isDirty'], () => {
// @ts-ignore
state[1]();
});
return (
<Component
change={form.setFieldValue}
formError={useSelector(ctx => ctx.formError)}
handleSubmit={form.submit}
isSubmitting={useSelector(ctx => ctx.isSubmitting)}
resetForm={useSelector(ctx => ctx.resetForm)}
isDirty={useSelector(ctx => ctx.isDirty)}
change={ctx.setFieldValue}
formError={ctx.formError}
handleSubmit={ctx.submit}
isSubmitting={ctx.isSubmitting}
resetForm={ctx.resetForm}
isDirty={ctx.isDirty}
{...props}
/>
);
Expand Down
14 changes: 0 additions & 14 deletions src/context/createContext.ts

This file was deleted.

50 changes: 50 additions & 0 deletions src/context/emitter.ts
@@ -0,0 +1,50 @@
type Force = () => void;

interface EmitMap {
[fieldId: string]: Array<Force>;
}

const mapping: EmitMap = {};
export function on(fieldId: string | Array<string>, cb: Force) {
if (Array.isArray(fieldId)) {
const disposers: Array<Force> = [];
fieldId.forEach((f) => {
if (!mapping[f]) { mapping[f] = []; }

mapping[f].push(cb);
disposers.push(() => {
if (mapping[f].indexOf(cb) !== -1) {
mapping[f].splice(mapping[f].indexOf(cb), 1);
}
});

});

return () => { disposers.forEach((c) => { c(); }); };
}
if (!mapping[fieldId]) {
mapping[fieldId] = [];
}
mapping[fieldId].push(cb);

return () => {
if (mapping[fieldId].indexOf(cb) !== -1) {
mapping[fieldId].splice(mapping[fieldId].indexOf(cb), 1);
}
};
}

export function emit(fieldId: string | Array<string>) {
if (Array.isArray(fieldId)) {
fieldId.forEach((f) => { notify(`${f}`); });
} else {
notify(`${fieldId}`);
}
notify('all');
}

function notify(fieldId: string) {
if (mapping[fieldId]) {
mapping[fieldId].forEach((cb) => { cb(); });
}
}
33 changes: 0 additions & 33 deletions src/context/useSelector.ts

This file was deleted.

4 changes: 0 additions & 4 deletions src/helpers/context.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/helpers/deriveKeys.ts
@@ -0,0 +1,18 @@
export const deriveKeys = (obj: { [key: string]: any }, parentKey?: string): Array<string> => {
parentKey = parentKey || '';
return Object.keys(obj).reduce<Array<string>>((acc, key) => {
const value = obj[key];
if (Array.isArray(value)) {
value.forEach((v, i) => {
typeof v === 'object' ?
acc.push(...deriveKeys(v, `${parentKey}${key}[${i}].`)) :
acc.push(`${parentKey}${key}[${i}].`);
});
} else if (typeof value === 'object') {
acc.push(...deriveKeys(value, `${parentKey}${key}.`));
} else {
acc.push(`${parentKey}${key}`);
}
return acc;
}, []);
};