Skip to content

Commit ea903c9

Browse files
committed
Implement middleware api
1 parent 121de24 commit ea903c9

File tree

9 files changed

+115
-34
lines changed

9 files changed

+115
-34
lines changed

src/DSVImport.stories.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ export default { title: 'Usage' };
66

77
type BasicType = { forename: string; surname: string; email: string };
88

9-
const columns: ColumnsType<BasicType> = [
10-
{ key: 'forename', label: 'Forename' },
11-
{ key: 'surname', label: 'Surname' },
12-
{ key: 'email', label: 'Email' }
13-
];
14-
15-
const onChangeAction = action('Parsed value has changed');
16-
179
export const BasicUsage = () => {
10+
const columns: ColumnsType<BasicType> = [
11+
{ key: 'forename', label: 'Forename' },
12+
{ key: 'surname', label: 'Surname' },
13+
{ key: 'email', label: 'Email' }
14+
];
15+
const onChangeAction = action('Parsed value has changed');
16+
1817
return (
1918
<DSVImport<BasicType> columns={columns} onChange={onChangeAction}>
2019
<DSVImport.TextareaInput />
@@ -25,6 +24,12 @@ export const BasicUsage = () => {
2524
BasicUsage.story = { name: 'Basic usage' };
2625

2726
export const UsingCallbacks = () => {
27+
const columns: ColumnsType<BasicType> = [
28+
{ key: 'forename', label: 'Forename' },
29+
{ key: 'surname', label: 'Surname' },
30+
{ key: 'email', label: 'Email' }
31+
];
32+
const onChangeAction = action('Parsed value has changed');
2833
const [state, setState] = useState<BasicType[]>([]);
2934

3035
const handleOnChange = (value: BasicType[]) => {
@@ -43,4 +48,20 @@ export const UsingCallbacks = () => {
4348
</>
4449
);
4550
};
46-
UsingCallbacks.story = { name: 'Using callbacks with states' };
51+
UsingCallbacks.story = { name: 'Using callbacks a state' };
52+
53+
export const UsingValidation = () => {
54+
const columns: ColumnsType<BasicType> = [
55+
{ key: 'forename', label: 'Forename' },
56+
{ key: 'surname', label: 'Surname' },
57+
{ key: 'email', label: 'Email', rules: [{ constraint: { unique: true }, message: 'Must be unique' }] }
58+
];
59+
const onChangeAction = action('Parsed value has changed');
60+
61+
return (
62+
<DSVImport<BasicType> columns={columns} onChange={onChangeAction}>
63+
<DSVImport.TextareaInput />
64+
<DSVImport.TablePreview />
65+
</DSVImport>
66+
);
67+
};

src/DSVImport.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React, { PropsWithChildren, useReducer, useEffect } from 'react';
22
import { ColumnsType } from './models/column';
3-
import { getDSVImportContext, useDSVImport } from './features/context';
3+
import { getDSVImportContext, useDSVImport, createReducer } from './features/context';
44
import { createParserMiddleware } from './middlewares/parserMiddleware';
55
import { State } from './models/state';
6+
import { applyMiddlewares } from './middlewares/middleware';
7+
import { createValidatorMiddleware } from './middlewares/validatorMiddleware';
68

79
interface EventListenerProps<T> {
810
onChange?: (value: T[]) => void;
@@ -27,11 +29,12 @@ export interface Props<T> {
2729

2830
export const DSVImport = <T extends { [key: string]: string }>(props: PropsWithChildren<Props<T>>) => {
2931
const DSVImportContext = getDSVImportContext<T>();
30-
const middleware = createParserMiddleware<T>();
3132
const initialValues: State<T> = { columns: props.columns };
33+
const [state, dispatch] = useReducer(createReducer<T>(), initialValues);
34+
const enhancedDispatch = applyMiddlewares(state, dispatch, createParserMiddleware(), createValidatorMiddleware());
3235

3336
return (
34-
<DSVImportContext.Provider value={useReducer(middleware, initialValues)}>
37+
<DSVImportContext.Provider value={[state, enhancedDispatch]}>
3538
<EventListener<T> onChange={props.onChange} />
3639
{props.children}
3740
</DSVImportContext.Provider>

src/features/context.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export const reducer = <T>(state: State<T>, action: Actions<T>) => {
1414
}
1515
};
1616

17+
export const createReducer = <T>() => {
18+
return (state: State<T>, action: Actions<T>) => {
19+
return reducer(state, action);
20+
};
21+
};
22+
1723
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1824
let contextSingleton: React.Context<[State<any>, Dispatch<Actions<any>>]>;
1925
export const getDSVImportContext = <T>() => {

src/middlewares/middleware.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Dispatch } from 'react';
2+
import { Actions } from '../features/context';
3+
import { State } from '../models/state';
4+
5+
type Middleware = <T>(state: State<T>, dispatch: Dispatch<Actions<T>>, action: Actions<T>) => void;
6+
7+
export const applyMiddlewares = <T>(state: State<T>, dispatch: Dispatch<Actions<T>>, ...middlewares: Middleware[]) => (
8+
action: Actions<T>
9+
) => {
10+
const without = (i: number) => {
11+
return middlewares.filter((_, filterIndex) => i !== filterIndex);
12+
};
13+
14+
const next = (nextMiddlewares: Middleware[]) => (value: Actions<T>) => {
15+
dispatch(value);
16+
nextMiddlewares.forEach((m, i) => {
17+
m(state, next(without(i)), value);
18+
});
19+
};
20+
21+
middlewares.forEach((m, i) => m(state, next(without(i)), action));
22+
};

src/middlewares/parserMiddleware.test.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,44 @@ describe('parserMiddleware', () => {
1616
1717

1818
it('should set new parsed data when raw data is set', () => {
19-
const newState = middleware(defaultState, { type: 'setRaw', raw: 'Max' });
19+
const dispatchMock = jest.fn();
20+
middleware(defaultState, dispatchMock, { type: 'setRaw', raw: 'Max' });
2021

21-
expect(newState.parsed).toStrictEqual([{ forename: 'Max', surname: undefined, email: undefined }]);
22+
expect(dispatchMock).toBeCalledTimes(1);
23+
expect(dispatchMock).toBeCalledWith({
24+
type: 'setParsed',
25+
parsed: [{ forename: 'Max', surname: undefined, email: undefined }]
26+
});
2227
});
2328

2429
it('should set parsed data to an empty array if there is no raw data', () => {
25-
middleware(defaultState, { type: 'setRaw', raw: 'Max' });
26-
const newState = middleware(defaultState, { type: 'setRaw', raw: '' });
27-
28-
expect(newState.parsed).toStrictEqual([]);
30+
const dispatchMock = jest.fn();
31+
const stateWithRawData = {
32+
...defaultState,
33+
raw: 'Max',
34+
parsed: [{ forename: 'Max', surname: '', email: '' }]
35+
};
36+
middleware(stateWithRawData, dispatchMock, { type: 'setRaw', raw: '' });
37+
38+
expect(dispatchMock).toBeCalledTimes(1);
39+
expect(dispatchMock).toBeCalledWith({
40+
type: 'setParsed',
41+
parsed: []
42+
});
2943
});
3044

3145
it('should detect the correct delimiter from raw data', () => {
3246
Object.values(Delimiter).forEach((d) => {
33-
const newState = middleware(defaultState, { type: 'setRaw', raw: rawData.replace(/!/g, d) });
34-
35-
expect(newState.parsed?.length).toBe(2);
36-
expect(newState.parsed?.[0]).toStrictEqual({ forename: 'Max', surname: 'Muster', email: '[email protected]' });
37-
expect(newState.parsed?.[1]).toStrictEqual({
38-
forename: '',
39-
surname: '',
40-
47+
const dispatchMock = jest.fn();
48+
middleware(defaultState, dispatchMock, { type: 'setRaw', raw: rawData.replace(/!/g, d) });
49+
50+
expect(dispatchMock).toBeCalledTimes(1);
51+
expect(dispatchMock).toBeCalledWith({
52+
type: 'setParsed',
53+
parsed: [
54+
{ forename: 'Max', surname: 'Muster', email: '[email protected]' },
55+
{ forename: '', surname: '', email: '[email protected]' }
56+
]
4157
});
4258
});
4359
});

src/middlewares/parserMiddleware.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { State } from '../models/state';
2-
import { Actions, reducer } from '../features/context';
2+
import { Actions } from '../features/context';
33
import { Delimiter } from '../models/delimiter';
44
import { ColumnsType } from '../models/column';
5+
import { Dispatch } from 'react';
56

67
const detectDelimiterFromValue = (value: string, defaultDelimiter = Delimiter.COMMA) => {
78
let currentDelimiter = defaultDelimiter;
@@ -29,9 +30,7 @@ const parseData = <T>(value: string, columns: ColumnsType<T>, delimiter: Delimit
2930
};
3031

3132
export const createParserMiddleware = <T>() => {
32-
return (state: State<T>, action: Actions<T>) => {
33-
let newState = reducer<T>(state, action);
34-
33+
return (state: State<T>, next: Dispatch<Actions<T>>, action: Actions<T>) => {
3534
if (action.type === 'setRaw') {
3635
const delimiter = detectDelimiterFromValue(action.raw);
3736

@@ -40,9 +39,7 @@ export const createParserMiddleware = <T>() => {
4039
parsed = parseData<T>(action.raw, state.columns, delimiter);
4140
}
4241

43-
newState = reducer<T>(state, { type: 'setParsed', parsed });
42+
next({ type: 'setParsed', parsed });
4443
}
45-
46-
return newState;
4744
};
4845
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { State } from '../models/state';
2+
import { Actions } from '../features/context';
3+
import { Dispatch } from 'react';
4+
5+
export const createValidatorMiddleware = <T>() => {
6+
return (state: State<T>, next: Dispatch<Actions<T>>, action: Actions<T>) => {
7+
if (action.type === 'setParsed') {
8+
}
9+
};
10+
};

src/models/column.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export type ColumnsType<T> = { key: keyof T; label: string }[];
1+
import { Rule } from './rule';
2+
3+
export type ColumnsType<T> = { key: keyof T; label: string; rules?: Rule[] }[];

src/models/rule.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Rule = {
2+
message: string;
3+
constraint: { unique: boolean } | { callback: (value: string) => boolean };
4+
};

0 commit comments

Comments
 (0)