ukrbublik / react-awesome-query-builder

User-friendly query builder for React
https://ukrbublik.github.io/react-awesome-query-builder
MIT License
2.01k stars 498 forks source link

Please add MaterialFieldCascader #320

Open i19s-frank opened 4 years ago

i19s-frank commented 4 years ago

Is your feature request related to a problem? Please describe. I have to migrate from antd to material but am strongly relying on the FieldCascader.

Describe the solution you'd like Implement a MaterialFieldCascader.

Describe alternatives you've considered I am currently investiganting whether I can implement it myself.

i19s-frank commented 4 years ago

Hello Denis,

I have implemented a solution fit to our needs. Since I don't have the time to adapt it into a meaningful PR I wanted to at least share the idea the solution is based on.

Since Material doesn't provide a component like the antd field selector, I went with a List with sticky Headers.

The code is in TS but you get the idea:


interface FieldCascaderProps extends FieldProps {
  selectedField: string | Empty,
  parentField: string | Empty,
}

interface Section {
  path: string,
  label: string,
  fields: FieldItem[],
}

const MaterialFieldCascader = (props: FieldCascaderProps) => {
  const {
    items, placeholder, selectedKey, setField
  } = props;
  const useStyles = makeStyles((theme: Theme) =>
      createStyles({
        root: {
          width: '100%',
          maxWidth: 360,
          backgroundColor: theme.palette.background.paper,
          position: 'relative',
          overflow: 'auto',
          maxHeight: 300,
        },
        listSection: {
          backgroundColor: 'inherit',
        },
        ul: {
          backgroundColor: 'inherit',
          padding: 0,
        },
        nested: {
          paddingLeft: theme.spacing(4),
        },
        fieldSelectButton: {}
      }),
  );

  const classes = useStyles();
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);

  const handleClick = (event: React.MouseEvent<any>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  const onFieldSeleced = (path: string) => {
    setField(path);
  }

  const open = Boolean(anchorEl);
  const id = open ? 'material-field-cascader-popover' : undefined;
  const sections: Section[] = [];

  let selectedTemp: string = placeholder!;

  for (let i in items) {
    let item = items[i];

    if (!item.items) {
      throw new Error("Cannot handle leaves in the root");
    }
    if (!item.path) {
      throw new Error("Path is missing!");
    }
    let section = {
      path: item.path, fields: [], label: item.label!
    };
    sections.push(section);
    handleChildren(item.items, sections, section);
  }

  function handleChildren(children: FieldItems, sections: Section[], parent: Section) {
    for (let i in children) {
      let child = children[i];
      // this is a leaf
      if (!child.items) {
        if (child.path === selectedKey) {
          selectedTemp = parent.label + " / " + child.label;
        }
        parent.fields.push(child)
      } else {
        let section = {
          path: child.path!, fields: [], label: parent.label + " / " + child.label
        };
        sections.push(section);
        handleChildren(child.items, sections, section);
      }
    }
  }

  const selected = selectedTemp;

  return <div>
    <Button aria-describedby={id} variant="text" className={classes.fieldSelectButton}
            onClick={handleClick}>
      {selected}
    </Button>
    <Popover
        id={id}
        open={open}
        anchorEl={anchorEl}
        onClose={handleClose}
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'center',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'center',
        }}
    >

      <List className={classes.root} subheader={<li/>}>
        {sections.map((section) => (
            <li id={`section-${section.path}`} key={`section-${section.path}`}
                className={classes.listSection}>
              <ul className={classes.ul}>
                <ListSubheader>{`${section.label}`}</ListSubheader>
                {section.fields.map((field) => (
                    <ListItem id={`item-${field.path}`} key={`item-${field.path}`}
                              className={classes.nested} onClick={(e) => {
                      onFieldSeleced(field.path!);
                      handleClose();
                    }}>
                      <ListItemText primary={`${field.label}`}/>
                    </ListItem>
                ))}
              </ul>
            </li>
        ))}
      </List>
    </Popover>
  </div>;
}
export default MaterialFieldCascader;

The implementation obviously isn't complete.

Ognian commented 3 years ago

Any news on this?

ukrbublik commented 3 years ago

I'll be working on features for MUI soon. It would be great if someone open PR with code above

ukrbublik commented 3 years ago

https://github.com/mui-org/material-ui/pull/20591

ukrbublik commented 3 years ago

https://codesandbox.io/s/material-ui-nested-menu-item-example-b25j6?file=/src/App.tsx