I am making a simple accordion and inside each accordion, there is a text editor.
Accordion.js
<div className="wrapper">
{accordionData.map((item, index) => (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={() => toggleHandler(index)}
>
{toggleValue !== index ? `Expand` : `Shrink`}
</div>
</Heading>
<Text> {toggleValue === index && item.content && <EditorContainer />} </Text>
</Accordion>
))}
</div>
Here accordion is made up of as a component. This line {toggleValue === index && item.content && <EditorContainer />} is made to check the accordion clicked and then it loads the content and text editor accordingly.
Complete working example:
https://codesandbox.io/s/react-accordion-forked-dcqbo
Steps to reproduce the issue:
-> Open the above link
-> There will be three accordion
-> Click on any of the accordion, that will change the text from Expand to Shrink
-> Now fill some random text inside the editor then click on the text Shrink
-> Again open the same accordion by clicking Expand
-> Now already entered value is missing
I doubt it happens because every time we expand/shrink, the text_editor.js component gets called and that has the state value like,
this.state = {
editorState: EditorState.createEmpty()
};
Here instead of EditorState.createEmpty(), Should I need to give any other thing?
Requirement:
How can I store the already entered value in the text editor. Even though user clicks expand/shrink, the entered text needs to be remain there in the editor.
Any help is much appreciated.
You are correct, the entered value is missing because you are unmounting the EditorContainer component when its shrinked — that when you expand it again it creates a new editorState which is empty.
2 Possible solutions I could think of.
Move editorState and onEditorStateChange to the Parent component and pass that to EditorContainer. This way, when we unmount the EditorContainer we won't lose the previous editorState because it's on the Parent.
We wrap our EditorContainer inside a div and we'll apply a display style when we toggle between shrink/expand. This way, we are only hiding the EditorContainer not unmounting so its states will retain.
I would choose to implement the 2nd solution because we only have to make changes to our Accordion.js file. In either ways, I would create a new component that would handle the current item. I call it NormalAccordionItem.
const NormalAccordionItem = ({ data }) => {
const [show, setShow] = useState(false);
function toggle() {
setShow((prev) => !prev);
}
return (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={toggle}
>
{show ? "Shrink" : "Expand"}
</div>
</Heading>
<Text>
<div style={{ display: show ? "block" : "none" }}> // notice this
<EditorContainer />
</div>
</Text>
</Accordion>
);
};
Then on our NormalAccordion we'll use NormalAccordionItem.
const NormalAccordion = () => {
return (
<div className="wrapper">
{accordionData.map((data) => (
<NormalAccordionItem data={data} key={data.id} />
))}
</div>
);
};
That's it, check the demo below.
Edit Updated demo to expand NormalAccordionItem one at a time.
Related
I am mapping through an array and displaying its data. I have edit button to modify that data. When I click edit button, dropdown shows and I am able to edit as shown in screenshot I shared. Open this screenshot
Problem is all of edit buttons work even when I click any one, I want only clicked edit button to work rather than all. How can I achieve this functionality?
This is my code:
const [show, setShow] = useState(false);
const handleShow = () => { setShow(!show); };
<p onClick={handleShow}>
Edit
{show === true ? (
<IoIosArrowUp style={{ color: "#F1BB0F" }} />
) : (
<IoIosArrowDown style={{ color: "#F1BB0F" }} />
)}
</p>
{show && (
<div>
<p>
Unlocks: {lockParams[index]?.endDateString}
</p>
<p>
Unlocker : <span>{lockParams[index]?.unlocker}</span>
</p>
</div>
)}
If you have N different buttons, and you want the view to be different depending on which of those buttons are toggled on or off, you should have state corresponding to those N buttons. An array of booleans would make sense here. That way, if, say, the 4th button is toggled, you can update the 4th element in the state array, and then when the component renders, while iterating over the 4th index, it can look at that 4th boolean to see what should be shown there.
You haven't shown the necessary code in the question, but, for example, if you have:
someArray.map((item) => (
// some JSX
<p onClick={handleShow}>
// etc
Then you need to initialize a state array containing the same number of elements as someArray, like this:
const [shows, setShows] = useState(() => someArray.map(() => false));
const handleShow = (i) => setShows((show, j) => j === i ? !show : show);
And then use index when rendering:
someArray.map((item, i) => (
// some JSX
<p onClick={() => handleShow(i)}>
Edit
{shows[i] ? (
<IoIosArrowUp style={{ color: "#F1BB0F" }} />
) : (
<IoIosArrowDown style={{ color: "#F1BB0F" }} />
)}
</p>
{shows[i] && (
<div>
<p>
Unlocks: {lockParams[index]?.endDateString}
I've implemented a drawer similar to the example shown here. For working reproductions, please follow the link above and edit the Responsive Drawer on Stackblitz or Codesandbox. All that needs to be done to see the issue is to add onClick={(e) => console.log(e.target.tagName)} to the <ListItem button>.
Everything works as expected, except if you click on the top/bottom edge of a ListItem - in that case, I'm not able to get to the value assigned to the ListItem, and it's treated like an escape/cancellation and closes the drawer. In the <ListItem> the method onClick={(e) => console.log(e.target.tagName) will correctly log SPAN if you click in the middle, but will log DIV and be unresponsive if you click on the edge.
Example of one of the list items:
<Collapse in = {isOpen} timeout = 'auto' unmountOnExit>
<List component = 'div' disablePadding>
<ListItem button key = {'Something'} value = {'Something'} sx = {{pl: 4}} onClick = {(e) => handleSelect(e)}>
<ListItemIcon><StarBorder /></ListItemIcon>
<ListItemText primary = {'Something'} />
</ListItem>
</List>
</Collapse>
Overall structure of the drawer:
<List>
<Box>
<ListItem />
<Collapse>
<ListItem />
<ListItem />
</Collapse>
</Box>
</List>
onClick:
const handleSelect = (e) =>
{
const parentTag = e.target.tagName
if (parentTag === 'DIV')
{
console.log(e.target.innerHTML)
for (let child of e.target.children)
{
if (child.tagName === 'SPAN')
{
console.log(child.innerHTML)
}
}
}
else if (parentTag === 'SPAN')
{
console.log(e.target.innerHTML)
}
}
If you were to click in the middle of a ListItem, then parentTag === 'SPAN', and the console will log Something as expected.
But if you click on the top or bottom edge, then parentTag === 'DIV', and console.log(e.target.innerHTML) will show the following:
<div class="MuiListItemIcon-root..."><svg class="MuiSvgIcon-root..."><path d="..."></path>
</svg></div><div class="MuiListItemText-root..."><span class="MuiTypography-root...">
Something
</span></div><span class="MuiTouchRipple-root..."><span class="css..."><span
class="MuiTouchRipple..."></span></span></span>
There are three <span> elements, and I need the value of the first. However, console.log(child.innerHTML) always logs the later ones:
<span class="css..."><span
class="MuiTouchRipple..."></span></span>
Is there a way to get to the actual value I need? Or a better way to handle this, maybe by making the <div> unclickable/expanding the click area of the ListItem?
We can traverse till topmost parent div and search the content span from there:
const handleSelect = (e) => {
let target = e.target;
// get to the parent
while (!target.classList.contains('MuiButtonBase-root')) {
target = target.parentElement;
}
// get the content span
target = target.querySelector('.MuiTypography-root');
// utilize the content
setContent(`Clicked on: ${selected.tagName}, content: ${target.innerHTML}`);
console.log(selected);
handleDrawerToggle();
};
Even if you click on svg path element, above code will get you to the desired span element.
demo on stackblitz.
Also, we can prevent clicks on parent div using pointer-events:none CSS rule. But this will create huge unclickable area. And the SVG icon is also clickable :/ We'll have to make a lot of changes in CSS to bring the desired span in front of/covering everything.
Old answer
If you are trying to figure out which item got clicked then you can define onclick handler like this:
<ListItem button key={text}
onClick={(e) => console.log(text) } >
OR
<ListItem button key={text}
onClick={(e) => handleSelect(text) } >
This will give you the list item name right away. Then you can open corresponding content.
That's actually a CSS problem. You need to make the child elements width and height equal to the parent elements width and height. This is true for every element which is inline by default and you want to work with it.
Here are some docs about the CSS box model:
box model
understanding the inline box model
In this case, you want to change the display element in ListItem to div
AKA
<ListItem component="div">
// some stuff
</ListItem>
I'm building a multi tab chat like UI using react and antd, it looks like image below.
On the left side you can see multiple tabs showing last names using antd Tabs, on the right side I'm using antd comments to display each comment on the conversation thread
Now, the issue is that I'm trying to use useRef so it scrolls automatically to bottom when a new message is sent, and my code works, but only if I'm on the first tab or on the last one but no with the one on the middle and I'm stuck on finding out why
This is my code:
//reference and scroll function
const myRef = useRef(null);
const executeScroll = () => {myRef.current.scrollIntoView({ behavior: "smooth" })};
//useEffect associated to the source of the chat messages array
useEffect(executeScroll, [BuzonDataAgrupado]);
//And the Tab component
<Tabs tabPosition='left' onChange={handleTabChange}>
{
Object.entries(BuzonDataAgrupado).map(([tabName, mensajes]) => {
return(
<TabPane tab={tabName} key={tabName}>
<Card className='buzon-container'>
<div style={divStyle}>
{mensajes.map((mensaje) => {
return(
<Comment className='buzon-message-sent'
key={mensaje._id}
author={<a>{mensaje.nombreFamilia}</a>}
content={<p>{mensaje.Texto}</p>}
datetime={
<Tooltip title
{moment(mensaje.Fecha).format('YYYY-MM-DD HH:mm:ss')}>
<span>{moment(mensaje.Fecha).fromNow()}</span>
</Tooltip>}/>
)//return
})} //map
//This is the reference where is scrolls to at the end of message list
<div ref={myRef}></div>
</div>
<Divider />
<div className='buzon-message-editor'>
<Form.Item>
<TextArea rows={2} onChange={handleMensajeChange} value={NuevoMensaje} />
</Form.Item>
<Form.Item>
<Button htmlType="submit" loading={SendingMessage} onClick={sendMessage} type="primary">Enviar mensaje</Button>
</Form.Item>
</div>
</Card>
</TabPane>
)})
}
</Tabs>
Thoughts?
Have you debugged the myRef? Does it get always reassigned to the proper div when you change tab?
I cannot see why your code wouldn't work, but I have an idea for a workaround:
give the div an id
find the element by the id and use that to scroll
document.getElementById('your-div-id').scrollIntoView({ behavior: 'smooth' })
You could improve this if you put ref on the <Tabs> component (or its parent if it's not possible) and then you could use
tabsRef.current.getElementById('your-div-id').scrollIntoView({ behavior: 'smooth' })
Note that in javascript it might be a good idea to first test if the result of getElementById('your-div-id') is not null or undefined.
I'm new to React, Nodejs and JavaScript so bear with me.
I'm doing some practice with onClick events to change text by clicking some buttons, I have an input type="checkbox" to make the text bold when checked and vise versa, 2 buttons to increase and decrease the text size by 1+ or 1- and a span that shows the current text size (16 is my default), and finally a span with the id="textSpan" that have the text meant to be modified. I also want this buttons, the checkbox and the span with the id="fontSizeSpan" that shows the current font size to be hidden by default and when you click the text it appears on its left.
This is the code so far:
class FontChooser extends React.Component {
constructor(props) {
super(props);
this.state = {hidden: true};
this.checkInput = React.createRef();
this.hide = React.createRef();
}
toggle(){
this.setState({hidden: !this.state.hidden});
this.hide.current
}
makeBold(){
this.setState({bold: !this.state.bold});
this.checkInput.current
}
changeSize(){
this.setState({size: !this.props.size})
for(var i = this.props.size; i <= this.props.max; i++);
}
render() {
return(
<div>
<input type="checkbox" id="boldCheckbox" ref={this.hide} hidden={false} onClick={this.makeBold.bind(this)}/>
<button id="decreaseButton" ref={this.hide} hidden={false}>-</button>
<span id="fontSizeSpan" ref={this.hide} hidden={false}>{this.props.size}</span>
<button id="increaseButton" ref={this.hide} hidden={false} onClick={this.changeSize.bind(this)}>+</button>
<span id="textSpan" ref={this.checkInput} onClick={this.toggle.bind(this)}>{this.props.text}</span>
</div>
);
}
right now their hidden attribute is false so I can see them.Here's the html which is not much:
<div id='container'></div>
<script type="text/jsx">
ReactDOM.render(
<div>
<FontChooser min='4' max='40' size='16' text='You can change me!' bold='false'/>
</div>,
document.getElementById('container'))
;
</script>
So far all I have managed is for the browser console(I'm using Firefox react component addon) to confirm there is a functioning event that doesn't really work, as in when I click the text, the buttons or the input checkbox the props does change to false or true every click but that's about it.
I appreciate it if someone could guide me through this.
NOTE:
just in case nothing is imported, also I setup a local server with Nodejs
Here is an Example of what you want: https://codesandbox.io/s/mystifying-cookies-v7w3l?file=/src/App.js
Basically, I have 4 variables: text, fontWeight, fontSize and showTools.
Each button has its own task and also you can select if show or not.
In React you don't have to care about ids like in older frameworks. You can generate the elements just in the place where you are with the information which you need. So, basically, we have the 4 variables and use them wisely where we want (as styles props, as text and even as a conditional to show components). It's the magic of React and JSX.
In the code I've use hooks, part of the latest definition of React. For that my Components is functional and not a Class. it makes it easier and faster for examples and prototyping.
The tools are show by default just to let you play with it
import React from "react";
import "./styles.css";
export default function App() {
const [text, setText] = React.useState("");
const [boldFont, setBoldFont] = React.useState(false);
const [fontSize, setFontSize] = React.useState(14);
const [showTools, setShowTools] = React.useState(true);
return (
<div className="App">
<div
style={{
fontWeight: boldFont ? "bold" : "normal",
fontSize: `${fontSize}px`
}}
>
<span onClick={() => setShowTools(!showTools)}>
{text || "Text Example"}
</span>
</div>
{showTools && (
<div>
<button onClick={() => setBoldFont(!boldFont)}>Bold</button> |
<button onClick={() => setFontSize(fontSize + 1)}>A+</button>
<button onClick={() => setFontSize(fontSize - 1)}>a-</button>
<input
type="text"
value={text}
onChange={event => {
setText(event.target.value);
}}
/>
</div>
)}
</div>
);
}
This my code sandbox example:
https://codesandbox.io/s/react-hooks-counter-demo-kevxp?file=/src/index.js
My problem is:
The list will always rerendering on every state change inside the page so the scroll will always back to the top. I want to know why this happen, and how to prevent this behaviour even the state of the list have changes then keep the last scroll position of the list
Every time App renders, you are creating a brand new definition for the Example component. It may do the same thing as the old one, but it's a new component. So react compares the element from one render with the element of the next render and sees that they have different component types. Thus, thus it is forced to unmount the old one and mount the new one, just as it would if you changed something from a <div> to a <span>. The new one begins scrolled to 0.
The solution to this is to create Example only once, outside of App.
const Example = props => (
<List
className="List"
height={80}
itemCount={props.propsAbc.length}
itemSize={20}
width={300}
itemData={{
dataAbc: props.propsAbc
}}
>
{({ index, style, data }) => (
<div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
{data.dataAbc[index]}
</div>
)}
</List>
);
function App() {
const [count, setCount] = useState(0);
let [dataArray, setDataArray] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
return (
<div className="App">
<h1>Scroll down the blue box, then click the button</h1>
<h2>You clicked {count} times!</h2>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(count + 1)}>Increment</button>
<div
style={{ maxHeight: "80px", overflow: "äuto", background: "lightblue" }}
>
<Example propsAbc={dataArray} />
</div>
</div>
);
}
https://codesandbox.io/s/react-hooks-counter-demo-qcjgj
I don't think it's a react window problem.
A react component re-renders because there's a state change. In this case, the state change is caused by setCount (when you click the increment button), which re-renders the entire component including Example.
If Example is its own component, the scroll position won't get refreshed, because it no longer depends on the count state.
A working sample here:
https://codesandbox.io/s/react-hooks-counter-demo-hbek7?file=/src/index.js