I am having a problem with React state changes accros two different components
The Parent Class has the state of the commands and the functions that change the state
constructor(props) {
super(props);
this.state = {
expanded: false,
addLock: false,
pendingCommand: undefined,
commands: [],
defCommands: [],
};
render() {
return (
<div>
<this.Commands/>
</div>
);
}
The two different components are Material ui Custom Accordions in tabs
Commands = () => {
const tabs = [
<CommandAccordion
cmd={this.state.defcommands}
expan={this.state.expanded} addLock={this.state.addLock}
hdlICh={this.handleInputChange}/>,
<CommandAccordion
cmd={this.state.commands} pendCmd={this.state.pendingCommand}
expan={this.state.expanded} addLock={this.state.addLock}
hdlICh={this.handleInputChange}/>]
}
The Accordion maps through the commands like this:
const commands = props.cmd;
const pendingCommand = props.pendCmd;
{[...pendingCommand === undefined ? [] : [pendingCommand], ...commands].map((obj, index) => (
<CstmAccordion expanded={expanded === obj._id} onChange={handleChange(obj._id, index)}
onClick={() => customOnChange(obj)}
key={'accordion-defcommands-' + index}>
<CstmAccordionDetails>
<Tabs value={tabId}>
<StyledTab label="Settings"/>
<StyledTab label="Advanced settings"/>
</Tabs>
<TabPanel value={tabId} Id={0} index={index} obj={obj} funcs={funcs}
commandV={commandValue}/>
<TabPanel value={tabId} Id={1} index={index} obj={obj} funcs={funcs}
commandV={commandValue}/>
Now the tab pannel has its own state which is set at the beginning and is used in a text field as value
const [commandValue, setCommandValue] = React.useState(obj.command);
<TextField
value={commandValue}
onBlur={funcs.handleInputChange(index, 'command')}
onSelect={() => {
funcs.handleOnFieldSelect(index)
}}
helperText={`What triggers the response? ${!commandValue ? 0 : commandNameValue.length}/40`}
onChange={(event) => {
if (event.target.value.length <= 40)
setCommandValue(event.target.value);
}}/>
Now the problem is if I first load everything it opens the tab for the default command in the parent component and everything looks fine. But if I change the tab to the custom commands, the first command in the Custom-Command-Tab has the same value like the first command in the Default-Command-Tab. (With Tabs I mean const tabs =[... in Commands = () =>{ and not <TabPanel .../>)
And if I change the value in the custom command, save it and change back to the default tab now the default commands have the value of the custom commands.
Also it might be important to now that value only syncs with the other commands with the same index. Which means the second command in the second tab has its own value and not the same value like the command before it - but is still synced with the second command in the first tab.
I think it might have something to do with the order in which the comopnents are rendered but I dont konw.
I've tried managing the state in the Accordion bevore the commands.map function and not in the TabPanel, and changing the state everytime the Accordion is clicked, and giving the TabPanel the commandValue and SetCommandValue and this works in terms of the sync problem, but with that the input into the TextField and has a 2 second input delay per char at worst.
I hope you can understand my problem even if it's a bit much.
Related
I have a map with 5 markers on it and accordion with 5 elements on the side. Each marker has a corresponding accordion.
I want to click on a marker and expand the corresponding accordion. Both accordion and markers on the map have the same key values (screenshot below).
I use Map() function to generate accordion as well as markers. Simplified code looks something like this:
function Markers() {
const map = useMap();
return (
loc.map(loc => {
return (
<Marker
icon={locationMarker}
key={loc.properties.id}
position={[loc.properties.y, loc.properties.x]}}}>
</Marker>
)
})
)
}
export default Markers
function LocationCard() {
return (
<Container>
{loc.map(loc => (
<Accordion key={loc.properties.id}>
<AccordionSummary>
<Typography> {loc.properties.title} </Typography>
</AccordionSummary>
<AccordionDetails>
<Typography> { loc.properties.description } </Typography>
</AccordionDetails>
</Accordion>
))
}
</Container>
);
}
export default LocationCard
I am basically looking for a functionality "On marker click, expand accordion".
Any idea how I can achieve this with my current setup?
Thanks
An accordion will internally store its own state (an uncontrolled component) to track if it's expanded but you can override it, making it a controlled component.
Go up the component tree of your markers + accordions to find the lowest common intersection where we can store some state and pass it down to both components.
Store the selected marker ID (or an array if you want to have many accordions open at once) and an event handler function to update the state when a marker is pressed.
const [selectedMarkerID, setSelectedMarkerID] = useState<number | undefined>();
const handleMarkerPressed = (id: number) => () => {
// If a marker is pressed a second time, close the accordion
setSelectedMarkerID(selectedMarkerID !== id ? id : undefined);
}
return (
<>
<MyMarkers markers={markers} handleMarkerPressed={handleMarkerPressed />
<MyAccordions markers={markers} selectedMarkerID={selectedMarkerID} />
</>)
Then in your markers.map function, set the expanded property like the following:
<Accordion
key={loc.properties.id}
expanded={selectedMarkerID}>
// If you want to disable opening of accordions, override the onClick
onClick={undefined}
>
And your marker should look something like:
<Marker
...
onClick={handleOpenMarker(marker.id)}
/>
An alternative to prop drilling the state + markers + event handler down both components would be some kind of store or context API
More info available in the MUI accordion docs
Edit
Here's a Codebox example of passing the state + event handler around to manage which accordions are expanded: https://codesandbox.io/s/intelligent-feather-x9grwv?file=/src/App.tsx
The goal is to keep the markers in sync with the accordions, to summarise:
The accordion state is hoisted up a level to the App.tsx component
The marker event handler + accordion event handler is passed down from App.tsx
When I collapse a TreeItem, I want all it's descendants TreeItems (it's children, their children, etc.) that are open to collapse too. At the existing state they do disappear, but the next time I expand the parent TreeItem they are expanded as well.
The TreeView is "uncontrolled" by default meaning that the component will handle the state for you, and you as a consumer get whatever default behavior MUI set. In order to achieve what you want you'll need to make the TreeView a "controlled" component. There is an example of using the controlled component here: https://mui.com/components/tree-view/#controlled-tree-view
The example on the MUI page is doing more than just handling the expanding/collapsing of nodes. In the simplest form you need to explicitly handle the "expanded" prop that gets passed into the TreeView yourself.
const Tree = () => {
// nodes "1", "1.1", "2" will start expanded
const [expanded,setExpanded] = useState(["1","1.2","2"]);
const createHandler = (id) => () => {
// if node was collapsed, add to expanded list
if(!expanded.includes(id)) {
setExpanded([...expanded,id]);
} else {
// remove clicked node, and children of clicked node from expanded list
setExpanded(expanded.filter(i => i !== id && !i.startsWith(`${id}.`));
}
};
return (
<TreeView expanded={expanded}>
<TreeItem nodeId="1" label="A" onClick={createHandler("1")}>
<TreeItem nodeId="1.1" label="B" />
<TreeItem nodeId="1.2" label="C" onClick={createHandler("1.2")}>
<TreeItem nodeId="1.2.1" label="D" />
</TreeItem>
</TreeItem>
<TreeItem nodeId="2" label="E" onClick={createHandler("2")}>
<TreeItem nodeId="2.1" label="F" />
</TreeItem>
</TreeView>
);
}
Note: there be some mistakes in the above example, I'm unable to run it at the moment to verify correctness.
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 building a conference website using three of these tabs (#speaker, #talks, #schedule). I think it is fair to want interactions between the tabs, here are a couple use cases that I cannot seem to solve.
From the #talks tab, I click on the bio hash - #johnsmith. This id exists within the page, but since I don't first switch tab to #speakers, nothing renders.
If I want to reference a specific talk and email someone the url: https://website.com#speaker_name the tabs won't open, and nothing but the tabs render.
The problem is compounded by the fact that when I click on an anchor tag href using a '#id', I must reload the page for it to fire.
I feel like there should be some way to pass a parameter when changing the tab or something... I'm in a tough spot because I'm rolling out code, but need this functionality badly.
Here is the actual open-source repo - https://github.com/kernelcon/website. The code I'm referencing can be found in src/pages/Agenda/.
Here is some example code.
Agenda.js
<Tabs defaultTab={this.state.defaultTab}
onChange={(tabId) => { this.changeTab(tabId) }}
vertical={vert}>
<TabList vertical>
<Tab tabFor="speakers">Speakers</Tab>
<Tab tabFor="talks">Talks</Tab>
<span>
<TabPanel tabId="speakers">
<Speakers />
</TabPanel>
<TabPanel tabId="talks">
<Talks />
</TabPanel>
</span>
</Tabs>
Talks.js
changeTab(id) {
window.location.reload(false);
}
getTalks() {
// Order Alphabetically
const talksOrdered = speakerConfig.sort((a,b) => (a.title > b.title) ? 1 : ((b.title > a.title) ? -1 : 0));
const talks = talksOrdered.map((ele, idx) => {
const twitterUrl = ele.twitter.replace('#', '');
return (
<div id={ele.talk_id}
key={idx}
className='single-talk'>
<div className='talk-title'>{ele.title}</div>
<div className='talk-sub-title'>
<div className='speaker-name'>
<a onClick={() => {this.changeTab(ele.speaker_id)}}
href={`#${ele.speaker_id}`}>{ele.speaker}</a>
</div>
...
I ended up accomplishing this by sending #tab_title/speaker_name, then adding a componentWillMount lifecycle method and function in the main tab file like below.
componentWillMount() {
const defaultTab = this.props.location.hash ? this.props.location.hash.split('#')[1] : 'schedule';
this.setState({
defaultTab: defaultTab
});
this.handleHashChange();
window.addEventListener('hashchange', this.handleHashChange);
}
handleHashChange = () => {
// given `#speakers/dave` now you have tabName='speakers', speakerHash='dave'
const [tabName, speakerHash] = window.location.hash.replace('#', '').split('/');
const tabNamesToWatchFor = [
'schedule',
'speakers'
];
if (tabNamesToWatchFor.includes(tabName)) {
this.setState({
defaultTab: tabName,
// pass this.state.speakerHash to <Speakers/> and use this for scrollIntoView in componentDidMount
speakerHash: speakerHash
});
}
}
Next, I went to the individual tab (in this case Speakers.js) and added a componentDidMount and componentDidUpdate method to help scroll to the speaker itself.
componentDidMount() {
this.handleScrollToSpeaker(this.props.speakerHash);
}
componentDidUpdate(prevProps) {
if (prevProps.speakerHash !== this.props.speakerHash) {
this.handleScrollToSpeaker(this.props.speakerHash);
}
}
handleScrollToSpeaker = hash => {
window.setTimeout(() => {
const ele = document.querySelector(`#${hash}`);
if (ele) {
ele.scrollIntoView({ block: 'start', behavior: 'smooth' });
}
}, 500)
}
I have 2 toggle components that show/hide panels under them which is working fine. However, If I toggle the second panel I need to make sure the first panel is off. Same with the first, I don't want the two panels toggled open at once.
Here is a basic example of what I have.
<ToggleControl
label={__( 'Toggle One' )}
checked={ toggleOne }
onChange={ () => this.props.setAttributes({ toggleOne: ! toggleOne }) }
/>
{ toggleOne ?
<PanelBody>
... Show Panel 1 Stuff
</PanelBody>
: null }
<ToggleControl
label={__('Add Image Divider')}
checked={toggleTwo}
onChange={() => this.props.setAttributes({ toggleTwo: !toggleTwo })}
/>
{ toggleTwo ?
<PanelBody>
... Show Panel 2 Stuff
</PanelBody>
: null }
I can toggle the other panel inside the onChange() of the other toggle by doing this...
onChange={() => this.props.setAttributes({ toggleTwo: !toggleTwo, toggleOne: !toggleOne })}
But I don't want to toggle it if its already off and can't seem to figure it out.
Thanks.
To turn off a panel, you can just set the toggle variable to false.
To turn off the first panel when toggling the second panel, you can do the following:
onChange={() => this.props.setAttributes({ toggleTwo: !toggleTwo, toggleOne: false })}
Similarly, to turn off the second panel when toggling the first panel, you can do the following:
onChange={() => this.props.setAttributes({ toggleOne: !toggleOne, toggleTwo: false })}
This will make sure that when toggling one panel, the other one is always off, so that you will not have both panels toggled on at the same time.
[Edit: Props are read-only, and while my original answer and the accepted answer were identical and do the trick, I'm adjusting my answer to show the "proper" way to do it through manipulating state, not props.]
I don't know why you're tracking the changes on this.props instead of this.state, but here is one way:
To be added in constructor:
this.state = { toggleOne: this.props.toggleOne, toggleTwo: this.props.toggleTwo }
For Toggle One:
onChange={ () => this.setState({ toggleOne: !this.state.toggleOne, toggleTwo: !this.state.toggleOne ? false : this.state.toggleTwo }) }
For Toggle Two:
onChange={ () => this.setState({ toggleTwo: !this.state.toggleTwo, toggleOne: !this.state.toggleTwo ? false : this.state.toggleOne }) }
And of course set the checked attributes to this.state.toggledOne/Two.
Logic:
Set ourselves to our opposite state.
If our current state is off (false) and we're toggled on (!false), turn our neighbor off (false); if we're switching from on to off, leave our neighbor where they are (which in most cases will be off because we were just on).
My additional true/false check at the end vs. the default of false in the accepted answer is meant to catch the case that both toggles were opened on purpose (i.e. programmatically). This may not ever be the case for your scenario, but for others looking for a similar solution who may want this feature, there you have it.