0
\$\begingroup\$

I have a number of these components, but will show just two as an example. They're very similar, but the differences are enough to where splitting them out felt needed. What I really need is some help. Is this the right approach, or would I be better served somehow combining them to reduce duplicate code? I have similar components for toggles like radios and checkboxes, as well as textarea that handle both plain text and markdown fields, etc.

Other feedback would also be appreciated :)

Textfield wrapper.

'use strict'

import React, { Component } from 'react'
import { FieldText, FieldLabel, FieldDescription, FieldErrorMessage } from './'
import PropTypes from 'prop-types'

class FieldTextWrap extends Component {
  static propTypes = {}

  constructor(props) {
    super(props)
    this.state = {
      value: '',
      isValid: true
    }
  }

  _handleChange = value => {
    const { formatter } = this.props
    const formattedValue = !!formatter ? formatter(value) : value
    this.setState({ value: formattedValue })
  }

  componentDidMount() {
    const {
      required,
      type,
      value,
      initialValue,
      dispatch,
      handleBlur,
      isForm,
      formId,
      id
    } = this.props

    !!initialValue && this.setState({ value: initialValue })
    !initialValue && !!value && this.setState({ value })

    const fieldData = {
      id,
      value,
      type,
      required,
      formId,
      valid: true
    }

    return isForm && dispatch(handleBlur(null, dispatch, fieldData))
  }

  componentDidUpdate(prevProps, prevState) {
    const { validationRule, required } = this.props
    const { value } = this.state
    const didUpdate = prevState.value !== value

    // validate and update
    const isValid = !!validationRule
      ? validationRule.func({
          field: { required, value },
          tests: validationRule.tests,
          options: validationRule.options
        })
      : true
    didUpdate && this.setState({ isValid })
  }

  componentWillUnmount() {
    const { removeFormField, id, isForm } = this.props

    isForm && removeFormField(id)
  }

  render() {
    const {
      className = '',
      description,
      fieldGroup = '',
      formName,
      formId,
      hideDescription,
      hideLabel,
      id,
      label,
      required,
      requireDefault,
      type,
      validationText: message = ''
    } = this.props

    const classes = `form-item ${formName}__${id} ${fieldGroup} ${type}-field ${className} ${type} text-type`
    const value = this.state.value
    const isValid = this.state.isValid

    const fieldData = {
      id,
      value,
      type,
      required,
      formId,
      valid: isValid,
      message
    }

    return (
      <div className={classes}>
        <FieldLabel
          name={id}
          label={label}
          required={required}
          requireDefault={requireDefault}
          hidden={hideLabel}
        />

        <div className={`${type}-field__wrapper`}>
          {isValid ? (
            <FieldDescription
              name={id}
              description={description}
              hidden={hideDescription}
            />
          ) : (
            <FieldErrorMessage name={id} message={message} />
          )}

          <FieldText
            {...this.props}
            value={value}
            handleChange={this._handleChange}
            fieldData={fieldData}
          />

          {type === 'date' && <div className="date-field__icon" />}
        </div>
      </div>
    )
  }
}

export default FieldTextWrap

Select Wrapper

'use strict'

import React, { Component } from 'react'
import {
  FieldSelect,
  FieldLabel,
  FieldDescription,
  FieldErrorMessage
} from './'
import PropTypes from 'prop-types'

class FieldSelectWrap extends Component {
  static propTypes = {}

  constructor(props) {
    super(props)
    this.state = {
      value: '',
      isValid: true
    }
  }

  _handleChange = value => this.setState({ value })

  componentDidMount() {
    const {
      required,
      type,
      value,
      initialValue,
      dispatch,
      handleBlur,
      isForm,
      formId,
      id
    } = this.props

    !!initialValue && this.setState({ value: initialValue })
    !initialValue && !!value && this.setState({ value })

    const fieldData = {
      id,
      value,
      type,
      required,
      formId,
      valid: true
    }

    return isForm && dispatch(handleBlur(null, dispatch, fieldData))
  }

  componentDidUpdate(prevProps, prevState) {
    const { validationRule, required } = this.props
    const { value } = this.state
    const didUpdate = prevState.value !== value

    // validate and update
    const isValid = !!validationRule
      ? validationRule.func({
          field: { required, value },
          tests: validationRule.tests,
          options: validationRule.options
        })
      : true
    didUpdate && this.setState({ isValid })
  }

  componentWillUnmount() {
    const { removeFormField, id, isForm } = this.props

    isForm && removeFormField(id)
  }

  render() {
    const {
      className = '',
      description,
      fieldGroup = '',
      formName,
      formId,
      hideDescription,
      hideLabel,
      id,
      label,
      required,
      requireDefault,
      type,
      validationText: message = ''
    } = this.props

    const classes = `form-item ${formName}__${id} ${fieldGroup} ${type}-field ${className} ${type}`
    const value = this.state.value
    const isValid = this.state.isValid

    const fieldData = {
      id,
      value,
      type,
      required,
      formId,
      valid: isValid,
      message
    }

    return (
      <div className={classes}>
        <FieldLabel
          name={id}
          label={label}
          required={required}
          requireDefault={requireDefault}
          hidden={hideLabel}
        />

        <div className={`${type}-field__wrapper`}>
          <FieldDescription
            name={id}
            description={description}
            hidden={hideDescription}
          />

          {!isValid && <FieldErrorMessage name={id} message={message} />}

          <FieldSelect
            {...this.props}
            value={value}
            handleChange={this._handleChange}
            fieldData={fieldData}
          />

          {type === 'date' && <div className="date-field__icon" />}
        </div>
      </div>
    )
  }
}

export default FieldSelectWrap
\$\endgroup\$
2
  • \$\begingroup\$ How would they be used? Is their usage similar as well? \$\endgroup\$
    – Mast
    Commented Aug 13, 2018 at 15:33
  • \$\begingroup\$ I'm using these to build forms. The first is the primary component called when including text fields, and the second when including select fields. So, their usage is similar, being children of my form module, but different in that they're used to manage different types of fields. Hopefully, that makes sense. \$\endgroup\$ Commented Aug 13, 2018 at 15:48

1 Answer 1

2
\$\begingroup\$

Combining them to reduce duplicate code is not a good approach. Prefer small composable components over large Swiss army knife components. Think single responsibility principle.

If your component takes a lot of properties, it may be a good candidate to make smaller. If you are testing for type, you may want to create multiple controls.

{type === 'date' && <div className="date-field__icon" />}

In your example, I recommend generating a generic wrapper component with a nested child.

Create wrapper component:

class LabelWrapper extends Component {
  ...
  render() {
   const {text, icon, error, children} = this.props;
   const ErrorMessage = this.getError(error);
   const Icon = this.getIcon(icon);
   return (
     <div className="label-wrapper">
       <label>{text}</label>
       <ErrorMessage/>
       {children}
       <Icon>
     </div>
    );
  }
  getIcon(icon) {
    return icon && <div className={icon} />}
  }
}

Use Wrapper in form passing in child component:

<LabelWrapper
  text='Full Name'
  error={error.fullName}>
  <TextInput
    id='fullName'
    value={form.firstName}
    handleChange={handleChange.firstName}/>
</LabelWrapper>
<LabelWrapper
  text='Choose Date'
  icon='date-field__icon'
  error={error.date}>
  <FieldSelect
    value={form.date}
    handleChange={handleChange.chooseDate}
    fieldData={fieldData} />
</LabelWrapper>

You can also compose a more specific control. This will prevent your form from getting too large.

class ChooseDate extends Component {
  ...
  render() {
   const {fieldData, handleChange, value, error} = this.props;
   return (
     <LabelWrapper
      text='Choose Date'
       icon='date-field__icon"'
       error={error}
       <FieldSelect
         value={value}
         handleChange={handleChange}
         fieldData={fieldData}/>
      </LabelWrapper>
    );
  }
}

A few other notes:

Resources:

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.