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

refactor(core/presentation): use render props everywhere #7316

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 @@ -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 = /[^@]+@[^@]+/;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored this to use buildValidators

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