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 1 commit
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
Next Next commit
[RFR] Migrate ra-core form code to TypeScript
  • Loading branch information
djhi committed Feb 13, 2019
commit 87a568160029348f17b4c8e7dc4cf38e30908ce2
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,22 @@
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 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 +56,7 @@ import warning from '../util/warning';
* </Edit>
* );
*/
export const FormDataConsumerView = ({
export const FormDataConsumerView: SFC<Props> = ({
children,
form,
formData,
Expand All @@ -60,7 +72,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 +112,25 @@ export const FormDataConsumerView = ({
return ret === undefined ? null : ret;
};

FormDataConsumerView.propTypes = {
children: PropTypes.func.isRequired,
data: PropTypes.object,
};
interface ConnectedProps {
children: (params: ChildrenFunctionParams) => ReactNode;
form: string;
record?: any;
source: string;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's missing an [key: string]: any;, since the various Form components inject more props to their children (like basePath), and these props are passed transparently to the consumer children


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 @@ -23,7 +48,10 @@ const getMessage = (message, messageArgs, value, values, props) =>
// 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 = (fn: any) =>
lodashMemoize(fn, (...args) => JSON.stringify(args));

type Required = (message?: string | MessageFunc) => Validator;
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

/**
* Required validator
Expand All @@ -37,16 +65,19 @@ const memoize = fn => lodashMemoize(fn, (...args) => JSON.stringify(args));
* const titleValidators = [required('The title is required')];
* <TextInput name="title" validate={titleValidators} />
*/
export const required = memoize((message = 'ra.validation.required') =>
Object.assign(
(value, values, props) =>
isEmpty(value)
? getMessage(message, undefined, value, values, props)
: undefined,
{ isRequired: true }
)
export const required: Required = memoize(
(message = 'ra.validation.required') =>
Object.assign(
(value, values, props) =>
isEmpty(value)
? getMessage(message, undefined, value, values, props)
: undefined,
{ isRequired: true }
)
);

type MinLength = (min: number, message?: string | MessageFunc) => Validator;

/**
* Minimum length validator
*
Expand All @@ -60,13 +91,15 @@ export const required = memoize((message = 'ra.validation.required') =>
* const passwordValidators = [minLength(10, 'Should be at least 10 characters')];
* <TextInput type="password" name="password" validate={passwordValidators} />
*/
export const minLength = memoize(
export const minLength: MinLength = memoize(
(min, message = 'ra.validation.minLength') => (value, values, props) =>
!isEmpty(value) && value.length < min
? getMessage(message, { min }, value, values, props)
: undefined
);

type MaxLength = (max: number, message?: string | MessageFunc) => Validator;

/**
* Maximum length validator
*
Expand All @@ -80,13 +113,15 @@ export const minLength = memoize(
* const nameValidators = [maxLength(10, 'Should be at most 10 characters')];
* <TextInput name="name" validate={nameValidators} />
*/
export const maxLength = memoize(
export const maxLength: MaxLength = memoize(
(max, message = 'ra.validation.maxLength') => (value, values, props) =>
!isEmpty(value) && value.length > max
? getMessage(message, { max }, value, values, props)
: undefined
);

type MinValue = (min: number, message?: string | MessageFunc) => Validator;

/**
* Minimum validator
*
Expand All @@ -100,13 +135,15 @@ export const maxLength = memoize(
* const fooValidators = [minValue(5, 'Should be more than 5')];
* <NumberInput name="foo" validate={fooValidators} />
*/
export const minValue = memoize(
export const minValue: MinValue = memoize(
(min, message = 'ra.validation.minValue') => (value, values, props) =>
!isEmpty(value) && value < min
? getMessage(message, { min }, value, values, props)
: undefined
);

type MaxValue = (max: number, message?: string | MessageFunc) => Validator;

/**
* Maximum validator
*
Expand All @@ -120,13 +157,15 @@ export const minValue = memoize(
* const fooValidators = [maxValue(10, 'Should be less than 10')];
* <NumberInput name="foo" validate={fooValidators} />
*/
export const maxValue = memoize(
export const maxValue: MaxValue = memoize(
(max, message = 'ra.validation.maxValue') => (value, values, props) =>
!isEmpty(value) && value > max
? getMessage(message, { max }, value, values, props)
: undefined
);

type NumberValidator = (message?: string | MessageFunc) => Validator;

/**
* Number validator
*
Expand All @@ -140,17 +179,22 @@ export const maxValue = memoize(
* <TextInput name="age" validate={ageValidators} />
*/
// tslint:disable-next-line:variable-name
export const number = memoize(
export const number: NumberValidator = memoize(
(message = 'ra.validation.number') => (value, values, props) =>
!isEmpty(value) && isNaN(Number(value))
? getMessage(message, undefined, value, values, props)
: undefined
);

type RegedValidator = (
pattern: RegExp,
message?: string | MessageFunc
) => Validator;

/**
* 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 All @@ -160,7 +204,7 @@ export const number = memoize(
* const zipValidators = [regex(/^\d{5}(?:[-\s]\d{4})?$/, 'Must be a zip code')];
* <TextInput name="zip" validate={zipValidators} />
*/
export const regex = lodashMemoize(
export const regex: RegedValidator = lodashMemoize(
(pattern, message = 'ra.validation.regex') => (value, values, props) =>
!isEmpty(value) && typeof value === 'string' && !pattern.test(value)
? getMessage(message, { pattern }, value, values, props)
Expand All @@ -170,6 +214,8 @@ export const regex = lodashMemoize(
}
);

type EmailValidator = (message?: string | MessageFunc) => Validator;

/**
* Email validator
*
Expand All @@ -182,15 +228,19 @@ export const regex = lodashMemoize(
* const emailValidators = [email('Must be an email')];
* <TextInput name="email" validate={emailValidators} />
*/
export const email = memoize((message = 'ra.validation.email') =>
regex(EMAIL_REGEX, message)
export const email: EmailValidator = 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(', '),
});
};

type ChoicesValidator = (
list: any[],
message?: string | MessageFunc
) => Validator;

/**
* Choices validator
Expand All @@ -205,7 +255,7 @@ const oneOfTypeMessage = ({ list }, value, values, { translate }) => {
* const genderValidators = [choices(['male', 'female'], 'Must be either Male or Female')];
* <TextInput name="gender" validate={genderValidators} />
*/
export const choices = memoize(
export const choices: ChoicesValidator = memoize(
(list, message = oneOfTypeMessage) => (value, values, props) =>
!isEmpty(value) && list.indexOf(value) === -1
? getMessage(message, { list }, value, values, props)
Expand Down
Loading