Skip to content

Commit

Permalink
Strongly typed form values for Flow and Typescript (#516)
Browse files Browse the repository at this point in the history
* Strongly typed form values for Flow and Typescript

* Upped version variable and ff version dep
  • Loading branch information
erikras authored Jun 11, 2019
1 parent f349c07 commit 9fe62c5
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 84 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"eslint-plugin-react": "^7.13.0",
"eslint-plugin-react-hooks": "^1.6.0",
"fast-deep-equal": "^2.0.1",
"final-form": "^4.13.0",
"final-form": "^4.14.0",
"flow-bin": "^0.98.1",
"glow": "^1.2.2",
"husky": "^2.3.0",
Expand All @@ -88,7 +88,7 @@
"typescript": "^3.4.5"
},
"peerDependencies": {
"final-form": "^4.13.0",
"final-form": "^4.14.0",
"react": "^16.8.0"
},
"lint-staged": {
Expand Down
17 changes: 12 additions & 5 deletions src/FormSpy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
import * as React from 'react'
import renderComponent from './renderComponent'
import type { FormSpyPropsWithForm as Props, FormSpyRenderProps } from './types'
import type { FormApi } from 'final-form'
import type { FormApi, FormValuesShape } from 'final-form'
import isSyntheticEvent from './isSyntheticEvent'
import useFormState from './useFormState'
import ReactFinalFormContext from './context'
import getContext from './getContext'

const FormSpy = ({ onChange, subscription, ...rest }: Props) => {
const reactFinalForm: ?FormApi = React.useContext(ReactFinalFormContext)
function FormSpy<FormValues: FormValuesShape>({
onChange,
subscription,
...rest
}: Props<FormValues>) {
const ReactFinalFormContext = getContext<FormValues>()
const reactFinalForm: ?FormApi<FormValues> = React.useContext(
ReactFinalFormContext
)
if (!reactFinalForm) {
throw new Error('FormSpy must be used inside of a ReactFinalForm component')
}
Expand All @@ -17,7 +24,7 @@ const FormSpy = ({ onChange, subscription, ...rest }: Props) => {
return null
}

const renderProps: FormSpyRenderProps = {
const renderProps: FormSpyRenderProps<FormValues> = {
form: {
...reactFinalForm,
reset: eventOrValues => {
Expand Down
26 changes: 14 additions & 12 deletions src/ReactFinalForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
Config,
FormSubscription,
FormState,
FormValuesShape,
Unsubscribe
} from 'final-form'
import type { FormProps as Props } from './types'
Expand All @@ -19,10 +20,10 @@ import useConstant from './useConstant'
import shallowEqual from './shallowEqual'
import isSyntheticEvent from './isSyntheticEvent'
import type { FormRenderProps } from './types.js.flow'
import ReactFinalFormContext from './context'
import getContext from './getContext'
import useLatest from './useLatest'

export const version = '6.0.1'
export const version = '6.1.0'

const versions = {
'final-form': ffVersion,
Expand All @@ -37,7 +38,7 @@ export const all: FormSubscription = formSubscriptionItems.reduce(
{}
)

const ReactFinalForm = ({
function ReactFinalForm<FormValues: FormValuesShape>({
debug,
decorators,
destroyOnUnregister,
Expand All @@ -50,8 +51,9 @@ const ReactFinalForm = ({
validate,
validateOnBlur,
...rest
}: Props) => {
const config: Config = {
}: Props<FormValues>) {
const ReactFinalFormContext = getContext<FormValues>()
const config: Config<FormValues> = {
debug,
destroyOnUnregister,
initialValues,
Expand All @@ -62,16 +64,16 @@ const ReactFinalForm = ({
validateOnBlur
}

const form: FormApi = useConstant(() => {
const f = createForm(config)
const form: FormApi<FormValues> = useConstant(() => {
const f = createForm<FormValues>(config)
f.pauseValidation()
return f
})

// synchronously register and unregister to query form state for our subscription on first render
const [state, setState] = React.useState<FormState>(
(): FormState => {
let initialState: FormState = {}
const [state, setState] = React.useState<FormState<FormValues>>(
(): FormState<FormValues> => {
let initialState: FormState<FormValues> = {}
form.subscribe(state => {
initialState = state
}, subscription)()
Expand All @@ -81,7 +83,7 @@ const ReactFinalForm = ({

// save a copy of state that can break through the closure
// on the shallowEqual() line below.
const stateRef = useLatest<FormState>(state)
const stateRef = useLatest<FormState<FormValues>>(state)

React.useEffect(() => {
// We have rendered, so all fields are no registered, so we can unpause validation
Expand Down Expand Up @@ -170,7 +172,7 @@ const ReactFinalForm = ({
return form.submit()
}

const renderProps: FormRenderProps = {
const renderProps: FormRenderProps<FormValues> = {
// assign to force Flow check
...state,
form: {
Expand Down
5 changes: 0 additions & 5 deletions src/context.js

This file was deleted.

14 changes: 14 additions & 0 deletions src/getContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @flow
import * as React from 'react'
import type { FormApi, FormValuesShape } from 'final-form'

let instance: React.Context<?FormApi<any>>

export default function getContext<
FormValues: FormValuesShape
>(): React.Context<FormApi<FormValues>> {
if (!instance) {
instance = React.createContext<?FormApi<FormValues>>()
}
return ((instance: any): React.Context<FormApi<FormValues>>)
}
6 changes: 5 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// @flow
import Form from './ReactFinalForm'
import FormSpy from './FormSpy'
export { default as Field } from './Field'
export { default as Form, version } from './ReactFinalForm'
export { default as FormSpy } from './FormSpy'
export { default as useField } from './useField'
export { default as useFormState } from './useFormState'
export { default as useForm } from './useForm'
export { default as context } from './context'
export function withTypes() {
return { Form, FormSpy }
}
18 changes: 13 additions & 5 deletions src/index.js.flow
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import * as React from 'react'
import type { FormApi, FormState } from 'final-form'
import type { FormApi, FormState, FormValuesShape } from 'final-form'
import type {
FieldProps,
FieldRenderProps,
Expand All @@ -20,12 +20,20 @@ export type {
} from './types'

declare export var Field: React.ComponentType<FieldProps>
declare export var Form: React.ComponentType<FormProps>
declare export var FormSpy: React.ComponentType<FormSpyProps>
declare export var useForm: (componentName?: string) => FormApi
declare export var useFormState: UseFormStateParams => ?FormState
declare export var Form: React.ComponentType<FormProps<Object>>
declare export var FormSpy: React.ComponentType<FormSpyProps<Object>>
declare export function useForm<FormValues: FormValuesShape>(
componentName?: string
): FormApi<FormValues>
declare export function useFormState<FormValues>(
params: UseFormStateParams<FormValues>
): ?FormState<FormValues>
declare export var useField: (
name: string,
config: UseFieldConfig
) => FieldRenderProps
declare export function withTypes<FormValues: FormValuesShape>(): {
Form: React.ComponentType<FormProps<FormValues>>,
FormSpy: React.ComponentType<FormSpyProps<FormValues>>
}
declare export var version: string
41 changes: 22 additions & 19 deletions src/types.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type {
Decorator,
FormState,
FormSubscription,
FormValuesShape,
FieldSubscription,
FieldValidator
} from 'final-form'

type SupportedInputs = 'input' | 'select' | 'textarea'

export type ReactContext = {
reactFinalForm: FormApi
export type ReactContext<FormValues: FormValuesShape> = {
reactFinalForm: FormApi<FormValues>
}

export type FieldInputProps = {
Expand Down Expand Up @@ -49,27 +50,27 @@ export type FieldRenderProps = {
}
}

export type FormRenderProps = {
export type FormRenderProps<FormValues: FormValuesShape> = {
handleSubmit: (?SyntheticEvent<HTMLFormElement>) => ?Promise<?Object>,
form: FormApi
} & FormState
form: FormApi<FormValues>
} & FormState<FormValues>

export type FormSpyRenderProps = {
form: FormApi
} & FormState
export type FormSpyRenderProps<FormValues: FormValuesShape> = {
form: FormApi<FormValues>
} & FormState<FormValues>

export type RenderableProps<T> = {
component?: React.ComponentType<*> | SupportedInputs,
children?: ((props: T) => React.Node) | React.Node,
render?: (props: T) => React.Node
}

export type FormProps = {
export type FormProps<FormValues: FormValuesShape> = {
subscription?: FormSubscription,
decorators?: Decorator[],
decorators?: Decorator<FormValues>[],
initialValuesEqual?: (?Object, ?Object) => boolean
} & Config &
RenderableProps<FormRenderProps>
} & Config<FormValues> &
RenderableProps<FormRenderProps<FormValues>>

export type UseFieldConfig = {
afterSubmit?: () => void,
Expand All @@ -95,14 +96,16 @@ export type FieldProps = UseFieldConfig & {
name: string
} & RenderableProps<FieldRenderProps>

export type UseFormStateParams = {
onChange?: (formState: FormState) => void,
export type UseFormStateParams<FormValues: FormValuesShape> = {
onChange?: (formState: FormState<FormValues>) => void,
subscription?: FormSubscription
}

export type FormSpyProps = UseFormStateParams &
RenderableProps<FormSpyRenderProps>
export type FormSpyProps<
FormValues: FormValuesShape
> = UseFormStateParams<FormValues> &
RenderableProps<FormSpyRenderProps<FormValues>>

export type FormSpyPropsWithForm = {
reactFinalForm: FormApi
} & FormSpyProps
export type FormSpyPropsWithForm<FormValues: FormValuesShape> = {
reactFinalForm: FormApi<FormValues>
} & FormSpyProps<FormValues>
13 changes: 9 additions & 4 deletions src/useField.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// @flow
import * as React from 'react'
import { fieldSubscriptionItems } from 'final-form'
import type { FieldSubscription, FieldState, FormApi } from 'final-form'
import type {
FieldSubscription,
FieldState,
FormApi,
FormValuesShape
} from 'final-form'
import type { UseFieldConfig, FieldInputProps, FieldRenderProps } from './types'
import isReactNative from './isReactNative'
import getValue from './getValue'
Expand All @@ -18,7 +23,7 @@ const defaultFormat = (value: ?any, name: string) =>
const defaultParse = (value: ?any, name: string) =>
value === '' ? undefined : value

const useField = (
function useField<FormValues: FormValuesShape>(
name: string,
{
afterSubmit,
Expand All @@ -38,8 +43,8 @@ const useField = (
validateFields,
value: _value
}: UseFieldConfig = {}
): FieldRenderProps => {
const form: FormApi = useForm('useField')
): FieldRenderProps {
const form: FormApi<FormValues> = useForm<FormValues>('useField')

const validateRef = useLatest(validate)

Expand Down
11 changes: 7 additions & 4 deletions src/useForm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// @flow
import * as React from 'react'
import type { FormApi } from 'final-form'
import ReactFinalFormContext from './context'
import type { FormApi, FormValuesShape } from 'final-form'
import getContext from './getContext'

const useForm = (componentName?: string): FormApi => {
const form: ?FormApi = React.useContext(ReactFinalFormContext)
function useForm<FormValues: FormValuesShape>(
componentName?: string
): FormApi<FormValues> {
const ReactFinalFormContext = getContext<FormValues>()
const form: ?FormApi<FormValues> = React.useContext(ReactFinalFormContext)
if (!form) {
throw new Error(
`${componentName || 'useForm'} must be used inside of a <Form> component`
Expand Down
14 changes: 7 additions & 7 deletions src/useFormState.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
// @flow
import * as React from 'react'
import type { UseFormStateParams } from './types'
import type { FormState, FormApi } from 'final-form'
import type { FormState, FormApi, FormValuesShape } from 'final-form'
import { all } from './ReactFinalForm'
import useForm from './useForm'

const useFormState = ({
function useFormState<FormValues: FormValuesShape>({
onChange,
subscription = all
}: UseFormStateParams = {}): FormState => {
const form: FormApi = useForm('useFormState')
}: UseFormStateParams<FormValues> = {}): FormState<FormValues> {
const form: FormApi<FormValues> = useForm<FormValues>('useFormState')
const firstRender = React.useRef(true)

// synchronously register and unregister to query field state for our subscription on first render
const [state, setState] = React.useState<FormState>(
(): FormState => {
let initialState: FormState = {}
const [state, setState] = React.useState<FormState<FormValues>>(
(): FormState<FormValues> => {
let initialState: FormState<FormValues> = {}
form.subscribe(state => {
initialState = state
}, subscription)()
Expand Down
Loading

0 comments on commit 9fe62c5

Please sign in to comment.