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.
- EditableContainer, and
- 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>
)
}
}
}
}
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;
}
...
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
}
...
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,
});
}
...
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
});
}
...
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: ''
});
}
}
...
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>
)
}
}
}
}
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}
/>
)
}
}
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
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 ✌️
Top comments (0)