Skip to content

Instantly share code, notes, and snippets.

@doomsower
Created September 24, 2017 14:25
Show Gist options
  • Save doomsower/ec88df86c8608bd72795488e91071b88 to your computer and use it in GitHub Desktop.
Save doomsower/ec88df86c8608bd72795488e91071b88 to your computer and use it in GitHub Desktop.
Using react apollo with redux-form
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');
});
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),
);
};
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