agilgur5 / react-signature-canvas

A React wrapper component around signature_pad (in < 150 LoC). Unopinionated and heavily updated fork of react-signature-pad

Home Page:https://agilgur5.github.io/react-signature-canvas/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Example with modal, aspect ratio, hooks, validation, and name

benfriend13 opened this issue · comments

I have worked through some of the deficiencies in the prior react hooks example posted, so please find an updated version with the following changes which I think is worthy of posting for some of the reasons below:

  • It's now a "controlled" component, eg. rather than handling both the button, modal and preview, this component just handles the modal. This lets you customise the appearance easily, and I have provided an example of this at the bottom.
  • I've added a "signee name" input, which makes sense for most use cases, but is toggle-able via the displayNameInput prop.
  • I've added validation ensuring you can't hit "save" without filling in the required fields. This is tricky, and thus my main reasoning for posting this example. It's tricky because using hooks you don't get access to an easy lifecycle event to know what the internal state of the signaturepad is. You could call isEmpty() inside a poll, but this example uses onDrawEnd which I think is elegant.

You'll note I've left bootstrap classes in. If you're on Bootstrap it'll look nice out of the box, otherwise you'll want to drop in your own utility classes.

import React, { useState, useRef, useCallback } from 'react'
import PropTypes from 'prop-types'
import SignatureCanvas from 'react-signature-canvas'
import Modal, { Footer } from 'react-modal'

const ModalSignatureCanvas = ({onSave, onHide, widthRatio, canvasProps, displayNameInput = false}) => {
  const [signatureResult, setSignatureResult] = useState('')
  const [name, setName] = useState('')
  const sigCanvas = useRef({})
  const sigPad = useRef({})

  const setNameOnChange = (event) => {
    setName(event.target.value)
  }

  const setSignatureOnChange = () => {
    const dataURL = sigCanvas.current.toDataURL()
    setSignatureResult(dataURL)
  }

  const saveInput = () => {
    onSave({dataURL: signatureResult, name: name})
  }

  const clearInput = () => {
    sigPad.current.clear()
    setSignatureResult('')
  }

  const measuredRef = useCallback(node => {
    const resizeCanvas = (signaturePad, canvas) => {
      canvas.width = canvas.parentElement.clientWidth // width of the .canvasWrapper
      canvas.height = canvas.parentElement.clientWidth / widthRatio
      signaturePad.clear()
    }

    if (node !== null) {
      sigCanvas.current = node.getCanvas()
      sigPad.current = node.getSignaturePad()
      resizeCanvas(node.getSignaturePad(), node.getCanvas())
    }
  }, [widthRatio])

  const isNameValidIfRequired = (displayNameInput && !!name) || !displayNameInput
  const isSignatureValid = !!signatureResult
  const isFormValid = isNameValidIfRequired && isSignatureValid

  return (
    <Modal
      title="Enter your signature"
      onHide={onHide}
    >
      <div className="canvasWrapper">
        <SignatureCanvas
          canvasProps={canvasProps}
          ref={measuredRef}
          onEnd={setSignatureOnChange}
        />
      </div>
      {displayNameInput &&
        <div className="nameInput">
          <label>Name of person entering signature:</label>
          <input type="text" className="form-control" onChange={setNameOnChange} value={name} />
        </div>}
      <Footer>
        <div className="btn-group btn-block">
          <button type="button" className="btn btn-secondary w-50" onClick={clearInput}>Clear</button>
          <button type="button" className="btn btn-primary w-50" onClick={saveInput} disabled={!isFormValid}>Save</button>
        </div>
      </Footer>
    </Modal>
  )
}
ModalSignatureCanvas.propTypes = {
  canvasProps: PropTypes.object,
  widthRatio: PropTypes.number.isRequired,
  onSave: PropTypes.func,
  onHide: PropTypes.func,
  displayNameInput: PropTypes.bool,
}

export default ModalSignatureCanvas

Example: Show signature and a change button

const SignatureCaptureInput = ({signature, signee, onClick, onHide, onSave, isModalOpen}) => {
  const buttonText = signature ? 'Change signature' : 'Collect signature'
  return (
    <>
      {signature &&
        <img className="img-fluid border mb-2" src={signature} />}
      {signee && <div className="blockquote-footer mb-2">{signee}</div>}
      <button type="button" className={classNames('btn btn-block', {'btn-secondary': signature, 'btn-primary': !signature})} onClick={onClick}>{buttonText}</button>
      {isModalOpen && <ModalSignatureCanvas widthRatio={3} onSave={onSave} onHide={onHide} displayNameInput />}
    </>
  )
}
SignatureCaptureInput.propTypes = {
  signature: PropTypes.string,
  signee: PropTypes.string,
  onClick: PropTypes.func,
  onHide: PropTypes.func,
  onSave: PropTypes.func,
  isModalOpen: PropTypes.bool,
}

// Usage
const [isModalOpen, setModalOpen] = useState(false)
const handleSignatureChange({dataURL, name}) => { ... }

<SignatureCaptureInput
  onSave={handleSignatureChange}
  signee={clientSignee}
  signature={clientSignature}
  onClick={() => setModalOpen(true})}
  onHide={() => setModalOpen(false})}
  isModalOpen={isModalOpen}
/>

Which looks like:

Prompt for collection:

Screen Shot 2020-03-06 at 2 51 22 pm

Modal open:

Screen Shot 2020-03-06 at 2 46 09 pm

Preview on completion:

Screen Shot 2020-03-06 at 2 45 58 pm

Apologies if this isn't in the right place to post this, but the docs just don't cover the intricacies of implementing this component. Hope this helps someone!

Appreciate the effort @benfriend13 .

While I certainly think this is a useful piece of code, as I've said in that issue, for examples to really be easy to grok and use, they must be minimal, and this adds a few features. This is similarly not a pure hooks example.

Perhaps this could be good for a Storybook with progressively advanced use cases in the future, right now these seem better as Gists, CodeSandbox examples, blog posts, tutorials, or separate components that wrap react-signature-canvas. Storybook will probably be the next thing I work on after some refactoring and TypeScript support (#42).

Also if you need to resize the canvas due to it being hidden initially, you can call ._resizeCanvas. It's currently a private API, but I've used it myself for this use case, so perhaps it might be good to make it public.