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,
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
This is something quite simple but somehow resulted in a crazy rabbit hole.
This link shows what I want:
https://www.w3schools.com/howto/howto_js_active_element.asp
Nothing special, now the thing becomes hairy for me when the elements in the navbar are rendered from an array of objects (from the specs). The approach I am following is basically rendering a list of buttons, this list of buttons is the state, since supposedly when you update a state it triggers a re-render, then when a button is clicked it "sets" the active class to false on the entire array-state then activates it only for the clicked one. So far it works.
The problem is that the active class is rendered two steps behind. One for the moment when the class in the array-state's elements are set to false, the other when the clicked element gets updated.
As far as I understand useState and setState are queues, hence those are applied asynchronously on each render, in order to avoid that and get the renders to show the current state, useEffect is utilized.
Now the thing is that I am not sure how to apply useEffect in order to achieve the immediate render of the "active" class.
This is the code I have:
import { options } from 'somewhere...'
export default function SideMenu(props){
let auxArr = []
let targetName
const [stateOptions, setStateOptions] = useState([...options])
const [currentOption, SetCurrentOption] = useState({})
function activeOption(e){
// this helps with event bubbling
if (e.target.tagName == "P" || e.target.tagName == "SPAN"){
targetName = e.target.parentElement.id
} else if (e.target.tagName == "IMG"){
targetName = e.target.parentElement.parentElement.id
} else {
targetName = e.target.id
}
// since the main state is an array of objects I am updating it
// in three steps, first the current object is "activated"
// then the main array-state gets "inactivated" to erase all
// the previous "active" classes, finally the activated object
// replaces the corresponding inactive object in the main state.
let targetElement = stateOptions.filter(e => e.id==targetName)[0]
SetCurrentOption({
id: targetElement.id,
activity:true,
img: targetElement.img,
name: targetElement.name
})
// first the "classes" are set to false, then the
// "activated" object replaces the corresponding one
// in the main object, from here comes the two
// steps delay.
auxArr = [...stateOptions]
auxArr.forEach(e => e.activity=false)
setStateOptions(auxArr)
const newOptions = stateOptions.map(e =>
e.id==currentOption.id ? currentOption : e
)
setStateOptions(newOptions)
}
return(
<aside className={styles.sideDiv}>
<nav>
{stateOptions.map(({id, img, name, activity, link}) => {
return(
<button key={id} id={id} onClick={activeOption} className={activity?styles.active:""}>
<Image src={img}/>
<p className={timeColor.theme}> {name} </p>
</button>
)
})}
</nav>
</aside>
)
}
Thanks in advance for any help you can provide.
Greetings
I have built a search and every time user types word it renders new checkboxes but new checkboxes don't work like they used to be none of the event listeners work on new checkboxes, when I'm clicking on checkboxes they just don't react, but in old ones, until search will render this they are working normally
//search in checkbox data
const checkOptions = (container, value, containerId) => {
for (let i = 0; i < props.unique[containerId].length; i++) {
let item = props.unique[containerId][i];
if (
props.unique[containerId][i] !== null &&
props.unique[containerId][i].includes(value)
) {
element = (
<label
onClick={(e) => {e.stopPropagation(); ifAnyChecked(e);}} key={i}>
<input onClick={(e) => {tableSearch(e);}} type="checkbox" value={item ? item : "empty"}/>
{item && item.length > 28 ? (
handleCheckbox(item)
) : (
<p>{item}</p>
)}
</label>
);
tempData += ReactDOMServer.renderToString(element);
}
}
container.innerHTML = tempData;
};
any idea what's happening?
Have you tried to use onChange event instead of onClick? As far as I know, input type checkbox doesn't have such an event like onClick.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox
I used to get this problem when I was working with Vanilla JS whenever i render a new element then that element was not triggering my events. That was because they were generated on runtime so the event wasn't bound to that element. Now I think that thing is happening here as well. So I changed your code and put it inside a state now it is working. I hope I helped. Do let me know if this is not the solution that you were looking for but it solves your problem though
I put the html inside a state array then i mapped it out inside the newCheckBox div. I changed the input to controlled input with fieldValue state. Lastly i changed the new checkbox alert from onClick={alert("doesn't goes in")} to onClick={() => alert("I think its working now right?")}
Here is the complete code sandbox
https://codesandbox.io/s/polished-sea-vedvh?file=/src/App.js
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 ).
So basically what I am doing is iterating through an array of data and making some kind of list. What I want to achieve here is on clicking on a particular list item a css class should get attached.
Iteration to make a list
var sports = allSports.sportList.map((sport) => {
return (
<SportItem icon= {sport.colorIcon} text = {sport.name} onClick={this.handleClick()} key= {sport.id}/>
)
})
A single list item
<div className="display-type icon-pad ">
<div className="icons link">
<img className="sport-icon" src={icon}/>
</div>
<p className="text-center">{text}</p>
</div>
I am not able to figure out what to do with handleClick so that If I click on a particular list it gets highlighted.
If you want to highlight the particular list item it's way better to call the handleClick function on the list item itself, and you can add CSS classes more accurately with this approach,
here is my sample code to implement the single list component
var SingleListItem = React.createClass({
getInitialState: function() {
return {
isClicked: false
};
},
handleClick: function() {
this.setState({
isClicked: true
})
},
render: function() {
var isClicked = this.state.isClicked;
var style = {
'background-color': ''
};
if (isClicked) {
style = {
'background-color': '#D3D3D3'
};
}
return (
<li onClick={this.handleClick} style={style}>{this.props.text}</li>
);
}
});
Keep a separate state variable for every item that can be selected and use classnames library to conditionally manipulate classes as facebook recommends.
Edit: ok, you've mentioned that only 1 element can be selected at a time,it means that we only need to store which one of them was selected (I'm going to use the selected item's id). And also I've noticed a typo in your code, you need to link the function when you declare a component, not call it
<SportItem onClick={this.handleClick} ...
(notice how handleClick no longer contains ()).
And now we're going to pass the element's id along with the event to the handleClick handler using partial application - bind method:
<SportItem onClick={this.handleClick.bind(this,sport.id} ...
And as I said we want to store the selected item's id in the state, so the handleClick could look like:
handleClick(id,event){
this.setState({selectedItemId: id})
...
}
Now we need to pass the selectedItemId to SportItem instances so they're aware of the current selection: <SportItem selectedItemId={selectedItemId} ....Also, don't forget to attach the onClick={this.handleClick} callback to where it needs to be, invoking which is going to trigger the change of the state in the parent:
<div onClick={this.props.onClick} className={classNames('foo', { myClass: this.props.selectedItemId == this.props.key}); // => the div will always have 'foo' class but 'myClass' will be added only if this is the element that's currently selected}>
</div>