Change the cursor position in a textarea with React - javascript

I have a textarea in React that I want to turn into a "notepad". Which means I want the "tab" key to indent instead of unfocus. I looked at this answer, but I can't get it to work with React. Here is my code:
handleKeyDown(event) {
if (event.keyCode === 9) { // tab was pressed
event.preventDefault();
var val = this.state.scriptString,
start = event.target.selectionStart,
end = event.target.selectionEnd;
this.setState({"scriptString": val.substring(0, start) + '\t' + val.substring(end)});
// This line doesn't work. The caret position is always at the end of the line
this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1;
}
}
onScriptChange(event) {
this.setState({scriptString: event.target.value});
}
render() {
return (
<textarea rows="30" cols="100"
ref="input"
onKeyDown={this.handleKeyDown.bind(this)}
onChange={this.onScriptChange.bind(this)}
value={this.state.scriptString}/>
)
}
When I run this code, even if I press the "tab" key in the middle of the string, my cursor always appears at the end of the string instead. Anyone knows how to correctly set the cursor position?

You have to change the cursor position after the state has been updated(setState() does not immediately mutate this.state)
In order to do that, you have to wrap this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1; in a function and pass it as the second argument to setState (callback).
handleKeyDown(event) {
if (event.keyCode === 9) { // tab was pressed
event.preventDefault();
var val = this.state.scriptString,
start = event.target.selectionStart,
end = event.target.selectionEnd;
this.setState(
{
"scriptString": val.substring(0, start) + '\t' + val.substring(end)
},
() => {
this.refs.input.selectionStart = this.refs.input.selectionEnd = start + 1
});
}
}
jsfiddle

For anyone looking for a quick React Hooks (16.8+) cursor position example:
import React, { useRef } from 'react';
export default () => {
const textareaRef = useRef();
const cursorPosition = 0;
return <textarea
ref={textareaRef}
onBlur={() => textareaRef.current.setSelectionRange(cursorPosition, cursorPosition)}
/>
}
In this example, setSelectionRange is used to set the cursor position to the value of cursorPosition when the input is no longer focused.
For more information about useRef, you can refer to React's official doc's Hook Part.

Here's a solution in a hooks-style architecture. My recommendation is to change the textarea value and selectionStart immediately on tab insertion.
import React, { useRef } from "react"
const CodeTextArea = ({ onChange, value, error }) => {
const textArea = useRef()
return (
<textarea
ref={textArea}
onKeyDown={e => {
if (e.key === "Tab") {
e.preventDefault()
const { selectionStart, selectionEnd } = e.target
const newValue =
value.substring(0, selectionStart) +
" " +
value.substring(selectionEnd)
onChange(newValue)
if (textArea.current) {
textArea.current.value = newValue
textArea.current.selectionStart = textArea.current.selectionEnd =
selectionStart + 2
}
}
}}
onChange={e => onChange(e.target.value)}
value={value}
/>
)
}

In React 15 best option is something like that:
class CursorForm extends Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
handleChange = event => {
// Custom set cursor on zero text position in input text field
event.target.selectionStart = 0
event.target.selectionEnd = 0
this.setState({value: event.target.value})
}
render () {
return (
<form>
<input type="text" value={this.state.value} onChange={this.handleChange} />
</form>
)
}
}
You can get full control of cursor position by event.target.selectionStart and event.target.selectionEnd values without any access to real DOM tree.

Related

How do I create a tab spacing inside textarea ReactJS + Bootstrap?

My main objective is to indent the space within the text area by a particular amount once I click the Tab button.
I am using React JS and Bootstrap. I have created a bootstrap text area in the render function like so.
<textarea class="col-12 form-control-lg" id="exampleFormControlTextarea1"placeholder="Write some Lyrics" rows="50" onKeyDown={this.useTab()} value={this.state.lyrics}
onChange={e => this.setState({ lyrics : e.target.value },()=>{
this.updateSongs("Body")
})}
</textarea>
Outside my render function I am running the useTab() method.
useTab(e) {
console.log(e.keyCode); //press TAB and get the keyCode
}
I get the below error on running this code.
Unhandled Rejection (TypeError): Cannot read property 'keyCode' of undefined
I did refer to the below solution but was still unable to fix the error.
ReactJS handle tab character in textarea
Do I have to bind the function in the constructor? I'm not really sure why the event is not being captured and what I seem to be missing here.
Please follow this example:
class App extends React.Component {
constructor() {
super();
this.textAreaRef = React.createRef();
this.state = {
lyrics: ""
};
}
onChange = event => {
this.setState({
lyrics: event.target.value
});
};
// Using arrow function so no need to bind 'this'
onKeyDown = event => {
// 'event.key' will return the key as a string: 'Tab'
// 'event.keyCode' will return the key code as a number: Tab = '9'
// You can use either of them
if (event.keyCode === 9) {
// Prevent the default action to not lose focus when tab
event.preventDefault();
// Get the cursor position
const { selectionStart, selectionEnd } = event.target;
// update the state
this.setState(
prevState => ({
lyrics:
prevState.lyrics.substring(0, selectionStart) +
"\t" + // '\t' = tab, size can be change by CSS
prevState.lyrics.substring(selectionEnd)
}),
// update the cursor position after the state is updated
() => {
this.textAreaRef.current.selectionStart = this.textAreaRef.current.selectionEnd =
selectionStart + 1;
}
);
}
};
render() {
return (
<div className="App">
<textarea
ref={this.textAreaRef}
className="col-12 form-control-lg"
placeholder="Write some Lyrics"
rows="50"
value={this.state.lyrics}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
</div>
);
}
}
ReactDOM.render( <App /> , document.getElementById('root'))
.App {
padding: 0 .5rem;
}
/* control tab size */
textarea {
tab-size: 8;
}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You need to add an event listener for your useTab function:
document.addEventListener('keydown', useTab);
Edit:
Instead of an event listener, you can use the onKeyDown handler in your input
useTab = (event) => {
event.preventDefault();
console.log(event.key)
}
render: function(){
return(
<div>
<input onKeyDown={this.useTab} />
</div>
);
}

Submit form on radio button select in React

I currently have several radio button forms which are submitting data to my backend nicely when i hit the return key. However I want each form to submit and move to the next component/form as soon as a radio button is selected. Is there a way to submit a form and move to the next component on radio button selection?
Can onselect be used here, and if so how do i go about using it?
Here's my code:
export class ChoiceOne extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (e) => {
const colourType = e.target.value;
this.setState({
colourType
});
};
handleSubmit(event) {
event.preventDefault();
const typeOneItem = {
time: new Date().toLocaleString("en-gb"),
typeOne: this.state.colourType
};
firebase.writeTo(`${firebase.getCurrentUser().uid}/typeOne`, typeOneItem);
this.props.onChosen(1);
}
render() {
const colour = ['Red', 'Blue', 'Green'];
return (
<
div className = "type1" >
<
div className = "content" >
<
form onSubmit = {
this.handleSubmit
} >
{
colour.map((colour, index) =>
<
label key = {
index
} > {
colour
} <
input value = {
colour.toUpperCase()
}
checked = {
this.state.colourType === colour.toUpperCase()
}
onChange = {
this.handleChange
}
type = "radio" / >
<
/label>
)
} <
input type = "submit"
hidden / >
<
/form> < /
div > <
/div>
call your handleSubmit function from the handleChange function
for example in the `handleChange function that is called every time a radio button is selected
handleChange = (e) => {
const colourType = e.target.value;
this.setState({
colourType
});
if(//check if you are ready to submit) {
this.handleSubmit();
e.preventDefault();
}
};
handleSubmit() {
const typeOneItem = {
time: new Date().toLocaleString("en-gb"),
typeOne: this.state.colourType
};
firebase.writeTo(`${firebase.getCurrentUser().uid}/typeOne`, typeOneItem);
this.props.onChosen(1);
}
you can use onselect but since you already have a onchange here it would be more efficient to use that!
Either you can do conditional rendering, by rendering the specific component on after successfull setState, for that you have to create the components you needed and you can use the props call back to set the data in the parent. Or you can keep an iterartor and do multiple returns inside the same component.

Mobile browser issue with textarea resize

I am working in React.js and have textarea elements that dynamically expand and contract based on the size of the user's input. The intended functionality is as follows:
This works correctly in a desktop context. However, on any mobile or tablet in a modern browser (tested Safari, Chrome and Firefox) the textarea element only expands, it does not contract when content is deleted.
At first I thought it might have something to do with the onChange handler I was employing, however, the same issue remains when swapping it out with an onInput handler. So I believe the issue resides in the resize() method.
Does anyone have an idea of why I'm experiencing this issue?
I have created a style-free fiddle to share with you the basic functionality. Interestingly, the bug doesn't occur in the JSFiddle simulator on a mobile device, but if you take the same code and put it in another react environment, the bug occurs on a mobile device in modern browsers.
class Application extends React.Component {
render() {
return (
<div>
<Textarea value="This is a test" maxLength={500}/>
</div>
);
}
}
class Textarea extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value
? this.props.maxLength && this.props.maxLength > 0
? this.props.value.length < this.props.maxLength
? this.props.value
: this.props.value.substring(0, this.props.maxLength)
: this.props.value
: '',
remaining: this.props.value
? this.props.value.length < this.props.maxLength
? this.props.maxLength - this.props.value.length
: 0
: this.props.maxLength
};
this.textAreaRef = React.createRef();
this.textAreaHeight = null;
this.textAreaoffSetHeight = null;
}
componentDidMount() {
window.addEventListener('resize', this.resize);
this.resize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
handleChange = event => {
const target = event.target || event.srcElement;
this.setState({
value: target.value,
remaining: target.value
? target.value.length < this.props.maxLength
? this.props.maxLength - target.value.length
: 0
: this.props.maxLength
});
this.resize();
};
resize = () => {
const node = this.textAreaRef.current;
node.style.height = '';
const style = window.getComputedStyle(node, null);
let heightOffset =
parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
this.textAreaoffSetHeight = node.offsetTop;
this.textAreaHeight = node.scrollHeight + heightOffset;
node.style.height = this.textAreaHeight + 'px';
this.resizeBorder();
this.resizeParentNode();
};
resizeBorder = () => {
const textAreaSize = this.textAreaHeight;
const node = this.textAreaRef.current;
const borderNode = node.parentNode.querySelector(
'.textarea__border'
);
if (borderNode !== null) {
borderNode.style.top =
this.textAreaoffSetHeight + textAreaSize - 1 + 'px';
}
};
resizeParentNode = () => {
const node = this.textAreaRef.current;
const parentNode = node.parentNode;
if (parentNode !== null) {
parentNode.style.height = this.textAreaHeight + 40 + 'px';
}
};
render() {
return (
<div className={'textarea'}>
<textarea
ref={this.textAreaRef}
className={
!this.state.value
? 'textarea__input'
: 'textarea__input active'
}
value={this.state.value}
maxLength={
this.props.maxLength && this.props.maxLength > 0 ? this.props.maxLength : null
}
onChange={this.handleChange}
/>
<div className={'textarea__message'}>
{this.state.remaining <= 0
? `You've reached ${this.props.maxLength} characters`
: `${this.state.remaining} characters remaining`}
</div>
</div>
);
}
}
ReactDOM.render(
<Application />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main id="app">
<!-- This element's contents will be replaced with your component. -->
</main>
The issue is that you're modifying the DOM directly (or trying to) instead of modifying state and allowing React to flow properly. You modify the DOM elements properties in resize() then any input change will immediate call handleChange(e) and re-flow your DOM overwriting the modifications.
NEVER MIX REACT WITH DOM TOUCHING!!!
Change your resize function to behave like your handleChange(e) function and set variables within the state which control those properties during the render() of the mark-up.

How to programmatically fill input elements built with React?

I'm tasked with crawling website built with React. I'm trying to fill in input fields and submitting the form using javascript injects to the page (either selenium or webview in mobile). This works like a charm on every other site + technology but React seems to be a real pain.
so here is a sample code
var email = document.getElementById( 'email' );
email.value = 'example#mail.com';
I the value changes on the DOM input element, but the React does not trigger the change event.
I've been trying plethora of different ways to get the React to update the state.
var event = new Event('change', { bubbles: true });
email.dispatchEvent( event );
no avail
var event = new Event('input', { bubbles: true });
email.dispatchEvent( event );
not working
email.onChange( event );
not working
I cannot believe interacting with React has been made so difficult. I would greatly appreciate any help.
Thank you
This accepted solution appears not to work in React > 15.6 (including React 16) as a result of changes to de-dupe input and change events.
You can see the React discussion here: https://github.com/facebook/react/issues/10135
And the suggested workaround here:
https://github.com/facebook/react/issues/10135#issuecomment-314441175
Reproduced here for convenience:
Instead of
input.value = 'foo';
input.dispatchEvent(new Event('input', {bubbles: true}));
You would use
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
}
and then
setNativeValue(input, 'foo');
input.dispatchEvent(new Event('input', { bubbles: true }));
React is listening for the input event of text fields.
You can change the value and manually trigger an input event, and react's onChange handler will trigger:
class Form extends React.Component {
constructor(props) {
super(props)
this.state = {value: ''}
}
handleChange(e) {
this.setState({value: e.target.value})
console.log('State updated to ', e.target.value);
}
render() {
return (
<div>
<input
id='textfield'
value={this.state.value}
onChange={this.handleChange.bind(this)}
/>
<p>{this.state.value}</p>
</div>
)
}
}
ReactDOM.render(
<Form />,
document.getElementById('app')
)
document.getElementById('textfield').value = 'foo'
const event = new Event('input', { bubbles: true })
document.getElementById('textfield').dispatchEvent(event)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'></div>
Here is the cleanest possible solution for inputs, selects, checkboxes, etc. (works not only for react inputs)
/**
* See [Modify React Component's State using jQuery/Plain Javascript from Chrome Extension](https://stackoverflow.com/q/41166005)
* See https://github.com/facebook/react/issues/11488#issuecomment-347775628
* See [How to programmatically fill input elements built with React?](https://stackoverflow.com/q/40894637)
* See https://github.com/facebook/react/issues/10135#issuecomment-401496776
*
* #param {HTMLInputElement | HTMLSelectElement} el
* #param {string} value
*/
function setNativeValue(el, value) {
const previousValue = el.value;
if (el.type === 'checkbox' || el.type === 'radio') {
if ((!!value && !el.checked) || (!!!value && el.checked)) {
el.click();
}
} else el.value = value;
const tracker = el._valueTracker;
if (tracker) {
tracker.setValue(previousValue);
}
// 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324
el.dispatchEvent(new Event('change', { bubbles: true }));
}
Usage:
setNativeValue(document.getElementById('name'), 'Your name');
document.getElementById('radio').click(); // or setNativeValue(document.getElementById('radio'), true)
document.getElementById('checkbox').click(); // or setNativeValue(document.getElementById('checkbox'), true)
I noticed the input element had some property with a name along the lines of __reactEventHandlers$..., which had some functions including an onChange.
This worked for finding that function and triggering it
let getReactEventHandlers = (element) => {
// the name of the attribute changes, so we find it using a match.
// It's something like `element.__reactEventHandlers$...`
let reactEventHandlersName = Object.keys(element)
.filter(key => key.match('reactEventHandler'));
return element[reactEventHandlersName];
}
let triggerReactOnChangeEvent = (element) => {
let ev = new Event('change');
// workaround to set the event target, because `ev.target = element` doesn't work
Object.defineProperty(ev, 'target', {writable: false, value: element});
getReactEventHandlers(element).onChange(ev);
}
input.value = "some value";
triggerReactOnChangeEvent(input);
Without element ids:
export default function SomeComponent() {
const inputRef = useRef();
const [address, setAddress] = useState("");
const onAddressChange = (e) => {
setAddress(e.target.value);
}
const setAddressProgrammatically = (newValue) => {
const event = new Event('change', { bubbles: true });
const input = inputRef.current;
if (input) {
setAddress(newValue);
input.value = newValue;
input.dispatchEvent(event);
}
}
return (
...
<input ref={inputRef} type="text" value={address} onChange={onAddressChange}/>
...
);
}
React 17 works with fibers:
function findReact(dom) {
let key = Object.keys(dom).find(key => key.startsWith("__reactFiber$"));
let internalInstance = dom[key];
if (internalInstance == null) return "internalInstance is null: " + key;
if (internalInstance.return) { // react 16+
return internalInstance._debugOwner
? internalInstance._debugOwner.stateNode
: internalInstance.return.stateNode;
} else { // react <16
return internalInstance._currentElement._owner._instance;
}
}
then:
findReact(domElement).onChangeWrapper("New value");
the domElement in this is the tr with the data-param-name of the field you are trying to change:
var domElement = ?.querySelectorAll('tr[data-param-name="<my field name>"]')

Create a numbered list within a textarea element in ReactJs

I am trying to create a text area element, where on load it should display a "1. ". The user can then type a sentence and hit return. Upon return it should render a "2." in the next line. When a user is on a line that has no text and clicks backspace, it should delete the number and return the focus to the previous number point. To illustrate this: User is on line "2." -> They press backspace which removes the "2." bullet point. -> returns them to the last character of line "1."
So far i have figured out this much:
const React = require('react');
const TextArea = React.createClass({
getInitialState: function() {
return {
textAreaVal: '1. '
};
},
editTextArea: function(value) {
this.setState({
textAreaVal: value
});
},
render: function() {
return (
<div className={"container"}>
<textarea autoFocus className={"proposal-textarea"} wrap="hard" defaultValue ={this.state.textAreaVal}
onChange={this.editTextArea} />
</div>
);
},
});
module.exports = TextArea;
Does anyone have any thoughts on the best way I can accomplish this?
What you're looking for is Reacts onKeyDown event.
Same way you have onChange set up, set up a function for onKeyDown that sends to this.handleKeyDown(event). Within that function, test event.charCode to determine which key was pressed (enter should be 13 and backspace should be 8), and then apply the necessary actions as needed.
EDIT: Moving my comment to the answer block;
To handle the incrementing number, simply add a secondary state element, lineNumber. Initialize it to 1 at start. Whenever you detect a keypress of Enter, increment lineNumber and append "\n" + this.state.lineNumber + ". " to your textAreaVal.
Well, look at this fiddle
const { Component, PropTypes } = React;
class NumberedTextArea extends Component {
constructor() {
super();
this._onKeyDown = this._onKeyDown.bind(this);
this.state = {
counter: 2,
text: `1. `
}
}
_onKeyDown(e) {
console.log(e.keyCode);
if (e.keyCode ===13) {
console.log(this.refs.text.value);
this.refs.text.value = `${this.refs.text.value}\n${this.state.counter++}. `;
e.preventDefault();
e.stopPropagation();
}
}
render() {
const style = {
height: 300,
width: 200
};
return (
<textarea ref="text" onKeyDown={this._onKeyDown} style={style}>
{this.state.text}
</textarea>
);
}
}
ReactDOM.render(
<NumberedTextArea />,
document.getElementById('root')
);

Categories

Resources