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

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

Closed
benfriend13 opened this issue Mar 6, 2020 · 1 comment
Closed
Labels
scope: example An example or examples could be improved

Comments

@benfriend13
Copy link

benfriend13 commented Mar 6, 2020

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!

@agilgur5 agilgur5 changed the title Updated react hooks example Example with modal, hooks, validation, and name Mar 6, 2020
@agilgur5 agilgur5 added the scope: docs Documentation could be improved label Mar 6, 2020
@agilgur5
Copy link
Owner

agilgur5 commented Mar 6, 2020

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.

@agilgur5 agilgur5 closed this as completed Mar 6, 2020
@agilgur5 agilgur5 added scope: example An example or examples could be improved and removed scope: docs Documentation could be improved labels Mar 6, 2020
@agilgur5 agilgur5 changed the title Example with modal, hooks, validation, and name Example with modal, aspect ratio, hooks, validation, and name Mar 6, 2020
@agilgur5 agilgur5 added this to the Storybook with examples milestone Apr 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope: example An example or examples could be improved
Projects
None yet
Development

No branches or pull requests

2 participants