Skip to content

Commit 0fb955b

Browse files
feat(react/createField): Add ability to provide custom state selector to Field
* feat(react/createField): Add ability to provide custom state selector to Field Add property 'customSelector' of type '(state: State) => any' to Field, the result of 'customSelector' goes to 'custom' property of render props * style: commas * custom => extra * fix(react): fix typings for extraSelector
1 parent 0290af0 commit 0fb955b

File tree

6 files changed

+105
-48
lines changed

6 files changed

+105
-48
lines changed

docs/modules/formBase.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,15 @@ type submit = () => Event<void>
104104
`formBase` is shipped with a bunch of memoized selectors.
105105
106106
```typescript
107-
type fieldSelector = (name: string) => <State>(state: State) => ({
108-
value: string | undefined,
109-
error: any,
110-
dirty: boolean,
111-
touched: boolean,
112-
active: boolean
113-
})
107+
type fieldSelector = <State, Extra>(name: string, extraSelector: (state: State) => Extra) =>
108+
(state: State) => ({
109+
value: string | undefined,
110+
error: any,
111+
dirty: boolean,
112+
touched: boolean,
113+
active: boolean,
114+
extra?: Extra
115+
})
114116

115117
type formSelector = () => <State>(state: State) => ({
116118
submitting: boolean,

docs/usage/react.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,16 @@ type FormApi = {
134134
pristine: boolean // fields were not touched
135135
}
136136

137-
type Field = React.Component<{
137+
type Field<State extends FormBaseState> = React.Component<{
138138
name: string, // field name
139+
extraSelector: (state: State) => any,
139140

140141
children?: (props: FieldApi) => React.ReactElement | null,
141142
render?: (props: FieldApi) => React.ReactElement | null,
142143
component?: React.ReactType<FieldApi>
143144
}>
144145

145-
type FieldApi = {
146+
type FieldApi<Extra = undefined> = {
146147
input: {
147148
name: string
148149
value: string
@@ -156,6 +157,7 @@ type FieldApi = {
156157
active: boolean // field is in focus
157158
dirty: boolean // field value differs from initial value
158159
}
160+
extra: Extra
159161
}
160162
```
161163

src/modules/formBase/selectors.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { FormBaseState } from './formBase.h'
12
import { fieldSelector, isDirtySelector, isReadySelector, isValidSelector } from './selectors'
23

34
describe('FormBase selectors', () => {
@@ -114,5 +115,31 @@ describe('FormBase selectors', () => {
114115
dirty: true,
115116
active: true
116117
})
118+
119+
expect(
120+
fieldSelector<FormBaseState & { extraValue: string }>('test', ({ extraValue }) => extraValue)(
121+
{
122+
values: {},
123+
errors: {
124+
test: 'Some error'
125+
},
126+
touched: {
127+
test: true
128+
},
129+
dirty: {
130+
test: true
131+
},
132+
active: 'test',
133+
extraValue: 'Some value'
134+
}
135+
)
136+
).toEqual({
137+
value: undefined,
138+
error: 'Some error',
139+
touched: true,
140+
dirty: true,
141+
active: true,
142+
extra: 'Some value'
143+
})
117144
})
118145
})

src/modules/formBase/selectors.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,29 @@ export const isDirtySelector = () =>
2222
export const isPristineSelector = () => <State extends FormBaseState>(state: State) =>
2323
state.pristine
2424

25-
export const fieldSelector = (name: string) =>
26-
createStructuredSelector({
27-
value: <State extends FormBaseState>(state: State) => state.values[name],
28-
error: <State extends FormBaseState>(state: State) => state.errors[name],
29-
dirty: <State extends FormBaseState>(state: State) => !!state.dirty[name],
30-
touched: <State extends FormBaseState>(state: State) => !!state.touched[name],
31-
active: <State extends FormBaseState>(state: State) => state.active === name
25+
const noop = () => undefined
26+
27+
export const fieldSelector = <State extends FormBaseState, Extra = undefined>(
28+
name: string,
29+
extraSelector: ((state: State) => Extra) = noop as any
30+
) =>
31+
createStructuredSelector<
32+
State,
33+
{
34+
value: any
35+
error: any
36+
dirty: boolean
37+
touched: boolean
38+
active: boolean
39+
extra: Extra
40+
}
41+
>({
42+
value: (state: State) => state.values[name],
43+
error: (state: State) => state.errors[name],
44+
dirty: (state: State) => !!state.dirty[name],
45+
touched: (state: State) => !!state.touched[name],
46+
active: (state: State) => state.active === name,
47+
extra: extraSelector
3248
})
3349

3450
export const formSelector = () =>

src/react/createField/createField.h.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { SyntheticEvent } from 'react'
2+
import { FormBaseState } from '../../modules/formBase/formBase.h'
23
import { RenderProps } from '../createConsumer/createConsumer.h'
34

4-
export type FieldApi = {
5+
export type FieldApi<Extra = undefined> = {
56
input: {
67
name: string
78
value: string
@@ -14,8 +15,10 @@ export type FieldApi = {
1415
touched: boolean
1516
active: boolean
1617
}
18+
extra: Extra
1719
}
1820

19-
export type FieldProps = RenderProps<FieldApi> & {
21+
export type FieldProps<State extends FormBaseState, Extra> = RenderProps<FieldApi<Extra>> & {
2022
name: string
23+
extraSelector?: (state: State) => Extra
2124
}
Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import React, { StatelessComponent, SyntheticEvent } from 'react'
1+
// tslint:disable-next-line no-unused-variable
2+
import React, { createElement, SyntheticEvent } from 'react'
23
import { setActive, setTouched, setValue } from '../../modules/formBase/events'
4+
import { FormBaseState } from '../../modules/formBase/formBase.h'
35
import { fieldSelector } from '../../modules/formBase/selectors'
46
import { createConsumer } from '../createConsumer/createConsumer'
57

@@ -48,10 +50,16 @@ import { FieldProps } from './createField.h'
4850
*
4951
* @param app Stapp application
5052
*/
51-
export const createField = <State, Api>(app: Stapp<State, Api>): StatelessComponent<FieldProps> => {
53+
export const createField = <State extends FormBaseState, Api>(app: Stapp<State, Api>) => {
5254
const Consumer = createConsumer(app)
5355

54-
return ({ name, children, render, component }) => {
56+
return <Extra>({
57+
name,
58+
extraSelector,
59+
children,
60+
render,
61+
component
62+
}: FieldProps<State, Extra>) => {
5563
const handleChange = (event: SyntheticEvent<HTMLInputElement>) =>
5664
app.dispatch(
5765
setValue({
@@ -66,34 +74,33 @@ export const createField = <State, Api>(app: Stapp<State, Api>): StatelessCompon
6674

6775
const handleFocus = () => app.dispatch(setActive(name))
6876

69-
return (
70-
<Consumer mapState={fieldSelector(name)}>
71-
{({ value, error, dirty, touched, active }) =>
72-
renderComponent(
73-
{
74-
children,
75-
render,
76-
component
77+
return createElement(Consumer, {
78+
mapState: fieldSelector(name, extraSelector),
79+
render: ({ value, error, dirty, touched, active, extra }: any) =>
80+
renderComponent(
81+
{
82+
children,
83+
render,
84+
component
85+
},
86+
{
87+
input: {
88+
name,
89+
value: value || '',
90+
onChange: handleChange,
91+
onBlur: handleBlur,
92+
onFocus: handleFocus
7793
},
78-
{
79-
input: {
80-
name,
81-
value: value || '',
82-
onChange: handleChange,
83-
onBlur: handleBlur,
84-
onFocus: handleFocus
85-
},
86-
meta: {
87-
error,
88-
touched,
89-
active,
90-
dirty
91-
}
94+
meta: {
95+
error,
96+
touched,
97+
active,
98+
dirty
9299
},
93-
'Field'
94-
)
95-
}
96-
</Consumer>
97-
)
100+
extra
101+
},
102+
'Field'
103+
)
104+
})
98105
}
99106
}

0 commit comments

Comments
 (0)