How to add text formatting to your web page with React
Many websites today have different places where users are allowed to express themselves. These can be comments on a blog post or article, feedback on a company’s services, etc.
Content creation websites and some messaging platforms allow users to add basic text formatting to their messages like making words bold, underlining and so on. In this article, I will describe how to add some simple text formatting options to a web page built with React.js.
Setting up the stage
We begin by creating a simple page with a text area and three buttons — bold, italics and underline:
import React from 'react';
import './App.css';class App extends React.Component {
constructor() {
super();
} render() {
return (
<div className="App">
<header className="App-header">
<span className="Controls">
<button><strong>B</strong></button>
<button><em>I</em></button>
<button><u>U</u></button>
</span>
<textarea rows="5" className="Text" />
</header>
</div>
);
}
}export default App;
Then we add some styling:
.App {text-align: center;}.App-header {min-height: 100vh;display: flex;flex-direction: column;align-items: center;justify-content: center;font-size: calc(10px + 2vmin);}.Controls {border: 1px solid grey;width: 50vw;text-align: left;padding: 10px;}button {font-size: calc(8px + 2vmin);margin: 0 2px;width: 50px;background-color: white;cursor: pointer;padding: 2px;}.Text {font-size: calc(5px + 2vmin);width: 50vw;padding: 10px;}
The code above gives us this:
Next, we add event handlers to the buttons so that whenever one of them is clicked, its background colour changes to light grey:
/* CSS */.Selected {background-color: lightgrey;color: white;}...// JSclass App extends React.Component {
constructor() {
super(); this.onBoldClick = this.onBoldClick.bind(this);
this.onItalicsClick = this.onItalicsClick.bind(this);
this.onUnderlineClick = this.onUnderlineClick.bind(this);
} onBoldClick(event) {
event.target.setAttribute("class", !this.state.bold ? "Selected" : "");
} onItalicsClick(event) {
event.target.setAttribute("class", !this.state.italized ? "Selected" : "");
} onUnderlineClick(event) {
event.target.setAttribute("class", !this.state.underlined ? "Selected" : "");
}
render() {
... <button onClick={this.onBoldClick}><strong>B</strong></button>
<button onClick={this.onItalicsClick}><em>I</em></button>
<button onClick={this.onUnderlineClick}><u>U</u></button> ...
}
}
The code above depends on three state parameters: this.state.bold
, this.state.italized
andis.state.underlined
which tell us which of the formatting options is currently selected. We need to add them to the constructor:
constructor() {...this.state = {bold: false,italized: false,underlined: false};}
The above gives us the following result when bold (B) is clicked:
Adding the logic
What we wish to do is to format whatever text that is written in the text area and display the result. To do that, we will create a div tag to hold the result, then we will assign this output tag and the text area each a ref so that we can access them easily later on:
constructor() {
super(); this.inputRef = React.createRef();
this.outputRef = React.createRef(); ...
}...<header className="App-header"><div ref={this.outputRef}></div><span className="Controls"><button onClick={this.onBoldClick}><strong>B</strong></button><button onClick={this.onItalicsClick}><em>I</em></button><button onClick={this.onUnderlineClick}><u>U</u></button></span><textarea rows="5" className="Text" ref={this.inputRef} /></header>...
Then we add a method to be called whenever the text in the text area has changed:
onInputChange() {// Do something}...render() {...<textarea rows="5" className="Text" ref={this.inputRef} onChange={this.onInputChange} />...}
Next, we will update the methods handling the click events for the three buttons:
onBoldClick(event) {event.target.setAttribute("class", !this.state.bold ? "Selected" : "");if (!this.state.bold) {this.outputRef.current.innerHTML += "<strong></strong>";}this.setState({bold: !this.state.bold});this.inputRef.current.focus();}
Here’s what the code above does: if the bold button is clicked, it changes its background colour as before, then it checks whether this click event is turning bold on or off. If it is to turn it on, then it adds <strong></strong>
to the output div. This strong tag would contain whatever text the user types next and will make it bold. Finally, it changes the state of the bold flag (to false if it was true, or to true if it was false). This logic is replicated for the italics and underline buttons.
The next step is to add a method called formatText() which would accept a string as its attribute, check which of the formatting options has been selected and apply formatting accordingly. If no formatting option is selected, then it just adds the string to the output:
formatText(text) {switch (true) { case this.state.bold: const allBold = this.outputRef.current.getElementsByTagName("strong"); const lastBold = allBold[allBold.length - 1]; lastBold.innerText += text; break; case this.state.italized: const allItalized = this.outputRef.current.getElementsByTagName("em"); const lastItalized = allItalized[allItalized.length - 1]; lastItalized.innerText += text; break; case this.state.underlined: const allUnderlined = this.outputRef.current.getElementsByTagName("u"); const lastUnderlined = allUnderlined[allUnderlined.length - 1]; lastUnderlined.innerText += text; break; default: this.outputRef.current.innerHTML += text; break;}}
The formatText() method works by checking which of the formatting options is true (or selected). Assuming bold is selected, it checks for the last strong element in the output div and adds the text to the element. This would make the text bold. The same is done for the italics and underline options.
We will call this formatText() method anytime the text in the text area is changed:
onInputChange() {const input = this.inputRef.current.value;const output = this.outputRef.current.innerText;const newText = input.slice(output.length);this.formatText(newText);}
With the above, when the user types something into the text area, the text is formatted if there is a formatting option selected, otherwise it is added to the output div:
However there is a problem — if the user makes a mistake and deletes a character, the change is not reflected in the output, that is, the output is only updated when the user types something new and not when they delete text:
To solve this, we will add a new method to handle when the length of the text in the text area is less than or equal to that of the text in the output. This can happen if the user deletes or replaces a part of the text.
transferText() {const input = this.inputRef.current.value;const output = this.outputRef.current.innerHTML;let inputCounter = input.length - 1, outputCounter = output.length - 1, isTag = false;while (outputCounter > -1) {// If the current character is '>', then we are in a HTML tag. Skip until we get to '<'.if (output[outputCounter] === ">") {isTag = true;outputCounter -= 1;continue;}if (isTag) {isTag = output[outputCounter] !== "<";outputCounter -= 1;continue;}// If inputCounter <= -1, then there is no more text to add to the output, so break.if (inputCounter <= -1) {this.outputRef.current.innerHTML = this.outputRef.current.innerHTML.slice(outputCounter + 1);break;}// Otherwise, replace the text in the output with the corresponding text in the text area.else {let temp = this.outputRef.current.innerHTML;temp = temp.slice(0, outputCounter) + input[inputCounter] + temp.slice(outputCounter + 1);this.outputRef.current.innerHTML = temp;inputCounter -= 1;outputCounter -= 1;}}}
The method transferText() above loops through the text in the output div’s inner HTML starting from the right, skips over HTML tags (such as <strong>
, </em>
etc.) and for each character in the output div, it replaces the text in the output with the corresponding text in the text area. If there is no more text in the text area and there is still text in the output div to loop through (this can happen if the user deleted text), it clears the remaining text in the output div.
Next, we’ll add this method to the change event handler for the text area:
onInputChange() {const input = this.inputRef.current.value;const output = this.outputRef.current.innerText;if (input.length > output.length) {const newText = input.slice(output.length);this.formatText(newText);}else {this.transferText();}}
The event handler now checks whether the text in the text area is more than that in the output (meaning the user just added some text) and formats the text if it is. If the text length is less than or equal to that in the output, it calls the transferText() method to transfer the text in the text area to the output.
Deleting text works properly now:
Let’s test the text formatting with each of the three options selected:
Conclusion
We have successfully created a web page with React that allows formatting of user-entered text. The web page supports bold, italics and underline formatting and has no problem with users deleting or changing text they’ve typed. One limitation of the above is that it does not support combining formatting options (such as using bold and underline for the same text).
Thanks for following this far. You can find the full code for this project here. If you have any question or comment, please drop it below.