Skip to content

Commit

Permalink
refactor(core/presentation): use render props everywhere (spinnaker#7316
Browse files Browse the repository at this point in the history
)


In FormField and FormikFormField, always use render prop to render
the input and layout.  Do not pass class references
  • Loading branch information
christopherthielen authored Aug 13, 2019
1 parent cef1c09 commit 40c3197
Show file tree
Hide file tree
Showing 15 changed files with 72 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class HealthCheck extends React.Component<IHealthCheckProps> {
{this.requiresHealthCheckPath() && (
<FormikFormField
name="healthCheckPath"
input={TextInput}
input={props => <TextInput {...props} />}
required={true}
onChange={this.healthCheckPathChanged}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export interface IServicesProps {
fieldName: string;
}

export const Services: React.SFC<IServicesProps> = (props: IServicesProps) => {
const { fieldName } = props;
export const Services: React.SFC<IServicesProps> = ({ fieldName }: IServicesProps) => {
return (
<div>
<div className="form-group">
Expand All @@ -31,7 +30,7 @@ export const Services: React.SFC<IServicesProps> = (props: IServicesProps) => {
<div className="sp-margin-m-bottom">
<FormikFormField
name={`${fieldName}[${index}]`}
input={p => <TextInput {...p} />}
input={props => <TextInput {...props} />}
required={true}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class ApplyEntityTagsStageConfig extends React.Component<IApplyEntityTags
<>
<StageConfigField label="Name">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={entityRef.entityId || ''}
onChange={(e: any) => this.entityRefFieldChanged('entityId', e.target.value)}
/>
Expand All @@ -91,22 +91,22 @@ export class ApplyEntityTagsStageConfig extends React.Component<IApplyEntityTags
</StageConfigField>
<StageConfigField label="Account">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={entityRef.account || ''}
onChange={(e: any) => this.entityRefFieldChanged('account', e.target.value)}
/>
</StageConfigField>
<StageConfigField label="Region" helpKey="pipeline.config.entitytags.region">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={entityRef.region || ''}
onChange={(e: any) => this.entityRefFieldChanged('region', e.target.value)}
/>
</StageConfigField>
{entityRef.entityType === 'securitygroup' && (
<StageConfigField label="VPC Id">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={entityRef.vpcId || ''}
onChange={(e: any) => this.entityRefFieldChanged('vpcId', e.target.value)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class TagEditor extends React.Component<ITagEditorProps, ITagEditorState>
</label>
<div className="col-md-8">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={tag.namespace || ''}
onChange={(event: any) => this.tagValueChanged('namespace', event.target.value)}
/>
Expand All @@ -101,7 +101,7 @@ export class TagEditor extends React.Component<ITagEditorProps, ITagEditorState>
<label className="col-md-3 sm-label-right">Name</label>
<div className="col-md-8">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={tag.name || ''}
onChange={(event: any) => this.tagValueChanged('name', event.target.value)}
/>
Expand All @@ -117,7 +117,7 @@ export class TagEditor extends React.Component<ITagEditorProps, ITagEditorState>
</label>
<div className="col-md-8">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={value || ''}
onChange={(event: any) => this.tagValueChanged('value', event.target.value)}
/>
Expand All @@ -128,7 +128,7 @@ export class TagEditor extends React.Component<ITagEditorProps, ITagEditorState>
<label className="col-md-3 sm-label-right">Message</label>
<div className="col-md-8">
<FormField
input={TextInput}
input={props => <TextInput {...props} />}
value={tag.value.message || ''}
onChange={(event: any) => this.tagValueChanged('value.message', event.target.value)}
/>
Expand All @@ -142,8 +142,8 @@ export class TagEditor extends React.Component<ITagEditorProps, ITagEditorState>
<label className="col-md-3 sm-label-right">Type</label>
<div className="col-md-8">
<FormField
input={inputProps => (
<ReactSelectInput {...inputProps} options={typeOptions} clearable={false} className="full-width" />
input={props => (
<ReactSelectInput {...props} options={typeOptions} clearable={false} className="full-width" />
)}
required={true}
onChange={(e: any) => this.handleTypeChanged(e.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function TriggerForm(triggerFormProps: ITriggerProps & { formik: FormikProps<ITr
name="runAsUser"
label="Run As User"
help={<HelpField id="pipeline.config.trigger.runAsUser" />}
input={RunAsUserInput}
input={props => <RunAsUserInput {...props} />}
/>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class Parameters extends React.Component<IParametersProps> {
<FormikFormField
name={'parameters.' + parameter.name}
fastField={false}
input={(props: any) => <TextInput {...props} inputClassName={'form-control input-sm'} />}
input={props => <TextInput {...props} inputClassName={'form-control input-sm'} />}
required={parameter.required}
/>
</div>
Expand All @@ -82,7 +82,7 @@ export class Parameters extends React.Component<IParametersProps> {
<FormikFormField
name={'parameters.' + parameter.name}
fastField={false}
input={(props: any) => (
input={props => (
<ReactSelectInput
{...props}
clearable={false}
Expand Down
13 changes: 9 additions & 4 deletions app/scripts/modules/core/src/presentation/forms/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IPromise } from 'angular';
import { $q } from 'ngimport';

import { noop } from 'core/utils';

import { LayoutContext } from './layouts';
import { useLatestPromise } from '../hooks';
import { createFieldValidator } from './FormikFormField';
Expand All @@ -12,6 +13,7 @@ import { IValidator, IValidatorResultRaw } from './validation';
import {
ICommonFormFieldProps,
IControlledInputProps,
IFieldLayoutProps,
IFieldLayoutPropsWithoutInput,
IValidationProps,
} from './interface';
Expand Down Expand Up @@ -47,8 +49,6 @@ export function FormField(props: IFormFieldProps) {
const addValidator = useCallback((v: IValidator) => setInternalValidators(list => list.concat(v)), []);
const removeValidator = useCallback((v: IValidator) => setInternalValidators(list => list.filter(x => x !== v)), []);

const fieldLayoutFromContext = useContext(LayoutContext);

const validate = useMemo(() => props.validate, []);
const fieldValidator = useMemo(
() => createFieldValidator(label, required, [].concat(validate || noop).concat(internalValidators)),
Expand All @@ -68,6 +68,11 @@ export function FormField(props: IFormFieldProps) {

const touched = firstDefined(touchedProp, hasBlurred);

const FieldLayoutFromContext = useContext(LayoutContext);
const inputRenderPropOrNode = firstDefined(input, noop);
const layoutFromContext = (layoutProps: IFieldLayoutProps) => <FieldLayoutFromContext {...layoutProps} />;
const layoutRenderPropOrNode = firstDefined(layout, layoutFromContext);

const validationProps: IValidationProps = {
touched,
validationMessage,
Expand All @@ -87,12 +92,12 @@ export function FormField(props: IFormFieldProps) {
};

// Render the input
const inputElement = renderContent(input, { ...controlledInputProps, validation: validationProps });
const inputElement = renderContent(inputRenderPropOrNode, { ...controlledInputProps, validation: validationProps });

// Render the layout passing the rendered input in
return (
<>
{renderContent(layout || fieldLayoutFromContext, {
{renderContent(layoutRenderPropOrNode, {
...fieldLayoutPropsWithoutInput,
...validationProps,
input: inputElement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import * as React from 'react';
import { isNil, isString } from 'lodash';
import { Field, FastField, FieldProps, getIn, FormikContext, FormikConsumer } from 'formik';

import { ICommonFormFieldProps, IFieldLayoutPropsWithoutInput, IValidationProps } from './interface';
import { noop } from 'core/utils';

import { ICommonFormFieldProps, IFieldLayoutProps, IFieldLayoutPropsWithoutInput, IValidationProps } from './interface';
import { WatchValue } from '../WatchValue';
import { LayoutContext } from './layouts/index';
import { composeValidators, IValidator, Validators } from './validation';
Expand Down Expand Up @@ -55,11 +57,10 @@ function FormikFormFieldImpl<T = any>(props: IFormikFormFieldImplProps<T>) {
const fastField = firstDefined(fastFieldProp, true);

const fieldLayoutPropsWithoutInput: IFieldLayoutPropsWithoutInput = { label, help, required, actions };
const fieldLayoutFromContext = useContext(LayoutContext);

const [internalValidators, setInternalValidators] = useState([]);
const addValidator = useCallback((v: IValidator) => setInternalValidators(list => list.concat(v)), []);
const removeValidator = useCallback((v: IValidator) => setInternalValidators(list => list.filter(x => x !== v)), []);
const FieldLayoutFromContext = useContext(LayoutContext);

const renderField = ({ field }: FieldProps<any>) => {
const validationProps: IValidationProps = {
Expand All @@ -70,11 +71,14 @@ function FormikFormFieldImpl<T = any>(props: IFormikFormFieldImplProps<T>) {
removeValidator,
};

const inputElement = renderContent(input, { ...field, validation: validationProps });
const inputRenderPropOrNode = firstDefined(input, noop);
const layoutFromContext = (layoutProps: IFieldLayoutProps) => <FieldLayoutFromContext {...layoutProps} />;
const layoutRenderPropOrNode = firstDefined(layout, layoutFromContext);
const inputElement = renderContent(inputRenderPropOrNode, { ...field, validation: validationProps });

return (
<WatchValue onChange={onChange} value={field.value}>
{renderContent(layout || fieldLayoutFromContext, {
{renderContent(layoutRenderPropOrNode, {
...fieldLayoutPropsWithoutInput,
...validationProps,
input: inputElement,
Expand Down
33 changes: 16 additions & 17 deletions app/scripts/modules/core/src/presentation/forms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ It has a submit button which is disabled when the form is invalid or being submi
<FormikFormField
name="name"
label="Your Name"
input={TextInput}
input={props => <TextInput {...props} />}
validate={value => (!value ? 'Please enter your name' : undefined)}
/>

<FormikFormField
name="email"
label="Your Email"
input={TextInput}
input={props => <TextInput {...props} />}
validate={value => {
if (!value) return 'Please enter your email';
if (!/[^@]+@[^@]+/.exec(value)) return 'Please enter a valid email';
Expand All @@ -47,9 +47,9 @@ It has a submit button which is disabled when the form is invalid or being submi
The `FormikFormField` component should be a descendent of a `Formik` component.
It accepts "all the props", organizes them, and passes them down to the Input and Layout in the correct spots.

- `input`: the Input component to use (see Input section below for details)
- `layout`: the (optional) Layout component to use. (see Layout section for details. `StandardFieldLayout` is used by default)
- `validate`: the (optional) formik [field validation function](https://jaredpalmer.com/formik/docs/api/field#validate) which receives the value and should return an error message, or a promise (for async)
- `input`: a render prop used to render an Input component (see Input section below for details)
- `layout`: an (optional) render propu sed to render a Layout component (see Layout section for details. `StandardFieldLayout` is used by default)
- `validate`: an (optional) formik [field validation function](https://jaredpalmer.com/formik/docs/api/field#validate) which receives the value and should return an error message
- `name`: the path to the field's value in the formik `values`
- `label`, `help`, `required`, `actions` (see `IFieldLayoutPropsWithoutInput`)
- `touched`, `validationMessage`, `validationStatus` (see: `IValidationProps`)
Expand All @@ -61,25 +61,24 @@ In addition to the `validate` prop, the field can also be validated at the form
This example shows validation of a formik field using the `Formik` component's `validate` prop.

```js
import { buildValidators } from 'core/presentation';

<Formik
initialValues={{ email: '' }}
validate={values => {
const errors = {};
if (!value) {
errors.email = 'Please enter your email';
} else if (!/[^@]+@[^@]+/.exec(value)) {
errors.email = 'Please enter a valid email';
}
return errors;
const emailRegexp = /[^@]+@[^@]+/;
const validator = buildValidators(values);
validator.field('email').required([value => (emailRegexp.exec(value) ? null : 'Please enter a valid email')]);
return validator.result();
}}
render={formik => {
return (
<Form>
<FormikFormField name="email" label="Your Email" input={TextInput} />
<FormikFormField name="email" label="Your Email" input={props => <TextInput {...props} />} />
</Form>
);
}}
/>
/>;
```

# FormField
Expand All @@ -104,15 +103,15 @@ It has a submit button which is disabled when the form is being submitted.
<FormField
name="name"
label="your name"
input={TextInput}
input={props => <TextInput {...props} />}
value={this.state.name}
onChange={evt => this.setState({ name: evt.target.value })}
validate={val => !val && <span>Please enter your name</span>}
/>
<FormField
name="email"
label="email address"
input={TextInput}
input={props => <TextInput {...props} />}
value={this.state.email}
onChange={evt => this.setState({ email: evt.target.value })}
validate={val => val && val.indexOf('@') === -1 && <span>Please enter a valid email</span>}
Expand Down Expand Up @@ -158,7 +157,7 @@ This example uses a custom layout to render the error above the input
<FormField
name="email"
label="email address"
input={TextInput}
input={props => <TextInput {...props} />}
layout={({ error, input }) => (
<div>
<div style={{ background: 'red' }}>{error}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ValidationMessage } from 'core/validation';

import { FormikFormField, IFormikFieldProps } from '../FormikFormField';
import { ExpressionError, ExpressionInput, ExpressionPreview, ISpelError } from '../inputs';
import { ICommonFormFieldProps, IFieldLayoutPropsWithoutInput, IFormInputProps } from '../interface';
import { ICommonFormFieldProps, IFieldLayoutPropsWithoutInput } from '../interface';

export interface IExpressionFieldProps {
placeholder?: string;
Expand Down Expand Up @@ -39,7 +39,7 @@ export class FormikExpressionField extends React.Component<IFormikExpressionFiel
return (
<FormikFormField
name={name}
input={(props: IFormInputProps) => (
input={props => (
<ExpressionInput onExpressionChange={changes => this.setState(changes)} context={context} {...props} />
)}
label={label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export class FormikExpressionRegexField extends React.Component<
spelError: null,
};

private renderRegexFields(props: IFormikExpressionRegexFieldProps, defaultExpanded: boolean) {
const { RegexHelp, regexName, replaceName } = props;
private renderRegexFields(fieldProps: IFormikExpressionRegexFieldProps, defaultExpanded: boolean) {
const { RegexHelp, regexName, replaceName } = fieldProps;

const sectionHeading = ({ chevron }: { chevron: JSX.Element }) => (
<span>
Expand Down Expand Up @@ -71,12 +71,16 @@ export class FormikExpressionRegexField extends React.Component<
<FormikFormField
name={regexName}
validate={validateRegexString}
layout={RegexLayout}
input={TextInput}
layout={props => <RegexLayout {...props} />}
input={props => <TextInput {...props} />}
touched={true}
/>
<code>/</code>
<FormikFormField name={replaceName} layout={RegexLayout} input={TextInput} />
<FormikFormField
name={replaceName}
layout={props => <RegexLayout {...props} />}
input={props => <TextInput {...props} />}
/>
<code>/g</code>
</div>
</CollapsibleSection>
Expand Down
Loading

0 comments on commit 40c3197

Please sign in to comment.