How to get the selected text from text area in react? - reactjs

I am trying to make a text editor in react.Does anyone knows how to get the selected text from the textarea so that styles can be applied on the selected text.I know we can use window.getSelection in javascript but I am curious to know If any other methods are available for this functionality?

Yes there is a method to do this, specially in React. The way you should go to achieve this is as follow.
step 1:- use ref in your textarea ui element. like
`<textarea
className='html-editor'
ref='myTextarea'
value = {this.state.textareaVal}
onChange={(event)=>{
this.setState({
textareaVal:event.target.value;
});
}}
>
</textarea>`
step 2:- now you can access the DOM element,using react refs.
let textVal = this.refs.myTextarea;
step 3:- use selectionStart and selectionEnd :- using selectionStart and
selectionEnd you can get to know your start and end pointer
of selected text.which can be done as below;
let cursorStart = textVal.selectionStart;
let cursorEnd = textVal.selectionEnd;
now you have start and end index of your selected text.
step 4 :- use javascript substring function to get the selected text.
this.state.textareaVal.substring(cursorStart,cursorEnd)

The best way to make a Text Editor in React is to use DraftJS.
If you are using React, DraftJS is the way to go about it. It abstracts away many of the challenges you would face while trying to create your own text editor from scratch. This includes managing the state of your editor (similarly to how you would manage a component's state), managing text selection, applying different attributes and so on.
You can get started by checking out the docs, and I would suggest watching the introduction video on that page, which goes through the difficulties DraftJS aims to solve.
I hope that helps.

How to do it in functional component? Expanding on the answer given by Sanjeev.
function MyEditor() {
const [state,setValue] = useState({value: ""});
//1
const myRef = React.createRef()
const inputsHandler = (e) =>{
var taxt = e.target.innerHTML
let textArray = taxt.split(/\n/gm)
console.log(textArray)
setValue( {value: e.target.value} )
}
const onDone = () => {
console.log("on done", stateNew)
dispatch(updateEditorVisibility())
// dispatch(props.reducer(state.value))
dispatch(stateNew.editorReducerAction(state.value))
}
return (
<div>
<textarea
type="text"
ref={myRef}
name="first_name"
onChange={inputsHandler}
value={state.value}/>
<button onClick={() => {
let textVal = myRef.current;
let cursorStart = textVal.selectionStart;
let cursorEnd = textVal.selectionEnd;
let selectedText = state.value.substring(cursorStart,cursorEnd)
console.log(selectedText)
}}>Log</button>
</div>
)}
Create a ref using create ref
const myRef = React.createRef();
set the ref in your textarea
ref={myRef}
To access use
let cursorStart = textVal.selectionStart;
let cursorEnd = textVal.selectionEnd;
let selectedText = state.value.substring(cursorStart,cursorEnd)
console.log(selectedText)````

Related

React event listener callback functions don't use updated states

When accessed from reDraw the resizableList is an empty array, when accessed from addImageClick it shows the actual array. the TextInput element contains a text input which calls eventBus.dispatch('addtext') on change.
So, after I add an image, I have two TextInput elements, and two Resizable elements. I change the text in one of the TextInput elements, empty state array. I trigger the add image button which logs the array before resetting it, array has two elements in it before reset.
import React,{useState,useRef,useEffect} from "react";
import Resizable from "./Resizable";
import TextInput from "./TextInput";
import eventBus from "../eventbus/EventBus";
export default function Meme(){
const [image, setImage] = useState(new Image());
const [resizableList, setResizableList] = useState([]);
const hiddenFileInput = useRef(null);
const cvs = useRef(null);
function reDraw(){
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
console.log(resizableList);
for (let i=0;i<resizableList.length;i++){
let el = document.getElementById(`${i}--textbox`);
console.log('test');
}
console.log('ran');
console.log(image);
}
function addResizable(width){
let resizable = {
cvs:cvs,
imgwidth:width,
image:image
}
setResizableList((prevResizableList)=>[...prevResizableList, resizable]);
}
function draw() {
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
console.log(image.width);
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
addResizable(width);
addResizable(width);
};
function addImageClick(e){
e.preventDefault();
console.log(resizableList);
setResizableList([]);
hiddenFileInput.current.click();
};
useEffect(()=>{
eventBus.on('addtext',reDraw);
image.onload = draw;
console.log('useeffect');
},[])
return (
<main>
<form className="form">
<div className='form--textboxes'>
{resizableList.map((item,index)=>{
return (<TextInput cvs={item.cvs} image={item.image} imgwidth={item.imgwidth} key={index} id={index}/>)
})}
<button className='form--button--textboxes'>Add Text</button>
</div>
<button className="form--button" onClick={addImageClick}>Add Image</button>
<div className="form--image">
<canvas id='meme' ref={cvs} className="form--meme" height="600" width="1200"></canvas>
{resizableList.map((item,index)=>{
return (<Resizable cvs={item.cvs} imgwidth={item.imgwidth} key={index} id={index}/>)
})}
</div>
</form>
<input
type="file"
name="myImage"
style={{display:'none'}}
ref={hiddenFileInput}
onChange={(event) => {
image.src = URL.createObjectURL(event.target.files[0]);
}}
/>
</main>
)
};
I apologize if the explanation above is too long. So far I've been able to get by just by reviewing questions, it's the first time I actually had to write one.
Update: What I basically want to do is loop over the resizableList array, and update the text on the canvas based on the position and size of each resizable. I know how to do that but I can't, because when I access the array from the reDraw function, the array shows as empty. I tried looking for alternatives but I couldn't find any. I create an id for each textbox/resizable of format: arrayindex--textbox and arrayindex--box. That's how the textboxes and resizables relate to eachother.
2nd Update: Sooooooo...
function addTextClick(e){
e.preventDefault();
console.log(resizableList);
}
function reDraw(){
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
console.log(resizableList);
button.current.click();
}
As I said previously, reDraw is triggered by an event fired by the text input's onChange. I also made this button which has the addTextClick as it's onClick. And yes, I trigger input's onchange, I get one empty array followed by one length 2 array(which is the resizableList array) in my console. ?????
I guess this kinda fixes my problem as I can just create a hidden button but this is more of a workaround and I would still like to understand why this is happening.
Updated with solution:
So, I ran into this same issue while working on some other functionality of my webapp.
The problem is that when you add an event listener the callback function is set with the state values present at the time. So it doesn't matter how many times your states are updated after setting the event listener, it's still going to use the states it had when it was set up.
Funny enough, after understanding what was causing the problem I found some posts from others running into this problem and using exactly the same workaround I used above, which is to create a reference to a hidden button and have the event listener's callback function trigger the onClick.
Another solution is to use the useEffect hook and recreate the event listener every time a state is updated, but in my case that doesn't really work that well since I have a lot of states that are updated constantly.

unable to edit pre populated textboxes using react

I have a form that has textboxes that are prepopulated from an WebAPI. When I try to delete the text in the textbox to make a change it doesn't delete the prepopulate text. If I try to type over the text, I can see only the first letter of the word I'm typing in the console, but nothing changes on the UI: It' like the textbox is in readonly mode WHICH IT IS NOT
const Details = () => {
const [ server, setServer] = useState([]);
useEffect(() = > {
getServerNames();
}
const getServerName = async() => {
//gets the list of server and their details from the API
}
const serverNameChange = (e) => {
setServer(e.target.value);
}
return (
<div>
{ details.map((data) => {
<input type="text" name="server" onChange={serverNameChange} value={data.serverName} />
))}
</div>
)
};
What am I missing to allow the users to edit the textbox? The textbox is prepopulated with data, however, it can be changed. This is only happening on textboxes that are prepopulated. I don't want to click an Edit button, I want to give the user the ability to make a change in the textbox and then save it.
That might be due to the fact, that data.serverName never changes. It’s basically a static value. If you set the value of an Input, you have to handle the changes (when typing) in the onchange event.
From what I assume, according to your code is that you have multiple input boxes with preloaded values in them and you want to change your serverName if one of them get changed by the value that is in the textinput.
If so, map your details into a state variable:
const [serverNames, setServerNames] = useState(details.map( data => data.serverName));
Map the inputs from your state variable like so:
{serverNames.map((name,index) => {
< input type="text" name="server" onChange={(e) => {updateServerState(e, index)}} value={serverNames[index]} />
}
}
And your updateServerState method looks like that:
updateServerState(e, index) {
let myStateData = [...serverNames];
myStateData[index] = e.target.value;
setServerNames(myStateData);
setServer(e.target.value);
}
Caution: I haven‘t tested the code, just wrote it down. But that should give you an idea of how to solve your issue.
TL;DR; Never use non-state variables for a dynamic value.

Handling form with single state in React for web app

I have an idea to create simple web app for my own needs. I would like user to be able to use picklist to define number of form fields needed (ex. 1 - 10). Picklist would create chosen number of components with text inputs. User can type string into text input and all data would be stored as single state. I could do it just simply saying string1, string2 and having 10 states but I wonder if this would be doable as single state because I would like all text inputs to create single string in form of token.
Example:
User choose 3 text inputs:
hi
hello
goodbye
This would form token of {hi/hello/goodbye}.
Please let me know how to handle that with React.
Thanks.
Probably you can solve this using an array of objects inside your state and latter assessing to the values. I'm not sure what do you want to do, but if you want to create an app that generates dynamically inputs and after set the values in the corresponding state, you can do something like this.
export default function DinamicLinks() {
const [quantity, setQuantity] = useState(0)
const [messages, setMessages] = useState([])
const createInputs = (quantity) => {
const params = []
for (let i = 1; i <= quantity; i++){
let param = {name:i};
params.push(param)
}
return params
}
const handleMessages = (passedMessage) => {
const filteredMessages = messages.filter((message) => message.name !== passedMessage.name)
setMessages([...filteredMessages, passedMessage])
}
return (
<div className="App">
<button onClick={() => setQuantity(quantity + 1)}>More inputs</button>
<button onClick={() => setQuantity(quantity - 1)}>Less inputs</button>
{createInputs(quantity).map(({name}) => <input key={name} onChange={(e) => handleMessages({name, message:e.currentTarget.value})}></input>)}
<button onClick={() => console.log(messages)}>See messages</button>
</div>
);
}
This code will go to generate inputs dynamically and will go to store his values inside an state, that store objects inside an array, later you can implement the logic to handle the text in the way you whatever you want. By the way, this code isn't the best in performance, but I want to give you and easy solution quickly. Undoubtedly there are better ways to do this, and also is valid to mention, don't abuse the arrow functions like I did in this example, are an easy ways to have trouble of performance in React.

React - toggle css class but do not rerender everything

I have a Gatsby website and I want to use it to show flashcards. I have found multiple existing solutions online, but I would like to try it a different way. However, I am completely stuck.
Desired result:
Show one random word at a time. When the users clicks on a button 'show', the meaning of the word should be shown.
Issue:
I can generate a random combination of the words. But when the user clicks on 'show', a new combination is loaded instead of the answer. I understand why this happens. However, I do not understand how to fix this.
I have tried this (stripped to the bare essentials):
export const Flashcard = () => {
const [flip, setFlip] = useState(false)
const onButtonClick = () => {
setFlip(!flip)
}
let word = words[GenerateRandomNumber(0, words.length - 1)] // words is an array containing all the possible combinations
return (
<div className={`card ${flip ? "flip" : ""}`}>
<div className="front">
{word["language1"]}
</div>
<div className="back">
{word["language2"]}
</div>
<button onClick={onButtonClick}>Show answer</button>
</div>
)
}
export default Flashcard
I feel like I am on a totally wrong path, but don't see how to fix it. Should I structure my logic different? Should I use some other hooks? Or anything else?
In addition, I would also like the user having the possibility to say he knew the meaning (clicking on a button 'correct') or not (button 'wrong'). If correct, a new word is shown, if wrong, the word is stored to be shown later again. How should I trigger this logic?
I think you should add a state to store the picked word
const [word, setWord] = useState("");
and When you do a click you need to get a random word and store in word
useEffect(() => {
const randomNumber = GenerateRandomNumber(0, words.length - 1);
const newWord = words[randomNumber];
setWord(newWord);
}, [flip]); //This useEffect will call everytime when flip change.
in return you can refer the word
{word["language1"]}
here is a demo

How does document.getSelection work under reactjs environment?

I was working on a electron-react project for epub file. Right now, I am planning to make the app capable of selecting text field and highlight it.
To achieve it, I was trying to use web's Window.getSelection api. However, there are some really weird things come up like in this.
In short, I am unable to capture the Selection object. It seems like even if I log the Selection object, this object could somehow jump to something else. Also, I cannot even serialize the Selection object with JSON.stringfy. This is super surprising, and this is my first time seeing something like this (I will get empty object to stringfy the Selection object).
So how to use Window.getSelection properly under react-electron environment? Or this api will not work well for text content which is generated by react's dangerouslySetInnerHTML?
Looks like the window.getSelection api needs to toString the selected object.
const getSelectionHandler = () => {
const selection = window.getSelection();
console.log("Got selection", selection.toString());
};
Super simple textarea and button to get selection react demo
this solution might help someone, catch last selection
const selection = useRef('');
const handleSelection = () => {
const text = document.getSelection().toString();
if (text) {
selection.current = text;
}
}
useEffect(() => {
document.addEventListener('selectionchange', handleSelection);
return () => {
document.removeEventListener('selectionchange', handleSelection);
};
});

Resources