Problems when updating state in React - javascript

Please, view my js fiddle where I've illustrated the problem
Here's my js fiddle:
https://jsfiddle.net/jajabya/fb93f7b0/
My goad is to get an input tag where special words (like dates or users' names could be highlighted by wrapping in span tags)
There must be a problem with div, because when I use input field instead everything works fine.
My problem is that I can't make the caret appear in the right place
Every time when the state updates in onInput
onInput(event) {
this.setState({
html: event.target.innerText.toUpperCase()
});
}
the caret rolls back to the beginning

My idea is to save the current caret position in the state, and set it back via a ref in componentDidUpdate() (since ref doesn't rerender the component).
Note: This is a prototype idea, that I've never battle tested, so use with caution.
The caret position code was take from this answers:
Code for getting the caret position
Code for setting caret position
class Editable extends React.Component {
componentDidUpdate(prev) {
const { position } = this.props;
if(position !== prev.position && this.ce.childNodes.length) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(this.ce.childNodes[0], position);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
render() {
return (
<div
contentEditable
className={this.props.className}
onInput={this.props.onInput}
ref={ce => this.ce = ce}
suppressContentEditableWarning>
{this.props.html}
</div>
);
}
}
class App extends React.Component {
state = {
html: 'Text',
caret: 0
};
handleInput = (event) => this.setState({
html: event.target.innerText.toUpperCase(),
position: window.getSelection().getRangeAt(0).startOffset
});
render() {
return (
<Editable
{...this.state}
className="Editable"
onInput={this.handleInput} />
);
}
}
ReactDOM.render(
<App />,
demo
);
<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="demo"></div>

Related

element.focus() doesn't work on rerendered contentEditable div element

import React, { useEffect, useState } from "react";
import "./styles.css";
const RichText = () => {
const [value, setValue] = useState("");
useEffect(() => {
const div = document.getElementById("textarea");
if (div) {
setTimeout(() => {
div.focus();
}, 0);
}
});
return (
<div
className="rich-text"
onInput={(e) => {
setValue(e.target.innerText);
}}
contentEditable
id="textarea"
dangerouslySetInnerHTML={{
__html: value
}}
/>
);
};
export default RichText;
I want to implement rich text component, the idea , is that you can type text inside some field , you can style this text ( make it bold, italic, underlined , etc) . I want to same text value inside on value state variable , and then wrap it somehow inside of html tags <p>Hello <b> Andrew</b></p> , and show it in real time inside the same field , styled. For showing html tags inside of div , contentEditable field, I need to use dangerouslySetInnerHTML , and this is the main problem. That on each button press , I update value, component then rerendered , focus goes at the start of field , but I want it to be at the end in time you typing new text . I've tried to make it by ref => ref.current.focus() , it doesn't work, in code above you can see that I also try to make it by vanilla js with using timeout , it also doesn't work , autoFocus - can be used only on input, textarea, etc , div can be used with this property . I have save it to ref , but then I can't show wrapped html inside of div . Tried a lot of cases , but it is seemles . Any ideas how to make it ?
The issue is when using useState hook with contentEditable and dangerouslySetInnerHTML to sync value. When you type something in the div it rerenders again and brings the cursor back to the start.
You can use instance variables in the function component (useRef to update value) to get rid of the issue.
And you should use innerHTML instead of innerText to get the HTML string saved
Try like below
import React, { useRef } from "react";
import "./styles.css";
const RichText = () => {
const editableRef = useRef(null);
const { current: value } = useRef(
'<div style="color:green">my initial content</div>'
);
const setValue = (e) => {
value.current = e.target.innerHTML;
};
const keepFocus = () => {
const { current } = editableRef;
if (current) {
current.focus();
}
};
return (
<div
className="rich-text"
onInput={setValue}
contentEditable
id="textarea"
ref={editableRef}
onBlur={keepFocus}
dangerouslySetInnerHTML={{
__html: value
}}
/>
);
};
export default RichText;
Code Sandbox
import React from "react";
import "./styles.css";
class TextInput extends React.Component {
constructor(props) {
super();
this.state = { value: "" };
}
shouldComponentUpdate() {
return true;
}
componentDidUpdate() {
const el = document.getElementById("textarea");
if (el) {
console.log(el.selectionStart, el.selectionEnd);
var range, selection;
if (document.createRange) {
//Firefox, Chrome, Opera, Safari, IE 9+
range = document.createRange(); //Create a range (a range is a like the selection but invisible)
range.selectNodeContents(el); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection(); //get the selection object (allows you to change selection)
selection.removeAllRanges(); //remove any selections already made
selection.addRange(range); //make the range you have just created the visible selection
} else if (document.selection) {
//IE 8 and lower
range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible)
range.moveToElementText(el); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
range.select(); //Select the range (make it the visible selection
}
}
}
update(value) {
this.setState({ value });
}
render() {
console.log(this.state.value);
return (
<div
className="rich-text"
onInput={(e) => {
this.update(e.target.innerText);
}}
contentEditable
id="textarea"
dangerouslySetInnerHTML={{
__html: `<b>${this.state.value}</b>`
}}
/>
);
}
}
export default TextInput;

Scroll with keyboard's up/down arrows keys in React?

I have a custom listbox, a div that contains a vertical list of other div children. I want to add an up/down arrows keys navigation to change which child is currently selected.
So when I click the first item and press the down arrow key, it should allow me to select the second item (following item). And if I click the up arrow key, it should select back the first item (previous item).
const renderInboxSummary = targetDetailsData.map((todo, index) => {
const hasNewMessageInd = todo.hasNewMessageInd;
return (
<div onClick={() => this.handleClick(targetDetailsData, todo.aprReference, index)}>
<div>
{todo.aprRecordUserName}
</div>
<div>
{todo.aprBranchCode}
</div>
<div>
{todo.aprScreeName}
</div>
</div>
);
});
Every div has a click event handler this.handleClick(targetDetailsData, todo.aprReference, index).
This can be done by using a ref in ReactJS and then adding an event listener for the keydown event and then moving the focus to the next or previous sibling.
Notes
I add tabindex attribute to each div to allow them to be focused upon
I use a ref on the wrapping element to listen for keydown
I check keycode for up/down to move to next/previous sibling
I believe the keycode for up/down on a full size keyboard is different, but I don't have one to test.
Solution
To test the demo, click on any div and then use up/down arrows
const { Component } = React;
class App extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.moveFocus();
}
moveFocus() {
const node = this.myRef.current;
node.addEventListener('keydown', function(e) {
const active = document.activeElement;
if(e.keyCode === 40 && active.nextSibling) {
active.nextSibling.focus();
}
if(e.keyCode === 38 && active.previousSibling) {
active.previousSibling.focus();
}
});
}
render() {
return (
<div ref={this.myRef}>
<div tabindex="0">First</div>
<div tabindex="1">Second</div>
<div tabindex="2">Third</div>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
div:focus {
color: red;
}
<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>
<div id="root"></div>
Documentation
https://reactjs.org/docs/refs-and-the-dom.html
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex

How can I conditionally show title tooltip if CSS ellipsis is active within a React component

Problem:
I'm looking for a clean way to show a title tooltip on items that have a CSS ellipsis applied. (Within a React component)
What I've tried:
I setup a ref, but it doesn't exist until componentDidUpdate, so within componentDidUpdate I forceUpdate. (This needs some more rework to handle prop changes and such and I would probably use setState instead.) This kind of works but there are a lot of caveats that I feel are unacceptable.
setState/forceUpdate - Maybe this is a necessary evil
What if the browser size changes? Do I need to re-render with every resize? I suppose I'd need a debounce on that as well. Yuck.
Question:
Is there a more graceful way to accomplish this goal?
Semi-functional MCVE:
https://codepen.io/anon/pen/mjYzMM
class App extends React.Component {
render() {
return (
<div>
<Test message="Overflow Ellipsis" />
<Test message="Fits" />
</div>
);
}
}
class Test extends React.Component {
constructor(props) {
super(props);
this.element = React.createRef();
}
componentDidMount() {
this.forceUpdate();
}
doesTextFit = () => {
if (!this.element) return false;
if (!this.element.current) return false;
console.log(
"***",
"offsetWidth: ",
this.element.current.offsetWidth,
"scrollWidth:",
this.element.current.scrollWidth,
"doesTextFit?",
this.element.current.scrollWidth <= this.element.current.offsetWidth
);
return this.element.current.scrollWidth <= this.element.current.offsetWidth;
};
render() {
return (
<p
className="collapse"
ref={this.element}
title={this.doesTextFit() ? "it fits!" : "overflow"}
>
{this.props.message}
</p>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
.collapse {
width:60px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="container"></div>
Since a lot of people are still viewing this question. I did finally figure out how to do it. I'll try to rewrite this into a working example at some point but here's the gist.
// Setup a ref
const labelRef = useRef(null);
// State for tracking if ellipsis is active
const [isEllipsisActive, setIsEllipsisActive] = useState(false);
// Setup a use effect
useEffect(() => {
if(labelRef?.current?.offsetWidth < labelRef?.current?.scrollWidth) {
setIsEllipsisActive(true);
}
}, [labelRef?.current, value, isLoading]); // I was also tracking if the data was loading
// Div you want to check if ellipsis is active
<div ref={labelRef}>{value}</div>
I use this framework agnostic snippet to this. Just include it on your page and see the magic happen ;)
(function() {
let lastMouseOverElement = null;
document.addEventListener("mouseover", function(event) {
let element = event.target;
if (element instanceof Element && element != lastMouseOverElement) {
lastMouseOverElement = element;
const style = window.getComputedStyle(element);
const whiteSpace = style.getPropertyValue("white-space");
const textOverflow = style.getPropertyValue("text-overflow");
if (whiteSpace == "nowrap" && textOverflow == "ellipsis" && element.offsetWidth < element.scrollWidth) {
element.setAttribute("title", element.textContent);
} else {
element.removeAttribute("title");
}
}
});
})();
From:
https://gist.github.com/JoackimPennerup/06592b655402d1d6181af32def40189d

document.execCommand ('copy') don't work in React

I have the function below that is called on click of a button . Everything works well, but the document.execCommand ('copy') simply does not work.
If I create another button and call only the contents of if in a separate function, it works well.
I have already tried calling a second function inside the first one, but it also does not work. the copy is only working if it is alone in the function.
Does anyone know what's going on?
copyNshort = () => {
const bitly = new BitlyClient('...') // Generic Access Token bit.ly
let txt = document.getElementById('link-result')
bitly.shorten(txt.value)
.then((res) => {
this.setState({ shortedLink: res.url })
if (this.state.shortedLink !== undefined) {
document.getElementById('link-result-shorted').select() // get textarea value and select
document.execCommand('copy') // copy selected
console.log('The link has been shortened and copied to clipboard!')
ReactDOM.render(<i className="fas fa-clipboard-check"></i>, document.getElementById('copied'))
}
console.log('Shortened link 👉🏼', res.url) // Shorted url
})
}
The problem is that the copy-to-clipboard functionality will only work as a direct result of a user's click event listener... This event cannot be virtualised and the execCommand will not work anywhere else than the immediate callback assigned to the event listener...
Because react virtualises and abstracts 'events' then that's very possibly where the problem lies and as suggested you should be using React's react-copy-to-clipboard.
You can use lib react-copy-to-clipboard to copy text.
import {CopyToClipboard} from 'react-copy-to-clipboard';`
function(props) {
return (
<CopyToClipboard text={'Text will be copied'}>
<button>Copy button</button>
</CopyToClipboard>
);
}
if you click button Copy button, it will copy the text Text will be copied
The lib react-copy-to-clipboard based on copy-to-clipboard does work for me, but if you want to copy the source into your own file, Some places need attention.
The code below works fine.
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<div className="App">
<h1
onClick={e => {
const range = document.createRange()
const selection = document.getSelection()
const mark = document.createElement('span')
mark.textContent = 'text to copy'
// reset user styles for span element
mark.style.all = 'unset'
// prevents scrolling to the end of the page
mark.style.position = 'fixed'
mark.style.top = 0
mark.style.clip = 'rect(0, 0, 0, 0)'
// used to preserve spaces and line breaks
mark.style.whiteSpace = 'pre'
// do not inherit user-select (it may be `none`)
mark.style.webkitUserSelect = 'text'
mark.style.MozUserSelect = 'text'
mark.style.msUserSelect = 'text'
mark.style.userSelect = 'text'
mark.addEventListener('copy', function(e) {
e.stopPropagation()
})
document.body.appendChild(mark)
// The following line is very important
if (selection.rangeCount > 0) {
selection.removeAllRanges()
}
range.selectNodeContents(mark)
selection.addRange(range)
document.execCommand('copy')
document.body.removeChild(mark)
}}
>
Click to Copy Text
</h1>
</div>
)
}
}
export default App
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<div className="App">
<h1
onClick={e => {
const mark = document.createElement('textarea')
mark.setAttribute('readonly', 'readonly')
mark.value = 'copy me'
mark.style.position = 'fixed'
mark.style.top = 0
mark.style.clip = 'rect(0, 0, 0, 0)'
document.body.appendChild(mark)
mark.select()
document.execCommand('copy')
document.body.removeChild(mark)
}}
>
Click to Copy Text
</h1>
</div>
)
}
}
export default App

React CSSTransitionGroup does not work even with key being set

I have been trying since yesterday to make an animation to my image carousel. As far as I understand, you wrap the content to be animated with the CSSTransitionGroup and make sure it stays in the dom and also specify a unique key to each child of the transition group. I believe I have followed all this yet I see no transition.
One thing worth to mention, While I was trying to get this working I suspected if something could be wrong with the key, so I tried setting the key with a random string. The key would change every-time the state changes, and for some unknown reason I could see the animation. Can someone explain this to me.
I am not sure where I am going wrong, whether the version of transition group or in setting the key to children, No clue !
Below is the code replicating my problem.
var CSSTransitionGroup = React.addons.CSSTransitionGroup
class Images extends React.Component {
constructor(props){
super(props);
this.state = {
showComponent: false,
}
}
componentWillReceiveProps(nextProps){
if (this.props.name === nextProps.showComponentName){
this.setState({
showComponent: true,
})
} else {
this.setState({
showComponent: false,
})
}
}
render() {
if (this.state.showComponent){
return (
<img src={this.props.url} />
)
} else {
return null;
}
}
}
class TransitionExample extends React.Component {
constructor (props) {
super(props);
this.onClick = this.onClick.bind(this);
this.state= {
showComponentName: null,
}
}
onClick(button) {
this.setState({
showComponentName: button.currentTarget.textContent,
})
}
render() {
var imageData = [
"http://lorempixel.com/output/technics-q-c-640-480-9.jpg",
"http://lorempixel.com/output/food-q-c-640-480-8.jpg",
"http://lorempixel.com/output/city-q-c-640-480-9.jpg",
"http://lorempixel.com/output/animals-q-c-640-480-3.jpg"
];
var images = [];
for (var i in imageData) {
i = parseInt(i, 10);
images.push(
<Images url={imageData[i]} showComponentName={this.state.showComponentName} name={imageData[i]} key={imageData[i]} />
);
}
return (
<div>
<div>
<button onClick={this.onClick}>{imageData[0]}</button>
<button onClick={this.onClick}>{imageData[1]}</button>
<button onClick={this.onClick}>{imageData[2]}</button>
<button onClick={this.onClick}>{imageData[3]}</button>
</div>
<div className="transitions">
<CSSTransitionGroup
transitionName="viewphoto"
transitionEnterTimeout={2000}
transitionLeaveTimeout={2000}
transitionAppearTimeout={2000}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}>
{images}
</CSSTransitionGroup>
</div>
</div>
);
}
}
ReactDOM.render(
<TransitionExample />,
document.getElementById('container')
);
I am also providing the link to the example on jsfiddle
The problem with your code is that images is always an array of elements that don't mount/unmount. The correct approach for this is to change the child. For example, if you substitute the return of the render method of your fiddle with this:
return (
<div>
<div>
<button onClick={this.onClick}>{imageData[0]}</button>
<button onClick={this.onClick}>{imageData[1]}</button>
<button onClick={this.onClick}>{imageData[2]}</button>
<button onClick={this.onClick}>{imageData[3]}</button>
</div>
<div className="transitions">
<CSSTransitionGroup
transitionName="viewphoto"
transitionEnterTimeout={2000}
transitionLeaveTimeout={2000}
transitionAppearTimeout={2000}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}>
<img src={this.state.showComponentName} key={this.state.showComponentName}/>
</CSSTransitionGroup>
</div>
</div>
);
The animation works! Using a simple img instead of your Images component and giving it the image url (this only works when you have clicked a button, showComponentName should be initialized to show the first image). You could also use a custom component of course, but the point here is that the children elements of CSSTransitionGroup must be changed if you want the animation to trigger because otherwise you are always rendering the same four Images components no matter whether they return the img or not. You might want to check out react-css-transition-replace since it usually works better when it comes to replacing.

Categories

Resources