So let's say that we have a HTML paragraph with some text:
<p>Hello. This is a random paragraph with some not so random text inside of this paragraph</p>
And we have an array of strings:
const highlightThisWords = ['random', 'paragraph', 'inside']
What I need is function that will highlight (change the style) of the text inside of the paragraph that is included inside of the array. Note the word paragraph is twice inside of the tag but i would need to highlight only the specific one that i clicked on. Also I need to do some computation after the click like increment a counter.
Enviroment: React.js without jquery possible
const highlightThisWords = ['random', 'paragraph', 'inside'];
const pNode = document.querySelector('p');
// turn pNode into a highlight-aware DOM
pNode.innerHTML = pNode.textContent.split(' ').map(word => {
return highlightThisWords.includes(word) ? `<span>${word}</span>` : word;
}).join(' ');
const potentialHighlights = pNode.querySelectorAll('span');
potentialHighlights.forEach(highlightableWord => {
highlightableWord.addEventListener('click', function(e) {
// dehighlight all the rest
pNode.querySelectorAll('.highlighted').forEach(highlighted => {
highlighted.classList.remove('highlighted')
});
// highlight the clicked word
highlightableWord.classList.add('highlighted');
});
});
.highlighted {
color: red;
}
<p>Hello. This is a random paragraph with some not so random text inside of this paragraph</p>
Above you find a sample snippet in vanilla js, implementing a minimal solution to your question. There is no human-sane way of determining which exact word was clicked in the paragraph, unless you wrap that word in an html tag of its own. The proposed answers so far are wrapping every single word into a tag. While this works, it would not perform great if you have long paragraphs (imagine thousands of DOM nodes in your memory just for a paragraph element). What I propose is to wrap only "potentially highlightable" words in tags.
You can either create a custom component and use that custom component for split words with " ", I did however tried to create a jsfiddle which isn't very clean, but shows a demo on how it'd work.
To show the code on this post:
class Hello extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
split(str) {
return str.split(" ");
}
make(str, i) {
return <span key={i} style={{marginLeft:3}} onClick={this.handleClick}>{str}</span>;
}
handleClick(e) {
console.log(this.props.highlights, e.target.innerText);
if (this.props.highlights.indexOf(e.target.innerText) !== -1) {
e.target.style.background = "red";
}
}
render() {
const parts = this.split(this.props.name);
return <div>{parts.map((d, i) => {
return this.make(d, i);
})}</div>;
}
}
ReactDOM.render(
<Hello highlights={['random', 'paragraph', 'inside']} name="This is a random paragraph with some not so random text inside of this paragraph" />,
document.getElementById('container')
);
Since you are using React, you can use String.prototype.split() to split the whole text into array of individual words, and then use conditional rendering to render them as highlighted or not:
class MyComponent extends React.Component {
render() {
const arrayOfStrings = stringContainingParagraph.split(' ');
return (
<div>
{arrayOfStrings.map((elem) => (
( highlightThisWords.indexOf(elem) !== -1 ) ?
<mark>{elem}</mark> :
<span>{elem}</span>
))}
</div>
);
}
}
You can then customize this code as you wish ( increment a counter, or using onClicks to get your desired functionality ).
Related
I am building a simple react app for learning purpose, I just started learning react-js, I was trying to add paragraph dynamically on user action and it worked perfectly But I want to add an onClick event in insertAdjacentHTML (basically innerHTML).
But onclick event is not working in innerHTML
app.js
const addParagraph = () => {
var paragraphSpace = document.getElementById('container')
paragraphSpace.insertAdjacentHTML('beforeend', `<p>I am dynamically created paragraph for showing purpose<p> <span id="delete-para" onClick={deleteParagraph(this)}>Delete</span>`
}
const deleteParagraph = (e) => {
document.querySelector(e).parent('div').remove();
}
class App extends React.Component {
render() {
return (
<div>
<div onClick={addParagraph}>
Click here to Add Paragraph
</div>
<div id="container"></div>
</div>
)
}
}
What I am trying to do ?
User will be able to add multiple paragraphs and I am trying to add a delete button on every paragraph so user can delete particular paragraph
I have also tried with eventListener like :-
const deleteParagraph = () => {
document.querySelector('#delete').addEventListener("click", "#delete",
function(e) {
e.preventDefault();
document.querySelector(this).parent('div').remove();
})
}
But It said
deleteParagraph is not defined
I also tried to wrap deleteParagraph in componentDidMount() But it removes everything from the window.
Any help would be much Appreciated. Thank You.
Do not manipulate the DOM directly, let React handle DOM changes instead. Here's one way to implement it properly.
class App extends React.Component {
state = { paragraphs: [] };
addParagraph = () => {
// do not mutate the state directly, make a clone
const newParagraphs = this.state.paragraphs.slice(0);
// and mutate the clone, add a new paragraph
newParagraphs.push('I am dynamically created paragraph for showing purpose');
// then update the paragraphs in the state
this.setState({ paragraphs: newParagraphs });
};
deleteParagraph = (index) => () => {
// do not mutate the state directly, make a clone
const newParagraphs = this.state.paragraphs.slice(0);
// and mutate the clone, delete the current paragraph
newParagraphs.splice(index, 1);
// then update the paragraphs in the state
this.setState({ paragraphs: newParagraphs });
};
render() {
return (
<div>
<div onClick={this.addParagraph}>Click here to Add Paragraph</div>
<div id="container">
{this.state.paragraphs.map((paragraph, index) => (
<>
<p>{paragraph}</p>
<span onClick={this.deleteParagraph(index)}>Delete</span>
</>
))}
</div>
</div>
);
}
}
insertAdjecentHTML should not be used in javascripts frameworks because they work on entirely different paradigm. React components are rerendered every time you change a component state.
So you want to manipulate look of your component by changing its state
Solution:
In constructor initialize your component's state which you will change later on button click. Initial state is array of empty paragraphs.
constructor() {
super()
this.state = {
paragraphs:[]
}
}
And alter that state on button click - like this:
<div onClick={addParagraph}>
Add Paragraph function
const addParagraph = () =>{
this.state = this.state.push('New paragraph')
}
Rendering paragraphs
<div id="container">
this.state.paragraphs.map(paragraph =>{
<p>{paragraph}</p>
})
</div>
Additional tip for ReactJS in 2022 - use Functional components instead of Class components
i'm trying to add a class after returning a map, basically im trying to highlight the text after translating each word im doing it by adding a CSS class to it. I'm able to add the class but just to the whole box instead of the words i changed, i've tried many methods but i guess im not understanding well the functions, i've tried matchAll, also tried replacing them as the previous method, and tried styling them directly but im still unable to achieve it, i've also tried adding the function right after the "return" but javascript ends the execution of the function when return is called.... :/
Thanks for your time!
export default function Text() {
var inputText;
var mapObj = { blue: "azul", green: "verde", yellow: "amarillo" };
function changeText() {
inputText = document.querySelector("#inputF");
document.getElementById("outputF").innerHTML = inputText.value.replaceAll(/\b(blue|green|yellow)\b(|,.$)/gi,
function(matched){
return mapObj[matched];
})
outputF.classList.add("mark");
}
return (
<div>
<textarea id="inputF"></textarea>
<button onClick={changeText}>Change</button>
<textarea id="outputF"></textarea>
</div>
);
};
///CSS
.mark{
background-color:blue;}
I would recommend using more React here instead of native JS especially for the state management of this component I may have gone a bit over board, but I updated your component to the following:
const Text = () => {
const [input, setInput] = React.useState('');
const [output, setOutput] = React.useState('');
const mapObj = {
blue: "azul",
green: "verde",
yellow: "amarillo"
};
const changeText = (e) => {
e.preventDefault();
const changedText = input.replaceAll(
/\b(blue|green|yellow)\b(|,.$)/gi,
(matched) => `<span class="mark">${mapObj[matched]}</span>`
)
setOutput(changedText);
}
return (
<form>
<textarea
id="inputF"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" onClick={changeText}>Change</button>
<div
id="outputF"
dangerouslySetInnerHTML={{__html: output}}
/>
</form>
);
};
Fiddle: https://jsfiddle.net/3tc2xzms/
For starters, I added 2 useState hooks to manage the input and output text. And then for the highlight portion, each matched word that was changed gets wrapped in a span.mark. Also the output textarea was changed to a div.
Related Docs:
Forms: https://reactjs.org/docs/forms.html
State Hook: https://reactjs.org/docs/hooks-state.html
Setting string as HTML: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
The style you applied will change the background color of whole textarea, what you can do instead is use setSelectionRange() method of textarea, that will give you the result near to what you are expecting.
function changeText() {
inputText = document.querySelector("#inputF");
document.getElementById("outputF").innerHTML = inputText.value.replaceAll(/\b(blue|green|yellow)\b(|,.$)/gi,
function(matched){
return mapObj[matched];
})
// something like this
// here you need to set start and end index for the highlights part, you can use the logic as per need
outputF.setSelectionRange(0, inputText.value.length )
}
and to change the highlight color you can use the following css:
::selection {
color: red;
background: yellow;
}
::-moz-selection { /* Code for Firefox */
color: red;
background: yellow;
}
On the homepage, I want users to hover over a span and when they do the content of the span changes and the background changes too.
I've created an array to store the data, the text changing and the background image. I'm going to concat the src files for the background.
I found this code snippet and have adapted it. I'm having an issue with the innerHTML as I'm not using jQuery. I don't know what to do in jsx.
The hover action is working, I did a console log but it's breaking at the innerHTML part.
The array is 2 component levels up.
//-Home.js
//--Hero.js
//---HeroImageItems.js (this is where the code below resides)
export class HeroImageItems extends Component {
loopHeroTitle = () => {
let list = this.props.heroImage.title;
titleSequence(0);
function titleSequence(i) {
if (list.length > i) {
setTimeout(function() {
document.getElementById("hero-image-title").innerHTML = list[i];
titleSequence(++i);
}, 1000);
} else if (list.length == i) {
titleSequence(0);
}
}
};
render() {
return (
<div className="hero-image-bg">
<p
className="hero-image-title"
id="hero-title-span"
onMouseOver={this.loopHeroTitle}
style={this.getActiveText()}
>
{this.props.heroImage.title}
</p>
</div>
);
}
}
HeroImageItems.propTypes = {
heroImages: PropTypes.object.isRequired
};
export default HeroImageItems;
You have the wrong id.
document.getElementById("hero-image-title").innerHTML = list[i];
is where you are setting the innerHTML.
This p tag has an id of "hero-title-span"
Put the correct id in and you should be good to go!
I'm having an interesting issue that I cannot debug.
Goal
On a class component, inside of render function, iterate over an array of objects from state using this.state.items.map((item, index) => {}) and return a contentEditable paragraph element.
On each contentEditable paragraph element, listen for the onKeyUp event. If the key being used from e.which is the enter (13) key, add a new item to this.state.items using the index of the element that was keyed, in order to insert a new element after that index using splice.
Seeing Expected Result?
No. The newly added item is instead being put at the end of the loop when it is being rendered.
Example situation and steps to reproduce:
Type "test1" into the first P element
Hit enter (a new P element is created and focused)
Type "test2" into this second, newly created, P element
Refocus on the first P element, either by shift+tab or clicking
Hit enter
See observed results: a new P element is created and focused, but it is at the end of the list and not where it is intended to be, which is between the "test1" and "test2" P elements
Here is the code that I have so far:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
items: [this.paragraphTemplate()]
}
}
render() {
return (
<section>
<div>
{this.state.items.map((item, index) => {
return <p ref={item.ref}
key={index}
contentEditable
suppressContentEditableWarning
onKeyUp={e => this.handleParagraphKeyUp(e, index, item)}></p>
})}
</div>
</section>
)
}
handleParagraphKeyUp = (e, index, item) => {
if (e.which === 13) {
let addition = this.paragraphTemplate()
this.setState(state => {
state.items.splice(index + 1, 0, addition)
return {
blocks: state.items
}
}, () => {
addition.ref.current.focus()
/* clear out the br and div elements that the browser might auto-add on "enter" from the element that was focused when the "enter" key was used */
this.state.items[index].ref.current.innerHTML = this.state.items[index].ref.current.innerHTML.replace(/<br\s*[\/]?>/gi, '').replace(/<[\/]?div>/gi, '')
})
return false
}
}
paragraphTemplate = () => {
return {
ref: React.createRef()
}
}
}
export default MyComponent
Here is a jsfiddle with the code from above.
If you take the above steps, you will see the issue that I am having.
Let me know if you require any further information, thanks in advance!
P.S. Please let me know if there any improvements that I can make to the code. I have been working in React for a short amount of time, and would love any feedback on how to make it better/cleaner.
UPDATED
Added key={index} to the P element. Note: this does not reflect any answers, it was merely added to stay in line with ReactJS list rendering.
to render a list of items, React needs key to keep track of the element
see this: https://reactjs.org/docs/lists-and-keys.html
here is your updated fiddle that working..
<p ref={item.ref}
key={item.id}
contentEditable
suppressContentEditableWarning
onKeyUp={e => this.handleParagraphKeyUp(e,
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