DEV Community

Othniel
Othniel

Posted on

React Double-tap Text to Input.

Overview

Ever thought of using double tapping on a text to make it an input field to edit the text?
Well i wanted to do something like that in my React application and I searched but didn't see any solution until i came across this github gist and it worked just fine bar some minor tweaks. So through this article i'll try to explain how it works and some additions I made to it.

Why not use onDoubleClick?
Well when you fire a double click on an element there is the danger of not being able to to an onClick event on that same element as seen on on this stackoverflow.

Getting Started

As seen on that github gist it'll take just to react components to get this done.

  1. EditableContainer, and
  2. FieldStyle. Of course we could name them whatever we want but i'll just stick with that.

Firstly, EditableContainer class

We'll break the code down into different segments to explain what is going on.
First we make our imports, initialize our class and render the component (standard).
import react and the FieldStyle component

import React from 'react';
import Field from './FieldStyle';
export default class EditableContainer extends React.Component {
  constructor (props) {
    super(props);
    // initialize the counter for the clicks.
    this.count = 0;

    // initialize the state
    this.state = {
      edit: false,
      value: ''
    }
  }
...
  render () {
    const {doubleClick, handleEnter, children, ...rest} = this.props;
    const {edit, value} = this.state;
    if (edit) {
      // edit mode
      return (
        <Field
          autoFocus
          defaultValue={value}
          onBlur={this.handleBlur.bind(this)}
          onKeyPress={this.handleEnter.bind(this)}
        />
      )
    } else {
      // view mode
      if(doubleClick){
        return (
          <p
            onClick={this.handleDoubleClick.bind(this)}
            {...rest}
          >
            {children}
          </p>
        )
      }else{
        return (
          <p
            onClick={this.handleSingleClick.bind(this)}
            {...rest}
          >
            {children}
          </p>
        )        
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The doubleClick prop is for when the parent component what it to either change to an input either after a single click or on double click, handleEnter is a callback function from the parent function on how to handle the input value and using it to carryout some operation (sending an asynchronous request to maybe edit something) after editing and exiting the the input field, the children is for the text value or maybe another component like an a tag and the ...rest is for other props like the className to be applied to the p tag.
If in edit mode it renders the input field with the value carrying the onBlur and onKeyPress action handlers making reference to methods we'll explain later, if not in edit mode it check if its a doubleClick operation or a single click and applies the appropriate onClick event handler.

getDerivedStateFromProps()

...
static getDerivedStateFromProps(props, state){
    if(props.edit){
      return { edit: props.edit };
    }
    return null;
  }
...
Enter fullscreen mode Exit fullscreen mode

The react component lifecycle method getDerivedStateFromProps that gets called with every change in props right before the render method is called. Further Reading
This function is to set an option to make the component editable at initialization by the parent component.

handleDoubleClick()

...
  handleDoubleClick (e) {
    // cancel previous callback
    if (this.timeout) clearTimeout(this.timeout);

    // increment count
    this.count++;

    // schedule new callback  [timeBetweenClicks] ms after last click
    this.timeout = setTimeout(() => {
      // listen for double clicks
      if (this.count === 2) {
        // turn on edit mode
        this.setState({
          edit: true,
          value: e.target.textContent
        })
      }

      // reset count
      this.count = 0
    }, 250) // 250 ms
    //}, settings.timeBetweenClicks) // 250 ms
  }
...
Enter fullscreen mode Exit fullscreen mode

This function is where the magic happens 😄.
First of it clears the previous callback on the timeout property, then it increments the click count. After that it create a new instance of the timeout and inside that callback it checks if the number of clicks is 2 signalling that there has been a double click in the specified time (the time there is 250ms of course you can change that, but it has to be reasonable because we don't want it take to long between clicks and it shouldn't be to short for it to be impossible to do the double click either).

handleSingleClick()

...
handleSingleClick (e) {
    this.setState({
      edit: true,
    });
  }
...
Enter fullscreen mode Exit fullscreen mode

This function is as simple as it appears once it's clicked it sets it to edit mode to make the input field appear.

handleBlur()

...
handleBlur (e) {
    // handle saving here, as we'll see in handle enter, I did't want to do that here in situations where the user mistakenly loses focus on the input field.

    // close edit mode
    this.setState({
      edit: false,
      value: e.target.value
    });
  }
...
Enter fullscreen mode Exit fullscreen mode

This function takes care of the event onBlur which happens when the user loses focus on the input, so we want to exit the edit mode and display the newly typed value. As I said in that comment, I did't think it wise to save the input value onBlur to prevent saving values when the user didn't intend to do that.

handleEnter()

...
handleEnter(e){
    if(e.code === "Enter" || e.charCode === 13 || e.which === 13){
      this.props.handleEnter(e.target.value);

      this.setState({
        edit: false,
        value: ''
      });
    }
  }
...
Enter fullscreen mode Exit fullscreen mode

This function is to check when the user uses the enter↩️ key or if the user on mobile it'll check its equivalent to send it to the parent component to do as it please with it (make an update operation asynchronously with it) then exit edit mode and clear the input value.
In hindsight the name might have been different but for its current purpose it'll do, but if we'll like to exit edit mode my say using the esc key we could change the name and check for that, but for now this will do.
..Putting all together..

import React from 'react';
//import settings from '../settings.js'
import Field from './FieldStyle';

export default class EditableContainer extends React.Component {
  constructor (props) {
    super(props);
    // init counter
    this.count = 0;

    // init state
    this.state = {
      edit: false,
      value: ''
    }
  }
  static getDerivedStateFromProps(props, state){
      //console.log(props.lists);
    if(props.edit){
      return { edit: props.edit };
    }
    return null;
  }

  componentWillUnmount () {
    // cancel click callback
    if (this.timeout) clearTimeout(this.timeout);
  }

  handleDoubleClick (e) {
    // cancel previous callback
    if (this.timeout) clearTimeout(this.timeout);

    // increment count
    this.count++;

    // schedule new callback  [timeBetweenClicks] ms after last click
    this.timeout = setTimeout(() => {
      // listen for double clicks
      if (this.count === 2) {
        // turn on edit mode
        this.setState({
          edit: true,
          value: e.target.textContent
        })
      }

      // reset count
      this.count = 0
    }, 250) // 250 ms
    //}, settings.timeBetweenClicks) // 250 ms
  }

  handleSingleClick (e) {
    this.setState({
      edit: true,
    });
  }

  handleBlur (e) {
    // handle saving here

    // close edit mode
    this.setState({
      edit: false,
      value: e.target.value
    });
  }
  handleEnter(e){
    if(e.code === "Enter" || e.charCode === 13 || e.which === 13){
      this.props.handleEnter(e.target.value);

      this.setState({
        edit: false,
        value: ''
      });
    }
  }

  render () {
    const {doubleClick, handleEnter, children, ...rest} = this.props;
    const {edit, value} = this.state;
    if (edit) {
      // edit mode
      return (
        <Field
          autoFocus
          defaultValue={value}
          onBlur={this.handleBlur.bind(this)}
          onKeyPress={this.handleEnter.bind(this)}
        />
      )
    } else {
      // view mode
      if(doubleClick){
        return (
          <p
            onClick={this.handleDoubleClick.bind(this)}
            {...rest}
          >
            {children}
          </p>
        )
      }else{
        return (
          <p
            onClick={this.handleSingleClick.bind(this)}
            {...rest}
          >
            {children}
          </p>
        )        
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

FieldStyle class

This class is more straight forward than the EditableContainer class

import React from 'react'

export default class FieldStyle extends React.Component {
  componentDidMount () {
    this.ref && this.ref.focus()
  }

  render () {
    const {autoFocus, ...rest} = this.props

    // auto focus
    const ref = autoFocus ? (ref) => { this.ref = ref } : null
    return (
      <input
        ref={ref}
        type="text"
        {...rest}
      />
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

The componentDidMount function would run when the component has been mounted.
this.ref && this.ref.focus()
Using this line of code we can check if the component has a ref and then we'll focus on it. In the render() method we first check if the autofocus prop is true then we ill create a ref on it to do the focusing as shown above, then the input is rendered.

Putting Our Component to Use

import React from 'react';
import EditableContainer from './EditableContainer';

const App = () => {
  const handleSingleTap(text){
    //carry out what ever we want to do with the text.
  }
  const handleDoubleTap(text){
    //carry out what ever we want to do with the text.
  }
  return(
    <div>
      <EditableContainer
        doubleClick={false}
        handleEnter={handleSingleTap}
        className='What-Ever-Classname'>
        Single tap to edit me!!
      </EditableContainer>    
      <EditableContainer
        doubleClick={true}
        handleEnter={handleDoubleTap}
        className='What-Ever-Classname'>
        Double tap to edit me!!
      </EditableContainer>
    </div>
  ) 
}

export default App
Enter fullscreen mode Exit fullscreen mode

Full implementation could be found here.

Finally

There is the npm package which is great for editing component but it uses a button which would not work for double click. I hope to try my hand in open source (first time 😅) and see if I can add this feature to the package so fingers crossed ✌️

Latest comments (0)