Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Migrate ra-core form code to TypeScript #2878

Merged
merged 3 commits into from
Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ describe('FormDataConsumerView', () => {
const formData = { id: 123, title: 'A title' };

shallow(
<FormDataConsumerView formData={formData}>
<FormDataConsumerView
form="a-form"
formData={formData}
source="a-field"
>
{children}
</FormDataConsumerView>
);
Expand All @@ -27,6 +31,7 @@ describe('FormDataConsumerView', () => {

shallow(
<FormDataConsumerView
form="a-form"
source="authors[0]"
index={0}
formData={formData}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import React from 'react';
import React, { ReactNode, SFC } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getFormValues, FormName } from 'redux-form';
import get from 'lodash/get';

import warning from '../util/warning';
import { ReduxState } from '../types';

interface ChildrenFunctionParams {
formData: any;
scopedFormData?: any;
getSource?: (source: string) => string;
}

interface ConnectedProps {
children: (params: ChildrenFunctionParams) => ReactNode;
form: string;
record?: any;
source: string;
[key: string]: any;
}

interface Props extends ConnectedProps {
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
formData: any;
index?: number;
}

/**
* Get the current (edited) value of the record from the form and pass it
Expand Down Expand Up @@ -44,7 +64,7 @@ import warning from '../util/warning';
* </Edit>
* );
*/
export const FormDataConsumerView = ({
export const FormDataConsumerView: SFC<Props> = ({
children,
form,
formData,
Expand All @@ -60,7 +80,7 @@ export const FormDataConsumerView = ({
// If we have an index, we are in an iterator like component (such as the SimpleFormIterator)
if (typeof index !== 'undefined') {
scopedFormData = get(formData, source);
getSource = scopedSource => {
getSource = (scopedSource: string) => {
getSourceHasBeenCalled = true;
return `${source}.${scopedSource}`;
};
Expand Down Expand Up @@ -100,20 +120,18 @@ export const FormDataConsumerView = ({
return ret === undefined ? null : ret;
};

FormDataConsumerView.propTypes = {
children: PropTypes.func.isRequired,
data: PropTypes.object,
};

const mapStateToProps = (state, { form, record }) => ({
const mapStateToProps = (
state: ReduxState,
{ form, record }: ConnectedProps
) => ({
formData: getFormValues(form)(state) || record,
});

const ConnectedFormDataConsumerView = connect(mapStateToProps)(
FormDataConsumerView
);

const FormDataConsumer = props => (
const FormDataConsumer = (props: ConnectedProps) => (
<FormName>
{({ form }) => <ConnectedFormDataConsumerView form={form} {...props} />}
</FormName>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import React from 'react';
import React, { ComponentType, SFC } from 'react';
import PropTypes from 'prop-types';
import { Field } from 'redux-form';
import withDefaultValue from './withDefaultValue';
import { Validator } from './validate';
import { InputProps } from './types';

export const isRequired = validate => {
if (validate && validate.isRequired) return true;
if (validate && validate.isRequired) {
return true;
}
if (Array.isArray(validate)) {
return !!validate.find(it => it.isRequired);
}
return false;
};

export const FormFieldView = ({ input, ...props }) =>
interface Props {
component: ComponentType<InputProps>;
defaultValue: any;
input?: any;
source: string;
validate: Validator | Validator[];
}

export const FormFieldView: SFC<Props> = ({ input, ...props }) =>
input ? ( // An ancestor is already decorated by Field
React.createElement(props.component, { input, ...props })
) : (
Expand All @@ -30,4 +42,5 @@ FormFieldView.propTypes = {
validate: PropTypes.oneOfType([PropTypes.func, PropTypes.array]),
};

export default withDefaultValue(FormFieldView);
const FormField = withDefaultValue(FormFieldView);
export default FormField;
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React from 'react';
import React, { ReactType } from 'react';
import FormField from './FormField';

export default (BaseComponent, fieldProps = {}) => {
export default (
BaseComponent: ReactType,
fieldProps: {
[key: string]: any;
} = {}
) => {
const WithFormField = props => (
<FormField component={BaseComponent} {...fieldProps} {...props} />
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LOCATION_CHANGE } from 'react-router-redux';
import { destroy } from 'redux-form';

import formMiddleware from './formMiddleware';
import { REDUX_FORM_NAME } from '../form/constants';
import { REDUX_FORM_NAME } from './constants';
import { resetForm } from '../actions/formActions';

describe('form middleware', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { destroy } from 'redux-form';
import isEqual from 'lodash/isEqual';

import { resetForm } from '../actions/formActions';
import { REDUX_FORM_NAME } from '../form/constants';
import { REDUX_FORM_NAME } from './constants';

/**
* This middleware ensure that whenever a location change happen, we get the
Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions packages/ra-core/src/form/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface InputProps {
defaultValue?: any;
input?: any;
source: string;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import lodashMemoize from 'lodash/memoize';
import { Translate } from '../types';

/* eslint-disable no-underscore-dangle */
/* @link http://stackoverflow.com/questions/46155/validate-email-address-in-javascript */
const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; // eslint-disable-line no-useless-escape

const isEmpty = value =>
const isEmpty = (value: any) =>
typeof value === 'undefined' || value === null || value === '';

const getMessage = (message, messageArgs, value, values, props) =>
export type Validator = (
value: any,
values: any,
props: any
) => string | null | undefined;

interface MessageFuncParams {
args: any;
value: any;
values: any;
translate: Translate;
[key: string]: any;
}

type MessageFunc = (params: MessageFuncParams) => string;

const getMessage = (
message: string | MessageFunc,
messageArgs: any,
value: any,
values: any,
props: {
translate: Translate;
}
) =>
typeof message === 'function'
? message({
args: messageArgs,
Expand All @@ -20,10 +45,16 @@ const getMessage = (message, messageArgs, value, values, props) =>
...messageArgs,
});

type Memoize = <T extends (...args: any[]) => any>(
func: T,
resolver?: (...args: any[]) => any
) => T;

// If we define validation functions directly in JSX, it will
// result in a new function at every render, and then trigger infinite re-render.
// Hence, we memoize every built-in validator to prevent a "Maximum call stack" error.
const memoize = fn => lodashMemoize(fn, (...args) => JSON.stringify(args));
const memoize: Memoize = (fn: any) =>
lodashMemoize(fn, (...args) => JSON.stringify(args));

/**
* Required validator
Expand Down Expand Up @@ -150,7 +181,7 @@ export const number = memoize(
/**
* Regular expression validator
*
* Returns an error if the value does not mactch the pattern given as parameter
* Returns an error if the value does not match the pattern given as parameter
*
* @param {RegExp} pattern
* @param {string|function} message
Expand Down Expand Up @@ -186,11 +217,10 @@ export const email = memoize((message = 'ra.validation.email') =>
regex(EMAIL_REGEX, message)
);

const oneOfTypeMessage = ({ list }, value, values, { translate }) => {
const oneOfTypeMessage: MessageFunc = ({ list, value, values, translate }) =>
translate('ra.validation.oneOf', {
options: list.join(', '),
});
};

/**
* Choices validator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'assert';
import { shallow } from 'enzyme';
import React from 'react';
import { DefaultValue } from './withDefaultValue';
import { DefaultValueView } from './withDefaultValue';

describe('withDefaultValue', () => {
describe('<DefaultValue />', () => {
Expand All @@ -10,7 +10,7 @@ describe('withDefaultValue', () => {
it('should not initialize the form if no default value', () => {
const initializeForm = jest.fn();
shallow(
<DefaultValue
<DefaultValueView
initializeForm={initializeForm}
decoratedComponent={BaseComponent}
source="title"
Expand All @@ -21,7 +21,7 @@ describe('withDefaultValue', () => {
it('should initialize the form with default value on mount', () => {
const initializeForm = jest.fn();
shallow(
<DefaultValue
<DefaultValueView
initializeForm={initializeForm}
decoratedComponent={BaseComponent}
source="title"
Expand All @@ -34,7 +34,7 @@ describe('withDefaultValue', () => {
it('should call initializeForm if a defaultValue changes', () => {
const initializeForm = jest.fn();
const wrapper = shallow(
<DefaultValue
<DefaultValueView
initializeForm={initializeForm}
decoratedComponent={BaseComponent}
source="bar"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { Component, createElement } from 'react';
import { Component, createElement, ComponentType } from 'react';
import PropTypes from 'prop-types';

import { connect } from 'react-redux';
import { initializeForm as initializeFormAction } from '../actions/formActions';
import { InputProps } from './types';

export class DefaultValue extends Component {
interface Props extends InputProps {
decoratedComponent: ComponentType<InputProps>;
initializeForm: typeof initializeFormAction;
}

export class DefaultValueView extends Component<Props> {
static propTypes = {
decoratedComponent: PropTypes.oneOfType([
PropTypes.element,
Expand Down Expand Up @@ -52,8 +58,10 @@ export class DefaultValue extends Component {
}
}

export default DecoratedComponent =>
const DefaultValue = (DecoratedComponent: ComponentType<InputProps>) =>
connect(
() => ({ decoratedComponent: DecoratedComponent }),
{ initializeForm: initializeFormAction }
)(DefaultValue);
)(DefaultValueView);

export default DefaultValue;