Components.js

import React, { useLayoutEffect, useRef } from 'react'
import classNames from 'classnames'

export const Field = ({
  id,
  label,
  InputLabelProps,
  error,
  touched,
  dirty,
  children,
}) => {
  const showError = touched && error

  return (
    <div className={classNames('form-group', showError && 'has-error')}>
      {label && (
        <div className="col-3 col-sm-12">
          <label className="form-label" htmlFor={id} {...InputLabelProps}>
            {label}
          </label>
        </div>
      )}
      <div className="col-9 col-sm-12" style={{ position: 'relative' }}>
        {children}
        {showError && <div className="form-input-hint">{error}</div>}
        <div
          style={{
            position: 'absolute',
            top: 0,
            right: 0,
            padding: 6,
            opacity: 0.3,
            pointerEvents: 'none',
          }}
        >
          {touched && <span className="label label-primary">Touched</span>}
          {dirty && (
            <span className="label label-warning" style={{ marginLeft: 3 }}>
              Dirty
            </span>
          )}
        </div>
      </div>
    </div>
  )
}

export const FormStateAndButton = ({
  anyTouched,
  anyDirty,
  anyError,
  values,
  errors,
  resetToInitial,
  resetToNew,
}) => (
  <div style={{ position: 'relative' }}>
    <Code>{JSON.stringify(values, null, 2)}</Code>

    {errors && (
      <Code style={{ background: '#fce2e2' }}>
        {JSON.stringify(errors, null, 2)}
      </Code>
    )}

    <div
      style={{
        position: 'absolute',
        top: 0,
        right: 0,
        padding: 6,
        pointerEvents: 'none',
      }}
    >
      {anyTouched && <span className="label label-primary">Form Touched</span>}
      {anyDirty && (
        <span className="label label-warning" style={{ marginLeft: 3 }}>
          Form Dirty
        </span>
      )}
      {anyError && (
        <span className="label label-error" style={{ marginLeft: 6 }}>
          Form Invalid
        </span>
      )}
    </div>
    <Button disabled={!anyDirty} type="submit">
      Submit
    </Button>
    {resetToInitial && (
      <Button onClick={resetToInitial}>Reset to initial values</Button>
    )}
    {resetToNew && (
      <Button onClick={resetToNew}>Reset to new initial values</Button>
    )}
  </div>
)

export const Code = ({ children, ...props }) => (
  <pre className="code" data-lang="JSON">
    <code {...props}>{children}</code>
  </pre>
)

export const Button = props => (
  <button
    type="button"
    className="btn btn-primary"
    style={{ margin: 3 }}
    {...props}
  />
)

export function handleStringChange(handler) {
  return event => handler(event.target.value)
}

export const Input = ({ onChange, value, ...otherProps }) => {
  // Maintaining cursor position is not necessary here.
  // It is necessary only if the value is being changed to a different one
  // and the cursor jumps to the end of controlled input.
  // https://github.com/facebook/react/issues/955
  const cursorStart = useRef(null)
  const cursorEnd = useRef(null)
  const inputRef = useRef(null)
  useLayoutEffect(() => {
    if (cursorStart.current && cursorEnd.current) {
      inputRef.current.setSelectionRange(cursorStart.current, cursorEnd.current)
    }
  }, [value])

  return (
    <input
      ref={inputRef}
      className="form-input"
      onChange={e => {
        cursorStart.current = e.target.selectionStart
        cursorEnd.current = e.target.selectionEnd
        return handleStringChange(onChange)(e)
      }}
      value={value}
      {...otherProps}
    />
  )
}

function formatDate(date) {
  let d = new Date(date),
    month = '' + (d.getMonth() + 1),
    day = '' + d.getDate(),
    year = d.getFullYear()

  if (month.length < 2) month = '0' + month
  if (day.length < 2) day = '0' + day

  return [year, month, day].join('-')
}

export const DatePicker = ({ value, onChange, ...otherProps }) => (
  <Input
    type="date"
    value={value ? formatDate(value, 'YYYY-MM-DD') : ''}
    onChange={value => {
      if (!value) return onChange(null) // for clear button
      return onChange(new Date(value).toISOString()) // set in utc as timezone is stripped in the server
    }}
    {...otherProps}
  />
)

export const TimePicker = props => <Input type="time" {...props} />

function formatDateTime(date) {
  return date.replace('Z', '')
}

export const DateTimePicker = ({ value, onChange, ...otherProps }) => (
  <Input
    type="datetime-local"
    value={value ? formatDateTime(value) : ''}
    onChange={value => {
      if (!value) return onChange(null) // for clear button
      return onChange(new Date(value).toISOString()) // set in utc as timezone is stripped in the server
    }}
    {...otherProps}
  />
)