Last time, I was trying to render a data table and I thought that I should make some fields editable right in the table listing. It is interesting to note that I never thought about any NPM library for it, so I went all out to create a simple but effective solution for myself.
In this post, I will demonstrate how I created an editable component. The component would be able to use different form fields and notify the parent if any change has been made. That said, Let’s move….
To start with, I will define the usage that I had for the component. The component to be was consumed like this:
<InplaceEdit
isUpdatingId={true || false}
id={idForTheField || ""}
updateKey={keyToReturnAnObjectWith || ""}
onUpdate={functionToCallForTheUpdate || () => {}}
text={textForDisplay || ""}
defaultValue={defaultInCaseValueDoesNotMatch || ""}
inputType={htmlInputType || "text"}
selectOptions={ optionForSelectInputType || [
{ value: true, label: "Yes" },
{ value: false, label: "No" }
]}
valueToText={ functionToConvertFieldValueToText || value => (value === "true" ? "Yes" : "No")}
/>;
With this, I was able to use the InplaceEdit component almost anywhere that I wanted as far as single value was expected.
Now, building for the usage. Firstly, I defined the InplaceEdit component:
import React from "react" // I won't repeat this after now
function InplaceEdit() {
return (
<div>
Inplace Edit Component
</div>
);
}
export default InplaceEdit; // I won't repeat this after now
Next, I wanted to toggle between two modes. One being a normal text, and the other when it is a form field. To do this, I added an onClick
handler to the root div
element, which updates a state that determines these modes.
function InplaceEdit() {
const [showEditor, updateShowEditor] = React.useState(false);
const handleShowEditor = () => {
updateShowEditor(true);
};
return (
<div onClick={handleShowEditor} style={{ cursor: "pointer" }}>
{showEditor ? "Show Editor" : "Show Text"}
</div>
);
}
Next, I required two distinct components for both the editor and display, I called them EditDisplay
and TextDisplay
, respectively. This made the InplaceEdit component looked more like this:
function InplaceEdit() {
const [showEditor, updateShowEditor] = React.useState(false);
const handleShowEditor = () => {
updateShowEditor(true);
};
return (
<div onClick={handleShowEditor} style={{ cursor: "pointer" }}>
{showEditor ? <EditDisplay/> : <TextDisplay />}
</div>
);
}
After that, I defined the EditDisplay
component and TextDisplay
components:
// EditDisplay Component
function EditDisplay() {
return (
<></>
);
}
// Text display component
const TextDisplay = () => <></>;
So, at this point, I was able to toggle between two modes, but nothing happens and nothing is rendered, yet. To move forward, I had to display the text which would be received by the InplaceEdit
component and passed to the TextDisplay
component as props. The below code is what I implemented for this phase:
// EditDisplay Component
function EditDisplay() {
return (
<></>
);
}
// Text display component
const TextDisplay = ({text}) => <span>{text}</span>;
function InplaceEdit({ text }) {
const [showEditor, updateShowEditor] = React.useState(false);
const handleShowEditor = () => {
updateShowEditor(true);
};
return (
<div onClick={handleShowEditor} style={{ cursor: "pointer" }}>
{showEditor ? <EditDisplay/> : <TextDisplay text={text} />}
</div>
);
}
I was glad when I got to this point. I am sure you are as well, at least, something is getting rendered.
It is very correct if you guessed that the TextDisplay component will not change its state afterwards, and bulk of the work is around the EditDisplay
component. So, sorry, I won’t be adding that style that you are thinking.
The next move I made was to tell the EditDisplay what type of edit is required and the default value for the edit.
// EditDisplay Component
function EditDisplay({ defaultValue, inputType }) {
return <></>
}
// Text display component
const TextDisplay = ({ text }) => <span>{text}</span>
function InplaceEdit({ text, inputType, defaultValue }) {
const [showEditor, updateShowEditor] = React.useState(false)
const handleShowEditor = () => {
updateShowEditor(true)
}
return (
<div onClick={handleShowEditor} style={{ cursor: "pointer" }}>
{showEditor ? (
<EditDisplay
inputType={inputType || "text"}
defaultValue={defaultValue}
/>
) : (
<TextDisplay text={text} />
)}
</div>
)
}
The defaultValue
and inputType
are both passed from the InplaceEdit
component and passed down to the EditDisplay
component, the inputType
is short-circuited to return text
in case the defaultValue is not set. Not that bad, right?
The next step is that, I put up some input
components for the update. I had TextInput
which looked like this:
const TextInput = ({ type, defaultValue, onBlur, disabled }) => {
const handleKeyUp = e => {
if (e.keyCode === 13) {
onBlur(e);
}
};
const inputRef = React.useRef();
React.useEffect(() => {
inputRef.current.focus();
}, [inputRef]);
return (
<input
ref={inputRef}
type={type}
defaultValue={defaultValue}
onBlur={onBlur}
disabled={disabled}
onKeyUp={handleKeyUp}
/>
);
};
This componentTextInput
does a pretty interesting stuff, first, it handles the KeyUp
event which sends information up to the parent(InplaceEdit
) component, it can be disabled, the type is set as from the parent and the defaultValue is also configurable through the props.
And the SelectInput
, I defined it to look like this:
const SelectInput = ({ options, defaultValue, onBlur, disabled }) => {
return (
<select onChange={onBlur} defaultValue={defaultValue} disabled={disabled}>
{options.map(option => {
return <option value={option.value}>{option.label}</option>;
})}
</select>
);
};
This component(SelectInput
) accepts the options as an array of object with label
and value
keys, defaultValue
for persistence, onBlur
event handler and can be disabled by setting its disabled
props. Not too serious, I think.
For the EditDisplay, I had to render one of the two types of components based on the inputType
props:
const EditDisplay = ({
inputType,
onchange,
defaultValue,
selectOptions,
onBlur,
disabled
}) => {
return (
<>
{inputType === "select" && (
<SelectInput
options={selectOptions}
defaultValue={defaultValue}
onchange={onchange}
onBlur={onBlur}
disabled={disabled}
/>
)}
{["text", "password"].includes(inputType) && (
<TextInput
type={inputType}
onchange={onchange}
/>
)}
</>
);
};
Next, I thought, the EditDisplay
component receives some props that are not yet defined in the InplaceEdit
component, so I defined them like so:
function InplaceEdit({
text,
inputType,
defaultValue,
valueToText,
updateKey,
onUpdate,
}) {
const [showEditor, updateShowEditor] = React.useState(false)
// Keeps the text synchronized
const [displayText, updateDisplayText] = React.useState(text)
const handleShowEditor = () => {
updateShowEditor(true)
}
// Callback for the onBlur event handler
const handleEditorInputChange = React.useCallback(e => {
const value = e.currentTarget.value
// Converts the value to a displayable text using a function from the parent consumer
const text = typeof valueToText === "function" ? valueToText(value) : value
// Updates the display text for synchronization
updateDisplayText(text)
// Hides the editor
updateShowEditor(false)
if (defaultValue !== value) {
// If the value has changed, we need to tell the consumer parents about it
const handler = typeof onUpdate === "function" ? onUpdate : () => {}
// Sets the data returning an object containing the value in the updateKey or just the value itself
const data = updateKey ? { [updateKey]: value } : value
// Call the update function with the data and resource id
handler(data, id)
}
})
return (
<>
<div onClick={handleShowEditor} style={{ cursor: "pointer" }}>
{showEditor ? (
<EditDisplay
inputType={inputType || "text"}
defaultValue={defaultValue}
onBlur={handleEditorInputChange}
selectOptions={selectOptions}
disabled={isUpdatingId === id}
/>
) : (
<TextDisplay text={displayText} />
)}
</div>
</>
)
}
Noticeable from the above snippet is that, the disabled
prop is only set if the isUpdatingId
is equals to the id
props, and the <TextDisplay />
now receives a displayText
state which is synchronized across the two children of InplaceEdit
component.
And guess what, that is all there is to an InplaceEdit
component that returns just a single value. It should accept a value/text and toggles between input and text states, too damn simple, I think.
Conclusion
This was a very interesting process for me, and I never regret it and never will. There are awesome React components for this and does better job than my local component. But I thougth, I should experiment a bit, and here I have it.
The full code is available as a gist here, if you want to check it out. And, thank you for reading.
Dhanyavaad! 🙇