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.
Related
I am using react. I am able to generate a tree from xml file. Currently treeview is showing as an unordered list. How to replace that with images?
Please find my sandbox: https://codesandbox.io/s/falling-waterfall-2irybs
The code is working fine in Visual Studio. I am not sure how to use sandbox, not able to understand sandbox error.
Images are also included in the Sandbox.
On page load tree is collapsed showing service and sales two nodes with PLUS image.
when node is open, image will convert into minus image. After expand, all opened nodes will show minus image except the last one. Last node will show paper image.
Please help in fixing my treeview code.
Thanks a lot.
You would need to re-style the li element's ::marker selector. If you are open to using a 3rd-party css-in-JS solution then I would suggest using the styled-components package.
Here's an example implementation:
import styled from "styled-components";
import plus from "./plus.gif";
import minus from "./minus.gif";
import paper from "./paper.gif";
const StyledLI = styled.li`
::marker {
content: url(${({ expanded, isLeaf }) =>
isLeaf ? paper : expanded ? minus : plus});
}
`;
Pass expanded and isLeaf props to the new styled component when rendering.
class TreeNode extends React.Component {
render() {
const { node, onToggle } = this.props;
const activeChildren =
node.isActive && node.nodes.length ? (
<ul>
{node.nodes.map((node) => (
<TreeNode
id={node.id}
key={node.key_id}
node={node}
onToggle={onToggle}
/>
))}
</ul>
) : null;
return (
<StyledLI
id={node.id}
expanded={node.isActive} // <-- expanded if active
isLeaf={!node.nodes.length} // <-- leaf node if no children nodes
linkpagename={node.linkpagename}
key={node.key_id}
onClick={(event) => {
event.stopPropagation();
onToggle(node);
}}
>
<Link
to={node.linkpagename}
style={{ textDecoration: "none", color: "#000000" }}
>
{node.description}
</Link>{" "}
- {node.key_id} - {node.linkpagename}
{activeChildren}
</StyledLI>
);
}
}
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 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.
I have a column list of elements, and each of them has hidden div, I want to show hidden element on click, but when I'm clicking another div, I want to hide current and show the one I clicked last, how can I do it correctly?
You could have two specific classes, one that hides element and one that shows element. when clicking on the element you can use jQuery or JavaScript to toggle the class that shows element for the hidden div of that specific element and hide everything for any other div.
The component you're rendering could take in an active prop and only render the second div if this prop is true.
With this, you could then keep track of which element is selected in the parent.
I threw together a working example with very simple content
import React, { useState } from 'react';
const ListItem = ({id, isActive, handleClick}) => {
return (
<div id={id} onClick={handleClick}>
Here is the element
{!!isActive && <div>I am the selected element</div>}
</div>
);
};
export const List = () => {
const [selectedItem, setSelectedItem] = useState();
const items = ['one', 'two', 'three'];
const handleClick = (event) => {
setSelectedItem(event.target.id);
}
return (
<div>
{items.map(item => (
<ListItem id={item} isActive={item===selectedItem} handleClick={handleClick}/>
))}
</div>
)
}
I have a list, build with material-ui. There are a lot of items in it, so scrollbar is visible.
What I would like to do is scroll to the selected item. Have any ideas of how to implement this?
Here is a demo sendbox link
After click on the item list should looks like this (selected item is in the center):
I know there is an accepted answer here, but I think using
<ListItem autoFocus={true}/>
would scroll that list item into view. The same logic to set the list item to be marked as selected could be used for setting the autoFocus attribute as well.
Hold a ref to the List, and upon click on ListItem, calculate how much you need to scroll based on:
list item height
the index of the selected item
number of visible list items.
const scrollableListRef = React.createRef();
function Row(props) {
const { index, style } = props;
const placeSelectedItemInTheMiddle = (index) => {
const LIST_ITEM_HEIGHT = 46;
const NUM_OF_VISIBLE_LIST_ITEMS = 9;
const amountToScroll = LIST_ITEM_HEIGHT * (index - (NUM_OF_VISIBLE_LIST_ITEMS / 2) + 1) ;
scrollableListRef.current.scrollTo(amountToScroll, 0);
}
return (
<ListItem button style={style} key={index}
onClick={() => {placeSelectedItemInTheMiddle(index)}}>
<ListItemText primary={`Item ${index + 1}`} />
</ListItem>
);
}
Chandra Hasa's answer should include a bit of extra explanation but the edit queue was full.
Material-ui has a built-in way to do this with the autoFocus prop. See the table below taken from the MUI Docs.
Prop
Type
Default
Description
autoFocus
boolean
false
If true, the list item is focused during the first mount. Focus will also be triggered if the value changes from false to true.
❗️ The autoFocus prop should only be true for one item in the list. If you add autoFocus={true} to every ListItem, it will continuously scroll to each element, like OP mentioned in his comment.
The browser will scroll to the one ListItem that has autoFocus={true}.
If you want to always scroll to the same ListItem every time the component renders, you can add autoFocus={true} to that one ListItem component, like this:
<List>
<ListItem>item1</ListItem>
<ListItem autoFocus={true}>The list will automatically scroll to this item</ListItem>
<ListItem>item3</ListItem>
</List>
In most cases, people want to dynamically determine which ListItem to scroll to. In this case, the value of the autoFocus prop needs to evaluate to a boolean. It should still only be true for one item in the list.
One use case is if you're rendering a long list with one option already selected. It's good to automatically scroll so the selected option is visible.
/* The list will auto-scroll to the **one** item that has an
`autoFocus` prop that evaluates to `true`.
If `selectedItem === item2`,
the list will automatically scroll to `item2`.
*/
<List>
<ListItem autoFocus={ selectedItem === item1 }> item1 </ListItem>
<ListItem autoFocus={ selectedItem === item2 }> item2 </ListItem>
<ListItem autoFocus={ selectedItem === item3 }> item3 </ListItem>
</List>
❗️ This question and answers use MUI v4. MUI v5 deprecated the autoFocus prop for ListItem components. Use ListItemButton instead. Don't worry, it doesn't look like an MUI Button component.
Deprecated - checkout ListItemButton instead (Source: MUI Docs)