We're working on an inline editor for our builder in Touchpoint. As part of the inline editor, users should be able to edit both the style and text of a component. For example, change the colour and text of a Button, inline!
In the past two weeks, I was thinking about how we can have an editable component?🤔 I ended up with three solutions and liked one of them which I want to share with you in this post.
Making content editable in HTML
There is an HTML attribute you can pass to any HTML tags which makes it editable. If you set contenteditable=true
for nearly any HTML tags the tag will become editable!
Here you can see an example:
You can read more about it here: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content
contentEditable prop in React
Before we look at how you can do it in React, I want to mention you should be very careful when you're using it in React. When you add contentEditable=true
to a component you see a warning from React: Warning: A component is contentEditable
and contains children
managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.
This warning means from this point React doesn't have control over those components anymore and a lot of unexpected bugs can happen. So it's really up to us to make sure what we're doing doesn't have any side effects and doesn't cause problems for the rest of our App. You can learn more about this warning in this StackOverflow question.
To turn off that warning (assuming we know what we're doing and have control over it) we can pass suppressContentEditableWarning=true
to our component to turn off the warning. So basically a component like this is editable in React:
const EditableDiv = () => {
return (
{" "}
<div contentEditable={true} suppressContentEditableWarning={true}>
Awesome Div!
</div> )}
A More Generic Solution
In my ideal world, I don't want to add that prop to every component and then do all the checks and logics to listen to the changes, I want to have a more generic solution that I can use across our App.
Here is my solution:
const EditableElement = (props) => {
const { onChange } = props;
const element = useRef();
let elements = React.Children.toArray(props.children);
if (elements.length > 1) {
throw Error("Can't have more than one child");
}
const onMouseUp = () => {
const value = element.current?.value || element.current?.innerText;
onChange(value);
};
useEffect(() => {
const value = element.current?.value || element.current?.innerText;
onChange(value);
}, []);
elements = React.cloneElement(elements[0], {
contentEditable: true,
suppressContentEditableWarning: true,
ref: element,
onKeyUp: onMouseUp,
});
return elements;
};
Basically what I want to do is to wrap my components inside this component and then ready to go!
Let's review the code together. The idea is we pass a ref
to our wrapped component with an event listener for onKeyUp
to listen to any keyboard events. There is a onChange
prop that we can pass to this component and it works as a callback, so every time there is a change it gets called.
There are some initial checks to make sure we're not accepting more than one component(there are ways to handle that scenario as well, but for our use case one component is enough). (if you're not familiar with React top-level APIs you can find more details in this part of documentation).
There are two Top-Level API we used here:
React.Children.toArray(props.children): It converts the props.children
to an array.
React.cloneElement: You can pass some new props to a React component. If you're interested you can read this post for more information.
Disclaimer: There are many other ways to implement this functionality using React custom Hooks, Higher-order components and all of those works.
After making sure we have only one child wrapped then we pass a ref
, onKeyUp
and contentEditable
to our component to make it editable.
Here is an example of how you can use this component:
export default function App() {
const initialValue = "value";
const [value, setValue] = useState(initialValue);
const handleChange = (value) => {
setValue(value);
};
return (
<div className="App">
<EditableElement onChange={handleChange}>
<div style={{ outline: "none" }}>
<p>{initialValue}</p>
</div>
</EditableElement>
<label>{value}</label>
</div>
);
}
As a reminder when you use this component or editable components in React you should be careful and make sure you don't cause any issues for your App. For example, in our use case, we only wrap the components we want to edit their text inside this component.
Here is a full example:
I hope you enjoyed this post! I like to hear your feedback and if you have other approaches to implement this functionality please share it in the comments!