1

I am working on React and I have created a form using TextField from @material-ui/core. The user will enter the text, it will pass to a server in the backend, and a new text will appear. I can easily do this by changing the value of the TextField. My problem is, when I show the output in the TextField, I want some of the strings to be highlighted as shown in the figures. Before After

I can get the indices and the tokens from the backend, but I didn't find a way to highlight parts of the TextField value. All what I found is styling the whole value using InputProps. I have tried using this answer, and react-highlight-words package, but I can't set the value using those as it will return an object and the shown value will be [object object] rather than a partially highlighted string I also tried find-substring-in-text-and-change-style-programmatically and uses TextField rather than Text, but I want only one TextField, not a different TextField for each word. I have added what I did based on this answer in my code.

Here is a simplified version of my code

index.js

 import React from 'react';
 import ReactDOM from 'react-dom';
 import './index.css';
 import App from './App';
 import registerServiceWorker from './registerServiceWorker';

 ReactDOM.render(<App />, document.getElementById('root'));
 registerServiceWorker();`

app.js

import React, { Component } from 'react';
import './App.css';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import HomePage from './HomePage';
import 'bootstrap/dist/css/bootstrap.css';

class App extends Component {

  render() {
    return (
      <div className="App">
      <>
        <Router>
          <Switch>
            <Route path='/' exact component={HomePage} />
          </Switch>
        </Router>
      </>
    </div>
    );
  }
}

export default App;

HomePage.js

import React from 'react'
import { TextField, Button } from "@material-ui/core";

const HomePage = () => {
  
    const [value, setValue] = React.useState();
    const [indices, setIndices] = React.useState();
    const [substrings, setSubstrings] = React.useState();

    const handleChange = (event) => {
        setValue(event.target.value)
    }

    const handleChange2 = (event) => {
      // What to set?
    }

    const handleClear = () => {
        setValue("")
    }

    const handleSubmit = () => {

        let axios = require('axios')

        let data = { 
            "text": value
        }

        axios.post('http://127.0.0.1:8000/predict', data)
            .then(response => (setValue(response.data.value),
            setIndices(response.data.indices),
            setSubstrings(response.data.substrings)
            ))

    }

    return (

      <form noValidate autoComplete="off">

        {/*This is the block that I tried from this link https://stackoverflow.com/questions/57808195/react-native-find-substring-in-text-and-change-style-programmatically*/}
        {/*
        
        {['red','black','green'].map(item => (
            <TextField 
            InputProps={{
              style: {
                background: item,
              },
            }}
            value={item}
            onChange={handleChange2}
            />
        ))}

        */}
                  
        <TextField
          value={value}
          rowsMax={15}
          InputProps={{
            style: {
              fontWeight: 500,
            },
          }}
          variant="outlined"
          onChange={handleChange}
        />
        
        <Button onClick={handleClear}>Clear</Button>
        <Button onClick={handleSubmit}>Submit</Button>

      </form>
      
    );
};

export default HomePage;

Is there a way to do this? or a simpler approach like passing an HTML code to the value?

UPDATE: I solved the problem using native contentEditable native

as Julien Ripet suggests. Here is the updataed HomePage.js code.

import React from 'react'
import {Button } from "@material-ui/core";

const HomePage = () => {

    // This handler will listen to the paste event. It will serves as "paste as plain text" to avoid pasting the style.
    const handlePaste = (event) => {
      // cancel paste
      event.preventDefault();
      // get text representation of clipboard
      var text = (event.originalEvent || event).clipboardData.getData('text/plain');
      // insert text manually
      document.execCommand("insertText", false, text);
    }

    // Set the textbox to empty when the clear button is clicked
    const handleClear = () => {
      var inputText = document.getElementById("textbox");
      inputText.innerHTML = "";
    }

    // Send the text to the server, pass the returned value with its indices to the highlight function to be highlighted and rendered on the same box
    const handleSubmit = () => {

      // Get the object of the textbox
      var inputText = document.getElementById("textbox");
      // use innerHTML instead of innerText if you want a string with the html tags
      var onlyText = inputText.innerText.trim().replaceAll("\n\n","\n")
      
      // Send data to the server, the response will be (value, indices). Pass it to the highlight function to be highlighted and rendered to the textbox.
      /*
      let axios = require('axios')
      let data = { 
        "text": onlyText
      }
      axios.post('http://127.0.0.1:8000/predict', data)
          .then(response => (highlight(response.data.value, response.data.indices)))          
      */

      // Example
      var text = "We are trying this text just to see the output";
      var indices = [{"start":1, "end":3},{"start":7, "end":8}];      
      highlight(text, indices)
    }

    const highlight = (text, indices) => {
      // Get the object of the textbox
      var inputText = document.getElementById("textbox");
  
      // Split the string to an array of strings for easier processing
      text = text.split(" ");
      
      // enclose to be highlighted words with an HTML span of class highlight 
      for (var i = 0; i < indices.length; i++) {
        text[indices[i].start] = "<span class='highlight'>" + text[indices[i].start] 
        text[indices[i].end -1] = text[indices[i].end -1] + "</span>"
      }
  
      // Join the array of strings into one string
      var new_text = text.join(' ')
      
      // Update the content of the textbox object with the highlighted text
      inputText.innerHTML = new_text;
    }
    
    
    return (

      <form noValidate autoComplete="off">

      <p
        className="textbox"
        variant="outlined" 
        id="textbox"
        onPaste={handlePaste}  /** Handle the paset event to avoid pasting the style*/
        fontWeight = {500}
        contentEditable = {true}
      ></p>

      <Button onClick={handleClear}>Clear</Button>
      <Button onClick={handleSubmit}>Submit</Button>

      </form>

    );
};

export default HomePage;

Added to App.css

.highlight {
  background-color: yellow;
}

.textbox {
  border: 1px solid #000000;
}

Here are some stackoverflow questions that helped me:
get the text content from a contenteditable div through javascript
Javascript trick for "paste as plain text" in execCommand

Amal
  • 13
  • 4
  • I think it's not possible, because if you want to highlight text, you have to add some styling, and if you want add stylings there should be included `html` tags. And if you can include `html` tags to your input, then you have a security issue – Romanas Apr 02 '21 at 12:31
  • I think I have no option to handle this problem but passing `html` tag or using native `

    ` as mentioned in the first answer. Would using native `

    ` will be more secure? if not, how can I pass an `html` tag to the value?

    – Amal Apr 03 '21 at 08:14

1 Answers1

0

welcome to StackOverflow.

I don't see anyway to solve your problem with react, or any related package. One way I could see it done, would be replacing your TextField with a native <p> with the attribute contenteditable="true", and styling that as you wish, maybe to look as close as possible to the original TextField.

Please tell me if you manage to do it, with my alternative solution or otherwise

Julien Ripet
  • 430
  • 3
  • 12