Semantic-ui-react: RFC: Form Validation

Created on 19 Aug 2016  路  24Comments  路  Source: Semantic-Org/Semantic-UI-React

This is a placeholder issue. Form validation will be implemented apart from the Form update PR #400. We've had a few offline discussions surrounding the validation API. Ideas will be posted here soon as we're able.

Have any input regarding form validation? We'd love to hear your ideas.

RFC enhancement help wanted

Most helpful comment

I made a bunch of formsy-react wrappers for the form elements:

formsy-semantic-ui-react

All 24 comments

Don't forget about i18n there.

Could you expound a little?

SUI form validation has default messages for shorthand rules.

image

There is must be ability for redefine these messages :hammer:

Gotcha, agreed.

Hi,

I am trying to write validation for form fields myself, and find this threads. It would be nice to have a simplified API outside Form as indicated on the top of this thread. But I think it would also be nice to provide some straight forward way/api to validate input fields manually. Maybe, like

<Button.Field label='email' name='email' validate={this.validateEmail} />

The validate method is called when the form is submit. When failed, the error message may be passed to its own state?

This was also my first inclination. Having the ability to validate a single field seems useful. I can see an equally valid argument that says, still validate the entire form model but only against a single field (e.g. do not specify validation for the other field names). What I'm less sure of is what the API looks like for wiring the model, field values, and error messages into the UI.

@davezuko did you begin work on validation the other day?

What I'm less sure of is what the API looks like for wiring the model, field values, and error messages into the UI.

generally, serializer may pick up these information from states of sub-components? just my 2ct.

Yep, generally it is clear. Specifics on the best way to do this is what I'm not clear about. Would love to see some experiments on this though. Goals being:

  • loose coupling of model - form - validator - error messages
  • no DOM dependency
  • minimal boilerplate, but not at the expense of explicit, declarative, and decoupled code

@levithomason - Not sure if this is beyond the scope of this issue, if so lmk and I can possibly open a new issue.

I was having some discussion with my team about building a form component on top of stardust forms, but given some of the things mentioned here I'm wondering how much we're willing to build directly into stardust. We're planning to do the work anyway, so would be cool to spread the love 馃槃

Just going to copy/paste a comment I wrote in our project, would love your thoughts on this:


Some thoughts:

  • Form should use local state to store the values.
  • Each of the inputs within the Form component should be "controlled" meaning that the onChange updates the values key in local state.
  • Props:

    • formSchema: React.PropTypes.array - describes the fields of the form and the order/structure

    • onSubmit: React.PropTypes.func - called with values when the form is submitted

    • defaultValues: React.PropTypes.object - initial values for the form

  • Ideal usage would be:
  onFormSubmit (values) {
    model.updateValues(values)
  }

  renderForm () {
    <Form formSchema={formSchema} defaultValues={model.values}, onSubmit={this.onFormSubmit} />
  }

Form Schema

  • We should imply as much as possible, e.g. only require { type: 'text', name: 'firstName' } and imply label: _.startCase(name)
  • Order of the fields array is the order of how they show up in the form
  • Use stardust forms (see: https://github.com/TechnologyAdvice/stardust/blob/master/docs/app/Examples/collections/Form/Shorthand/FormFieldControlExample.js)

Given a schema:

[
  { type: 'text', name: 'firstName' },
  { type: 'text', name: 'lastName' },
  { type: 'submit', name: 'Submit' }
]

We should render:

<Form>
  <Form.Field control={Input} name='firstName' /* Other props */ />
  <Form.Field control={Input} name='lastName' /* Other props */ />

  <Form.Field control={Button}>Submit</Form.Field>
</Form>

For inputs that should show up next to each other:

[
  {
    type: 'group',
    fields: [
      { type: 'text', name: 'firstName' },
      { type: 'text', name: 'lastName' }
    ],
  },
  { type: 'text', name: 'title' },
  { type: 'submit', name: 'Submit' }
]
<Form>
  <Form.Group widths='equal'>
    <Form.Field control={Input} name='firstName' /* Other props */ />
    <Form.Field control={Input} name='lastName' /* Other props */ />
  </Form.Group>
  <Form.Field control={Input} name='title' /* Other props */ />

  <Form.Field control={Button}>Submit</Form.Field>
</Form>

For inputs that are collections (e.g. email addresses) :

[
  {
    type: 'group',
    fields: [
      { type: 'text', name: 'firstName' },
      { type: 'text', name: 'lastName' }
    ],
  },
  { type: 'text', name: 'title' },
  {
    type: 'nested',
    name: 'emailAddresses',
    fields: [
      { type: 'text', name: 'label' },
      { type: 'text', name: 'address' }
    ]
  },
  { type: 'submit', name: 'Submit' }
]
<Form>
  <Form.Group widths='equal'>
    <Form.Field control={Input} name='firstName' /* Other props */ />
    <Form.Field control={Input} name='lastName' /* Other props */ />
  </Form.Group>
  <Form.Field control={Input} name='title' /* Other props */ />
  <Form.Group widths='equal'>
    {
      emailAddresses.map((emailAddress, index) => (
        <Form.Field control={Input} name=`emailAddresses.${index}.label` /* Other props */ />
        <Form.Field control={Input} name=`emailAddresses.${index}.address` /* Other props */ />
      )
    }
    <Form.Field control={Button}>Add Email Address</Form.Field>
  </Form.Group>

  <Form.Field control={Button}>Submit</Form.Field>
</Form>

If you've gotten this far haha, regarding validations:

  • Could be passed within the field prop, e.g. { type: 'text', name: 'title', validation: (value) => value.length < 255 }
  • Could be passed within a group, e.g.
{
  type: 'group',
  fields: [
    { type: 'text', name: 'firstName' },
    { type: 'text', name: 'lastName' }
  ],
  validation: (data) => data.firstName || data.lastName
}
  • Validations could be optionally enforced onChange, onBlur, or onSubmit

The nicest thing with this approach is that you're keeping everything in data and wouldn't need to touch the DOM. Using naming like collection.0.field you can use _.get(values, keyPath) and _.set(values, keyPath, value) to get/set values.

I wound up implementing some of the ideas I wrote about in the previous comment. See the PR that's referenced right above ^

Awesome, working on an appropriate response as well. Will post shortly.

This is definitely the right place, super thankful to see this. I agree with the goals here. I have mostly the same thoughts in terms of generating forms from an object, having a model, serializing on submit, validating that model, and showing errors. We also have a few form validation needs here at TA. Both in SPAs and static properties.

Separation of Concerns

I'd like to create a set of packages that can work in any context, whether Node, SPA, or vanilla browser JS. I see a few separate pieces to this issue as a whole. We have already solved many of these problems at TA. We just need to pull them together, tweak some, and fill in missing parts. I'll layout what I see so far as the separate packages, then describe a potential workflow.

Form

This is the Stardust Form and shorthand.

_Done_

  • Supports all SUI markup permutations
  • Serializes data onSubmit, demo
  • Allows passing a custom serializer function

_Needs_

  • Shorthand generation
  • Shorthand for passing errors or some API for handling Form/Field coupling
  • Decoupled from DOM, currently passes the form node to the serializer function.

I'm curious if the shorthand and/or Form/Field data/error coupling should be a separate wrapper component as well, <FormGenerator />? Not sure.

Serializer

This is currently baked into the Form component. Try it here.

_Done_

  • Handles all form inputs including groups
  • Includes edge case support and dev warnings for bad form design
  • Serializes form inputs into a single object lending well to a model or schema
  • Fully tested

_Needs_

  • Decoupled from DOM, currently takes a form node and traverses children
  • Should accept an array of form control objects IMO, either virtual or DOM nodes will do
  • packaged independently

Schema

We may have create something here, but TA has also solved this on a broader scale with Obey. Might be a long shot, but I want to explore pulling what we can from there. Maybe breaking things out for reuse.

_Done_

  • Possible reuse from Obey

_Needs_

  • Independent minimal package for defining a schema

Validator

Again, Obey includes this. I'm thinking this should also be a separate package though. I just want a package that knows how to validate an object and returns the errors.

_Done_

  • Possible reuse from Obey

_Needs_

  • Accept a schema
  • Accept a model
  • Return a list of errors

Theoretical Workflow

  1. Define a schema
  2. Create a Form, or generate one using some shorthand
  3. Capture Form data (model) onSubmit/onChange
  4. Pass schema/model to validator, capturing errors
  5. Pass errors to Form for display

If these are separate packages, you can use schema/serializer/validator you want. We'll just ship with a sane default set. You can also use these packages in any other customized workflow you want. We could just bundle them in a sane default workflow.

If we have a separate <FormGenerator /> (bad name) component, it would wrap these packages up in a workflow like described above. You could customize it's behavior. Perhaps showing inline error popups, or a single error message, etc. If it is not satisfactory, you can wrap the <Form /> yourself for a fully custom experience. This would also keep bloat out of the Form component.

Finally, since these packages are not DOM dependent, nor React dependent, you can use them in Node or a vanilla DOM experience. It also means that we can look to the community for pieces of the tool chain.

Is form validation currently possible?

+1. Really could do with this.

Formsy (https://github.com/christianalfoni/formsy-react) works really well for validation and form building in general. already has integrations for material UI (https://github.com/mbrookes/formsy-material-ui) and bootstrap.

Would be cool to make a semantic-ui-react integration??

This is certainly something I'd like to explore. A simple wrapper around their API may be the best bet.

I'm going to freeze this issue here. We've got some good notes and input above to work with. We've also got the advanced form work as a reference, #527.

The immediate next step should be a PR exploring a thin wrapper around formsy-react. Future convo should happen on PRs related to this work.

We're also considering options for implementing something similar to https://github.com/davezuko/react-reformed.

I've pulled the 1.0 milestone from this issue. There are a lot of great ways to validate forms with extra packages. You can search our issues to find several. These should be used for now.

We still plan on adding validation, it is just not a priority item for the core team ATM.

There's also my package called uniforms. Currently it's not using this package but only SUI classes, but the concept is to abstract schemas into unified schema called _bridge_ and provide many fields which are usable with any schema.

_(it's not an advertisement, but example of how to separate schema from components and DOM structure)_

I made a bunch of formsy-react wrappers for the form elements:

formsy-semantic-ui-react

Closing for housekeeping. We're still interested in elegant form validation within Semantic UI React and welcome PRs. Until then, there are plenty of workarounds above.

I had the same struggle to do validation so i rolled out my own ADHOC validation, albeit, it can be better, one thing I struggled with, was that there is no clean API to update props outside of my render function, so each component needs a reference to the validator. The use of factories to create the components in SUI make them a bit of a locked box, if the form field could handle populating an error label it would great in addition to a label, here's some snippits:
Replace your Semantic Form with this ValidatingForm.

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import {
  Form,
  Label,
  Menu } from 'semantic-ui-react'
import FormInput from './form_input'
class ValidatingForm extends React.Component {
  validate() {
    const form = this
    const {
      validators,
      children
    } = this.props
    const findItem = (node, key) => {
      let parent
      let item
      const getChildren = (node, key) => {
        const parsedChildren = _.filter(node, item => item !== undefined && item !== false)
        for(let child of parsedChildren) {
           // console.log('child', child.type._meta)
           parent = child
           const children = _.get(child, 'props.children')
          if (_.isObject(children) && key === _.get(children, 'props.name')){
             item = children
           } else if (_.get(child, 'props.name') && key === _.get(child, 'props.name')) {
             item = child
             break
           } else if (_.isArray(children)) {
             getChildren(children, key)
           }
        }
      }
      getChildren(node, key)
      return { child: item, parent }
    }
    const newValidators = { }
    Object.entries(validators).forEach(([key, item]) => {
      const { child, parent } = findItem(children, key)
      const value = _.get(child, 'props.value') || _.get(child, 'props.defaultValue')
      const name =  _.get(child, 'props.name')
      const f = _.get(validators[name],'validator')
      const message = _.get(validators[name],'message')
      const isValid = _.isFunction(f) ? f(value) : undefined
      console.log(item.isValid, _.isFunction(f) ? f(value) : undefined)
      newValidators[name] = { validator: f, message, isValid }
    })
    form.props.onValidate(newValidators)
    const result = Object.entries(validators).find(([key, item]) => !item.isValid)
    return !(result && result.length > 0)
  }
  render() {
    return (<Form {...this.state} {...this.props} />)
  }
}
  1. Replace React Form.Input with FormInput:
import React, { Children, cloneElement, Component } from 'react'
import cx from 'classnames'
import _ from 'lodash'
import PropTypes from 'prop-types'

import {
  childrenUtils,
  createHTMLInput,
  createShorthandFactory,
  customPropTypes,
  getElementType,
  getUnhandledProps,
  META,
  partitionHTMLProps,
  SUI,
  useKeyOnly,
  useValueAndKey,
} from 'semantic-ui-react/dist/commonjs/lib'
import { Button, Icon, Label, FormField } from 'semantic-ui-react'

class Input extends Component {
  constructor(props) {
    super(props)
  }

  static propTypes = {
    as: customPropTypes.as,
    action: PropTypes.oneOfType([
      PropTypes.bool,
      customPropTypes.itemShorthand,
    ]),
    actionPosition: PropTypes.oneOf(['left']),
    children: PropTypes.node,
    className: PropTypes.string,
    disabled: PropTypes.bool,
    error: PropTypes.bool,
    fluid: PropTypes.bool,
    focus: PropTypes.bool,
    icon: PropTypes.oneOfType([
      PropTypes.bool,
      customPropTypes.itemShorthand,
    ]),
    iconPosition: PropTypes.oneOf(['left']),
    input: customPropTypes.itemShorthand,
    inverted: PropTypes.bool,
    label: customPropTypes.itemShorthand,
    labelPosition: PropTypes.oneOf(['left', 'right', 'left corner', 'right corner']),
    loading: PropTypes.bool,
    onChange: PropTypes.func,
    size: PropTypes.oneOf(SUI.SIZES),
    tabIndex: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string,
    ]),
    transparent: PropTypes.bool,
    type: PropTypes.string,
    validate: PropTypes.func
  }

  static defaultProps = {
    type: 'text',
  //   validate: _.noop
  }

  static _meta = {
    name: 'Input',
    type: META.TYPES.ELEMENT,
  }

  computeIcon = () => {
    const { loading, icon } = this.props
    if (!_.isNil(icon)) return icon
    if (loading) return 'spinner'
  }

  computeTabIndex = () => {
    const { disabled, tabIndex } = this.props

    if (!_.isNil(tabIndex)) return tabIndex
    if (disabled) return -1
  }

  focus = () => (this.inputRef.focus())

  handleChange = (e) => {
    const value = _.get(e, 'target.value')

    _.invoke(this.props, 'onChange', e, { ...this.props, value })
  }

  handleChildOverrides = (child, defaultProps) => ({
    ...defaultProps,
    ...child.props,
    ref: (c) => {
      _.invoke(child, 'ref', c)
      this.handleInputRef(c)
    },
  })

  handleInputRef = c => (this.inputRef = c)

  partitionProps = () => {
    const { disabled, type, validate } = this.props
    const tabIndex = this.computeTabIndex()
    const unhandled = getUnhandledProps(Input, this.props)
    const [htmlInputProps, rest] = partitionHTMLProps(unhandled)
    return [{
      ...htmlInputProps,
      disabled,
      validate,
      type,
      tabIndex,
      onChange: this.handleChange,
      ref: this.handleInputRef,
    }, rest]
  }

  render() {
    const {
      action,
      actionPosition,
      children,
      className,
      disabled,
      error,
      fluid,
      focus,
      icon,
      iconPosition,
      input,
      inverted,
      label,
      labelPosition,
      loading,
      size,
      transparent,
      type,
      validate
    } = this.props

    const classes = cx(
      'ui',
      size,
      useKeyOnly(disabled, 'disabled'),
      useKeyOnly(error, 'error'),
      useKeyOnly(fluid, 'fluid'),
      useKeyOnly(focus, 'focus'),
      useKeyOnly(inverted, 'inverted'),
      useKeyOnly(loading, 'loading'),
      useKeyOnly(transparent, 'transparent'),
      useValueAndKey(actionPosition, 'action') || useKeyOnly(action, 'action'),
      useValueAndKey(iconPosition, 'icon') || useKeyOnly(icon || loading, 'icon'),
      useValueAndKey(labelPosition, 'labeled') || useKeyOnly(label, 'labeled'),
      'input',
      className,
    )
    const ElementType = getElementType(Input, this.props)
    const [htmlInputProps, rest] = this.partitionProps()

    // Render with children
    // ----------------------------------------
    if (!childrenUtils.isNil(children)) {
      // add htmlInputProps to the `<input />` child
      const childElements = _.map(Children.toArray(children), (child) => {
        if (child.type !== 'input') return child
        return cloneElement(child, this.handleChildOverrides(child, htmlInputProps))
      })

      return <ElementType {...rest}  validate={this.props.validate} className={classes}>{childElements}</ElementType>
    }

    // Render Shorthand
    // ----------------------------------------
    const actionElement = Button.create(action)
    const labelElement = Label.create(label, {
      defaultProps: {
        className: cx(
          'label',
          // add 'left|right corner'
          _.includes(labelPosition, 'corner') && labelPosition,
        ),
      },
    })

    return (
      <ElementType validate={this.props.validate} {...rest} className={classes}>
        {actionPosition === 'left' && actionElement}
        {labelPosition !== 'right' && labelElement}
        {createHTMLInput(input || type, { defaultProps: htmlInputProps })}
        {actionPosition !== 'left' && actionElement}
        {Icon.create(this.computeIcon())}
        {labelPosition === 'right' && labelElement}
      </ElementType>
    )
  }
}

Input.create = createShorthandFactory(Input, type => ({ type }))


class FormInput extends Component {
  static _meta = {
    name: 'FormInput',
    parent: 'Form',
    type: META.TYPES.COLLECTION,
  }

  static defaultProps = {
    as: FormField,
    control: Input
  }
  static propTypes = {
    as: customPropTypes.as,
    control: FormField.propTypes.control,
    validator: PropTypes.obj
  }

  render(){
    const { control } = this.props
    const errorStyle = { background: 'none',
         color: '#9f3a38',
         fontSize: '0.7em',
         verticalAlign: 'top',
         lineHeight: '14px',
         textTransform: 'uppercase'
       }
    const { error, label, validator: { isValid, message }, ...rest } = { ...getUnhandledProps(FormInput, this.props) }
    const errorLabel = <Label style={{ background:'none', color:'black', fontSize: '1em' }}
      content={<span>{label} <span style={errorStyle}>{message}</span>
      </span>}/>

    const newProps = { ...rest, error: error || isValid === false, label: isValid === false ? errorLabel : label  }
    const ElementType = getElementType(FormInput, newProps)
    return <ElementType {...newProps} control={control} />
  }
}

export default FormInput

To use, you i had to just import and muck with the Input, this only works with Input type fields.

To use:

  1. Add this to your state:
 validators: {
      isbn13: isIsbn13,
      title: isRequired,
      author: isRequired,
      weeklyPrice: isRequired,
      price: isRequired
  1. Add this callback on your view from your form
onValidate = (validators) => {
    this.setState({ validators })
  }
  1. Your form looks like this
<ValidatingForm ref="book_form"
                onValidate={this.onValidate}
                validators={this.state.validators}
</ValidatingForm>

  1. My new Form Input looks like this
<FormInput
                    onChange={this.handleChange}
                    onInput={this.handleChange}
                    width={5}
                    name="isbn13"
                    value={isbn13}
                    fluid
                    label="ISBN13 *"
                    type="text"
                    placeholder="ISBN13"
                    validator={validators.isbn13}
                  />
  1. And the validator example objects look like this:
export const isRequired = {
    validator: (item = '') => {
      console.log(item)
      return !(_.isEmpty(item))
    },
    message: 'Required'
}

export const isIsbn13 = {
  validator: (item = '') => {
    return !!_.get(item.match(/9\d{12}(?=(\b|[^\d]))/), '0')
  },
  message: 'Please enter a valid ISBN13'
}
  1. To invoke it, right now just do a on the validating form, returns false if invalid
 book_form.refs.validate()

Hope this helps, validation is a pretty important feature for forms, and the biggest lacking feature IMHO. Any ideas how to improve this gladly accepted.

Hope this helps

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ryanpcmcquen picture ryanpcmcquen  路  3Comments

jayphelps picture jayphelps  路  3Comments

dilizarov picture dilizarov  路  3Comments

nix1 picture nix1  路  3Comments

devsli picture devsli  路  3Comments