Created
September 24, 2017 14:25
-
-
Save doomsower/ec88df86c8608bd72795488e91071b88 to your computer and use it in GitHub Desktop.
Using react apollo with redux-form
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createMemoryHistory, History } from 'history'; | |
import * as Joi from 'joi'; | |
import * as React from 'react'; | |
import { Provider } from 'react-redux'; | |
import { MemoryRouter, Router } from 'react-router'; | |
import { ComponentEnhancer, withProps } from 'recompose'; | |
import { combineReducers, createStore } from 'redux'; | |
import { InjectedFormProps, reducer as formReducer } from 'redux-form'; | |
import { mountWithMuiContext } from '../../test'; | |
import { Omit } from '../../ww-commons/ts'; | |
import Loading from '../Loading'; | |
import { formContainer, FormContainerOptions } from './formContainer'; | |
import { validateInput } from './validateInput'; | |
import { flushPromises } from '../../test/flushPromises'; | |
jest.mock('./validateInput', () => ({ | |
validateInput: jest.fn(), | |
})); | |
interface QueryResult { | |
entity: { | |
data: { foo: string }, | |
loading: boolean, | |
}; | |
} | |
interface MutationResult { | |
upsertEntity: { | |
foo: string; | |
}; | |
} | |
interface FormInput { | |
foo: string; | |
} | |
const ValidationSchema = Joi.object().keys({ foo: Joi.string() }); | |
type Opts = FormContainerOptions<QueryResult, MutationResult, FormInput>; | |
const serializeForm = jest.fn((o: any) => ({ foo: 'bar_s' })); | |
const deserializeForm = jest.fn((o: any) => ({ foo: 'bar_d' })); | |
const mutateError = jest.fn((data: any) => Promise.reject({ message: 'Some mutation error' })); | |
const mutateSuccess = jest.fn((data: any) => Promise.resolve(data)); | |
const options: Omit<Opts, 'queryContainer' | 'mutationContainer'> = { | |
formName: 'entity', | |
propName: 'entity', | |
backPath: '/entities', | |
validationSchema: ValidationSchema, | |
serializeForm, | |
deserializeForm, | |
}; | |
const detailsContainer = (loading: boolean) => withProps({ | |
entity: { | |
data: { foo: 'bar' }, | |
loading, | |
}, | |
}) as ComponentEnhancer<QueryResult, any>; | |
const mutationContainer = (error: boolean) => withProps({ | |
mutate: error ? mutateError : mutateSuccess, | |
}) as ComponentEnhancer<MutationResult, any>; | |
class Receiver extends React.PureComponent<InjectedFormProps<any>> { | |
render() { | |
return ( | |
<form onSubmit={this.props.handleSubmit} /> | |
); | |
} | |
} | |
const mountThings = (detailsLoading: boolean, mutationError: boolean, history?: History) => { | |
const wrapper = formContainer({ | |
...options, | |
queryContainer: detailsContainer(detailsLoading), | |
mutationContainer: mutationContainer(mutationError), | |
}); | |
const store = createStore(combineReducers({ form: formReducer })); | |
const Wrapped: React.ComponentType<any> = wrapper(Receiver); | |
const router = !!history ? | |
(<Router history={history}><Wrapped /></Router>) : | |
(<MemoryRouter><Wrapped /></MemoryRouter>); | |
return mountWithMuiContext(( | |
<Provider store={store}> | |
{router} | |
</Provider>), | |
); | |
}; | |
beforeEach(() => { | |
serializeForm.mockClear(); | |
deserializeForm.mockClear(); | |
mutateError.mockClear(); | |
mutateSuccess.mockClear(); | |
}); | |
it('should render loading when query is loading', () => { | |
const wrapped = mountThings(true, false); | |
expect(wrapped.containsMatchingElement(<Loading />)).toBe(true); | |
}); | |
it('should deserialize query to form initialValues', () => { | |
const wrapped = mountThings(false, false); | |
const receivers = wrapped.find(Receiver); | |
expect(receivers.length).toBe(1); | |
const receiver = receivers.at(0); | |
expect(receiver.prop('initialValues')).toEqual({ foo: 'bar_d' }); | |
}); | |
it('should use validation schema', () => { | |
mountThings(false, false); | |
expect(validateInput).toBeCalledWith(ValidationSchema); | |
}); | |
it('should send serialized values', () => { | |
const wrapped = mountThings(false, false); | |
const receiver = wrapped.find(Receiver).at(0) as any; | |
receiver.find('form').simulate('submit'); | |
expect(serializeForm).toBeCalledWith({ foo: 'bar_d' }); | |
}); | |
it('should call mutate on submit', () => { | |
const wrapped = mountThings(false, false); | |
const receiver = wrapped.find(Receiver).at(0) as any; | |
receiver.find('form').simulate('submit'); | |
expect(mutateSuccess).toBeCalled(); | |
}); | |
it('should render loading while submitting', () => { | |
const wrapped = mountThings(false, false); | |
const receiver = wrapped.find(Receiver).at(0) as any; | |
receiver.find('form').simulate('submit'); | |
expect(wrapped.containsMatchingElement(<Loading />)).toBe(true); | |
}); | |
it('should navigate on successful mutation', async () => { | |
const history = createMemoryHistory(); | |
history.replace = jest.fn(); | |
const wrapped = mountThings(false, false, history); | |
const receiver = wrapped.find(Receiver).at(0) as any; | |
await receiver.find('form').simulate('submit'); | |
expect(history.replace).toBeCalledWith('/entities'); | |
}); | |
it('should pass form error on mutation error', async () => { | |
const wrapped = mountThings(false, true); | |
const receiver = wrapped.find(Receiver).at(0); | |
await receiver.find('form').simulate('submit'); | |
await flushPromises(); | |
const receiver2 = wrapped.find(Receiver).at(0); | |
expect(receiver2.prop('error')).toBe('Some mutation error'); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Schema } from 'joi'; | |
import { ApolloError, ChildProps } from 'react-apollo'; | |
import { RouteComponentProps, withRouter } from 'react-router-dom'; | |
import { ComponentEnhancer, compose, mapProps } from 'recompose'; | |
import { ConfigProps, InjectedFormProps, reduxForm, SubmissionError } from 'redux-form'; | |
import { withLoading } from '../withLoading'; | |
import { validateInput } from './validateInput'; | |
export interface FormContainerOptions<QueryResult, MutationResult, FormInput> { | |
/** | |
* Graphql query enhancer | |
*/ | |
queryContainer: ComponentEnhancer<QueryResult, any>; | |
/** | |
* Graphql mutation enhancer | |
*/ | |
mutationContainer: ComponentEnhancer<MutationResult, any>; | |
/** | |
* redux-form form name | |
*/ | |
formName: string; | |
/** | |
* Name of the prop where queryContainer puts the result | |
*/ | |
propName: string; | |
/** | |
* react-router will navigate to this path on successful form submission | |
*/ | |
backPath: string; | |
/** | |
* Convert graphql query result into something that can be feed to form (i.e. markdown -> draft.js) | |
*/ | |
deserializeForm: (data: any) => FormInput; | |
/** | |
* Convert form data into graphql mutation input (i.e. draft.js -> markdown string) | |
*/ | |
serializeForm: (input: FormInput) => any; | |
/** | |
* redux-form validation schema, works with form data (i.e. draft.js EditorState) | |
*/ | |
validationSchema: Schema; | |
} | |
export const formContainer = <QueryResult, MutationResult, FormInput>( | |
options: FormContainerOptions<QueryResult, MutationResult, FormInput>, | |
) => { | |
const { | |
queryContainer, | |
mutationContainer, | |
propName, | |
backPath, | |
deserializeForm, | |
serializeForm, | |
validationSchema, | |
formName, | |
} = options; | |
type FormProps = Partial<ConfigProps<FormInput>>; | |
type MappedProps = ChildProps<QueryResult, MutationResult> & RouteComponentProps<any>; | |
return compose( | |
queryContainer, | |
mutationContainer, | |
withRouter, | |
mapProps<FormProps, MappedProps>(({ [propName]: details, history, mutate }) => ({ | |
[propName]: details, | |
initialValues: deserializeForm(details.data!), | |
onSubmit: (input: FormInput) => { | |
// Make it clear that we return promise | |
return mutate!({ variables: { [propName]: serializeForm(input) } }) | |
.then(() => history.replace(backPath)) | |
.catch((e: ApolloError) => { | |
throw new SubmissionError({ _error: e.message }); | |
}); | |
}, | |
})), | |
withLoading<QueryResult>(({ [propName]: details }) => details.loading), | |
reduxForm({ | |
form: formName, | |
validate: validateInput(validationSchema), | |
}), | |
withLoading<InjectedFormProps>(props => props.submitting), | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { graphql } from 'react-apollo'; | |
import { RouteComponentProps, withRouter } from 'react-router-dom'; | |
import { compose, lifecycle, mapProps, withState } from 'recompose'; | |
import { formContainer } from '../../../components/forms'; | |
import { withRegion } from '../../../ww-clients/features/regions'; | |
import { RegionFormSchema } from '../../../ww-commons'; | |
import deserializeForm from './deserializeForm'; | |
import serializeForm from './serializeForm'; | |
import UPSERT_REGION from './upsertRegion.mutation'; | |
const regionForm = formContainer({ | |
formName: 'region', | |
propName: 'region', | |
backPath: '/regions', | |
queryContainer: withRegion({ errorOnMissingId: false }), | |
mutationContainer: graphql(UPSERT_REGION, { alias: 'withUpsertRegion' }), | |
serializeForm, | |
deserializeForm, | |
validationSchema: RegionFormSchema, | |
}); | |
export default regionForm; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment