По умолчанию открывается складное меню на основе идентификатора - PullRequest
8 голосов
/ 10 марта 2020

Я создаю вложенное меню и подменю, и все уже сделано на данный момент .. Теперь мне нужно сделать так, чтобы это разборное меню открывалось по умолчанию на основе заданного идентификатора ..

Вы также можете взглянуть на полный фрагмент рабочего кода ниже,

const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);

const openMenuId = "3.1.1.1";

const {Component, Fragment} = React;
const {Button, Collapse, Input} = Reactstrap;

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {menuItems: []};
  }

  render() {
    return <MenuItemContainer menuItems={this.state.menuItems} />;
  }

  componentDidMount() {
    loadMenu().then(menuItems => this.setState({menuItems}));
  }
}

function MenuItemContainer(props) {
  if (!props.menuItems.length) return null;
  
  const renderMenuItem = menuItem =>
    <li key={menuItem.id}><MenuItem {...menuItem} /></li>;
    
  return <ul>{props.menuItems.map(renderMenuItem)}</ul>;
}
MenuItemContainer.defaultProps = {menuItems: []};

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: false};
    this.toggle = this.toggle.bind(this);
  }

  render() {
    let isLastChild = this.props.children ? false : true;
    return (
      <Fragment>
        <Button onClick={this.toggle}>{this.props.name}</Button>
        <Fragment>
          {isLastChild ? <Input type="checkbox" value={this.props.id} /> : ''}
        </Fragment>
        <Collapse isOpen={this.state.isOpen}>
          <MenuItemContainer menuItems={this.props.children} />
        </Collapse>
      </Fragment>
    );
  }

  toggle() {
    this.setState(({isOpen}) => ({isOpen: !isOpen}));
  }
}

ReactDOM.render(<Menu />, document.getElementById("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>

<div id="root"></div>

Требование:

У меня есть значение id, хранящееся в const openMenuId = "3.1.1.1.1"; в родительском компоненте (вы можете посмотреть эта переменная ниже loadMenu переменная массива) ..

Несмотря на то, что есть несколько подменю, этот идентификатор будет принадлежать только дочернему идентификатору последнего уровня и, следовательно, будет иметь флажок наверняка, так что флажок должен быть установлен и меню до родительского уровня должны быть открыты.

Например,

Поскольку openMenuId равен "3.1.1.1.1", и, следовательно, ясно, что последний дочерний уровень меню three, которое является Three - one - one - one - one, должно быть проверено как openMenuId, и значение флажка здесь совпадает. Затем соответствующие меню и подменю должны быть расширены до последнего уровня.

Это только для поведения по умолчанию на посещаемой странице, поэтому после этого пользователь может свернуться назад и иметь возможность проверить любые другие флажки в любых других меню. Но при посещении страницы у меня будет определенный идентификатор, который должен открываться по умолчанию, а также нуждается в чтобы быть проверенным в флажке ..

Пожалуйста, помогите мне добиться результата открытия соответствующего меню, сравнивая идентификатор, переданный в качестве реквизита, и сделайте соответствующее меню отмеченным ..

Борьба за Давно, пожалуйста, помогите мне .. Большое спасибо заранее ..

Ответы [ 4 ]

3 голосов
/ 18 марта 2020

Какой замечательный вопрос! Мне очень понравилось придумывать решение для этого.

Поскольку вы хотели задать начальное состояние как для состояния меню, так и для состояния флажка, я думаю, что управление состоянием обоих на уровне <Menu> (или даже выше!) хорошая идея. Это не только облегчает определение начального состояния от родителя, но также предоставляет вам большую гибкость, если вам понадобится более сложное поведение меню или флажков в будущем.

Поскольку структура меню рекурсивна Я думаю, что рекурсивная структура состояния меню работает довольно хорошо. Прежде чем я go в коде, вот небольшой GIF, который, я надеюсь, поможет объяснить, как выглядит состояние:

Демо

Video showing menu three columns: the menu, the menu state as JSON, and the checkbox state as JSON. As menus and checkboxes are clicked, the states update.

Вот фрагмент игровой площадки:

const loadMenu = () =>
  Promise.resolve([
    {
      id: "1",
      name: "One",
      children: [
        {
          id: "1.1",
          name: "One - one",
          children: [
            { id: "1.1.1", name: "One - one - one" },
            { id: "1.1.2", name: "One - one - two" },
            { id: "1.1.3", name: "One - one - three" }
          ]
        }
      ]
    },
    { id: "2", name: "Two", children: [{ id: "2.1", name: "Two - one" }] },
    {
      id: "3",
      name: "Three",
      children: [
        {
          id: "3.1",
          name: "Three - one",
          children: [
            {
              id: "3.1.1",
              name: "Three - one - one",
              children: [
                {
                  id: "3.1.1.1",
                  name: "Three - one - one - one",
                  children: [
                    { id: "3.1.1.1.1", name: "Three - one - one - one - one" }
                  ]
                }
              ]
            }
          ]
        }
      ]
    },
    { id: "4", name: "Four" },
    {
      id: "5",
      name: "Five",
      children: [
        { id: "5.1", name: "Five - one" },
        { id: "5.2", name: "Five - two" },
        { id: "5.3", name: "Five - three" },
        { id: "5.4", name: "Five - four" }
      ]
    },
    { id: "6", name: "Six" }
  ]);

const { Component, Fragment } = React;
const { Button, Collapse, Input } = Reactstrap;

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      menuItems: [],
      openMenus: {},
      checkedMenus: {}
    };
    this.handleMenuToggle = this.handleMenuToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const { menuItems, openMenus, checkedMenus } = this.state;

    return (
      
        
          
        
        
          Menu state
          {JSON.stringify(openMenus, null, 2)}

Флажок состояния

{JSON.stringify(checkedMenus, null, 2)}
); } componentDidMount () {const {initialOpenMenuId, initialCheckedMenuIds} = this.props; loadMenu (). then (menuItems => {const initialMenuState = {}; this.setState ({menuItems, openMenus: expandNodeById (initialMenuState, initialOpenMenuId), checkedMenus: initialCheckedMenuIds.reduce ((a cc) (val = ... a cc, [val]: true}), {})});}); } handleMenuToggle (toggledId) {this.setState (({openMenus}) => ({openMenus: toggleNodeById (openMenus, toggledId)}))); } handleChecked (toggledId) {this.setState (({checkedMenus}) => ({checkedMenus: {... checkedMenus, [toggledId]: checkedMenus [toggledId] ?expandedNode (): extendedNode ()}})); }} function MenuItemContainer ({openMenus, onMenuToggle, checkedMenus, onChecked, menuItems = []}) {if (! menuItems.length) return null; const renderMenuItem = menuItem => ( ); return
; } класс MenuItem extends Component {constructor (props) {super (props); this.handleToggle = this.handleToggle.bind (this); this.handleChecked = this.handleChecked.bind (this); } render () {const {children, name, id, openMenus, onMenuToggle, checkedMenus, onChecked} = this.props; const isLastChild =! children; return ( {name} {isLastChild && ( )} ); } handleToggle () {this.props.onMenuToggle (this.props.id); } handleChecked () {this.props.onChecked (this.props.id); }} ReactDOM.render ( , document.getElementById ("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>

<div id="root"></div>

Ответ

Кодовое пошаговое руководство ниже.

const loadMenu = () =>
  Promise.resolve([
    {
      id: "1",
      name: "One",
      children: [
        {
          id: "1.1",
          name: "One - one",
          children: [
            { id: "1.1.1", name: "One - one - one" },
            { id: "1.1.2", name: "One - one - two" },
            { id: "1.1.3", name: "One - one - three" }
          ]
        }
      ]
    },
    { id: "2", name: "Two", children: [{ id: "2.1", name: "Two - one" }] },
    {
      id: "3",
      name: "Three",
      children: [
        {
          id: "3.1",
          name: "Three - one",
          children: [
            {
              id: "3.1.1",
              name: "Three - one - one",
              children: [
                {
                  id: "3.1.1.1",
                  name: "Three - one - one - one",
                  children: [
                    { id: "3.1.1.1.1", name: "Three - one - one - one - one" }
                  ]
                }
              ]
            }
          ]
        }
      ]
    },
    { id: "4", name: "Four" },
    {
      id: "5",
      name: "Five",
      children: [
        { id: "5.1", name: "Five - one" },
        { id: "5.2", name: "Five - two" },
        { id: "5.3", name: "Five - three" },
        { id: "5.4", name: "Five - four" }
      ]
    },
    { id: "6", name: "Six" }
  ]);

const { Component, Fragment } = React;
const { Button, Collapse, Input } = Reactstrap;

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      menuItems: [],
      openMenus: {},
      checkedMenus: {}
    };
    this.handleMenuToggle = this.handleMenuToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const { menuItems, openMenus, checkedMenus } = this.state;

    return (
      <MenuItemContainer
        openMenus={openMenus}
        menuItems={menuItems}
        onMenuToggle={this.handleMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={this.handleChecked}
      />
    );
  }

  componentDidMount() {
    const { initialOpenMenuId, initialCheckedMenuIds } = this.props;

    loadMenu().then(menuItems => {
      const initialMenuState = {};
      this.setState({
        menuItems,
        openMenus: expandNodeById(initialMenuState, initialOpenMenuId),
        checkedMenus: initialCheckedMenuIds.reduce(
          (acc, val) => ({ ...acc, [val]: true }),
          {}
        )
      });
    });
  }

  handleMenuToggle(toggledId) {
    this.setState(({ openMenus }) => ({
      openMenus: toggleNodeById(openMenus, toggledId)
    }));
  }

  handleChecked(toggledId) {
    this.setState(({ checkedMenus }) => ({
      checkedMenus: {
        ...checkedMenus,
        [toggledId]: checkedMenus[toggledId] ? unexpandedNode() : expandedNode()
      }
    }));
  }
}

function MenuItemContainer({
  openMenus,
  onMenuToggle,
  checkedMenus,
  onChecked,
  menuItems = []
}) {
  if (!menuItems.length) return null;

  const renderMenuItem = menuItem => (
    <li key={menuItem.id}>
      <MenuItem
        openMenus={openMenus}
        onMenuToggle={onMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={onChecked}
        {...menuItem}
      />
    </li>
  );

  return <ul>{menuItems.map(renderMenuItem)}</ul>;
}

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.handleToggle = this.handleToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const {
      children,
      name,
      id,
      openMenus,
      onMenuToggle,
      checkedMenus,
      onChecked
    } = this.props;

    const isLastChild = !children;
    return (
      <Fragment>
        <Button onClick={isLastChild ? this.handleChecked : this.handleToggle}>
          {name}
        </Button>
        {isLastChild && (
          <Input
            addon
            type="checkbox"
            onChange={this.handleChecked}
            checked={!!checkedMenus[id]}
            value={id}
          />
        )}

        <Collapse isOpen={openMenus ? !!openMenus[id] : false}>
          <MenuItemContainer
            menuItems={children}
            // Pass down child menus' state
            openMenus={openMenus && openMenus[id]}
            onMenuToggle={onMenuToggle}
            checkedMenus={checkedMenus}
            onChecked={onChecked}
          />
        </Collapse>
      </Fragment>
    );
  }

  handleToggle() {
    this.props.onMenuToggle(this.props.id);
  }

  handleChecked() {
    this.props.onChecked(this.props.id);
  }
}

ReactDOM.render(
  <Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />,
  document.getElementById("root")
);
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>

<div id="root"></div>

Пошаговое руководство

Прежде чем начать, я должен сказать, что позволил себе сменить код для использования современного JavaScript такие функции, как деструктуризация объекта , деструктуризация массива , остальные и значения по умолчанию .

Создание состояния

Итак. Поскольку идентификаторы пунктов меню - это числа, разделенные точкой, мы можем воспользоваться этим при построении состояния. Состояние по сути является древовидной структурой, где каждое подменю является дочерним по отношению к его родительскому элементу, а конечный узел («последнее меню» или «самое глубокое меню») имеет значение либо {}, если оно развернуто, либо undefined если нет. Вот как создается начальное состояние меню:

<Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />

// ...

loadMenu().then(menuItems => {
  const initialMenuState = {};
  this.setState({
    menuItems,
    openMenus: expandNodeById(initialMenuState, initialOpenMenuId),
    checkedMenus: initialCheckedMenuIds.reduce(
      (acc, val) => ({ ...acc, [val]: true }),
      {}
    )
  });
});

// ...

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

Давайте разберем это по частям.

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

Это просто удобные функции, которые мы определяем, чтобы мы могли легко изменить значение, которое мы используем для представления расширенного и нерасширенного узла. Это также делает код немного более читабельным по сравнению с простым использованием в коде литерала {} или undefined. Расширенные и нерасширенные значения также могут быть true и false, что имеет значение, так как расширенный узел истинен , а нерасширенный узел ложный. Подробнее об этом позже.

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

Эти функции позволяют переключать или расширять конкретное меню c в состоянии меню. Первый параметр - это само состояние меню, второй - идентификатор строки меню (например, "3.1.1.1.1"), а третий - функция, выполняющая замену. Думайте об этом как о функции, которую вы передаете .map(). Функциональность заменителя отделена от фактической итерации рекурсивного дерева, так что вы можете легко реализовать больше функциональности позже - например, если вы хотите, чтобы какое-то конкретное меню c не расширялось, вы можете просто передать функцию, которая возвращает unexpandedNode().

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

Эта функция используется двумя предыдущими для обеспечения более чистого интерфейса. Идентификатор разделен здесь точками (.), которые дают нам массив частей идентификатора. Следующая функция работает с этим массивом, а не со строкой ID напрямую, потому что таким образом нам не нужно делать .indexOf('.') shenanigans.

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

Функция replaceNode является основным вопросом. Это рекурсивная функция, которая создает новое дерево из старого дерева меню, заменяя старый целевой узел предоставленной функцией заменителя. Если в дереве отсутствуют промежуточные части, например, когда дерево {}, но мы хотим заменить узел 3.1.1.1, оно создает родительские узлы между ними. Вроде как mkdir -p, если вы знакомы с командой.

Так что это состояние меню. Состояние флажка (checkedMenus) - это просто индекс, ключом является идентификатор и значение true, если элемент отмечен. Это состояние не является рекурсивным, поскольку их не нужно проверять или проверять рекурсивно. Если вы решите, что хотите отобразить индикатор того, что что-то в этом пункте меню отмечено, простым решением было бы изменить состояние флажка на рекурсивное, как состояние меню.

Рендеринг дерева

Компонент <Menu> передает состояния до <MenuItemContainer>, что делает <MenuItem> s.

function MenuItemContainer({
  openMenus,
  onMenuToggle,
  checkedMenus,
  onChecked,
  menuItems = []
}) {
  if (!menuItems.length) return null;

  const renderMenuItem = menuItem => (
    <li key={menuItem.id}>
      <MenuItem
        openMenus={openMenus}
        onMenuToggle={onMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={onChecked}
        {...menuItem}
      />
    </li>
  );

  return <ul>{menuItems.map(renderMenuItem)}</ul>;
}

Компонент <MenuItemContainer> не очень отличается от исходного компонента. Однако компонент <MenuItem> выглядит немного по-другому.

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.handleToggle = this.handleToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const {
      children,
      name,
      id,
      openMenus,
      onMenuToggle,
      checkedMenus,
      onChecked
    } = this.props;

    const isLastChild = !children;
    return (
      <Fragment>
        <Button onClick={isLastChild ? this.handleChecked : this.handleToggle}>
          {name}
        </Button>
        {isLastChild && (
          <Input
            addon
            type="checkbox"
            onChange={this.handleChecked}
            checked={!!checkedMenus[id]}
            value={id}
          />
        )}

        <Collapse isOpen={openMenus ? !!openMenus[id] : false}>
          <MenuItemContainer
            menuItems={children}
            // Pass down child menus' state
            openMenus={openMenus && openMenus[id]}
            onMenuToggle={onMenuToggle}
            checkedMenus={checkedMenus}
            onChecked={onChecked}
          />
        </Collapse>
      </Fragment>
    );
  }

  handleToggle() {
    this.props.onMenuToggle(this.props.id);
  }

  handleChecked() {
    this.props.onChecked(this.props.id);
  }
}

Здесь важная часть: openMenus={openMenus && openMenus[id]}. Вместо передачи всего состояния меню мы передаем только дерево состояний, которое содержит дочерние элементы текущего элемента. Это позволяет компоненту очень легко проверить, должен ли он быть открыт или свернут - просто проверьте, найден ли у него его собственный идентификатор (openMenus ? !!openMenus[id] : false)!

Я также изменил кнопку-переключатель, чтобы переключать флажок вместо состояния меню, если это самый глубокий элемент в меню - если это не то, что вам нужно, его довольно быстро изменить обратно.

Я также использую !! здесь для приведения {} и undefined из состояния меню в true или false. Вот почему я сказал, что важно только, правдивы они или нет. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} * * * * * * * * * * * * * * * * * * * * * * * * * * * *} Теперь, когда он здесь: исходное состояние до <Menu>. initialOpenMenuId также может быть массивом (или initialCheckedMenuIds может быть единственной строкой), но это соответствует теме вопроса c.

Возможности для улучшения

Решение сейчас проходит через множество состояний, например, обратные вызовы onMenuToggle и onChecked и состояние checkedMenus, которое не является рекурсивным. В них можно использовать контекст React .

.
0 голосов
/ 10 марта 2020

const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);

const openMenuId = "3.1.1.1.1";

const {Component, Fragment} = React;
const {Button, Collapse, Input} = Reactstrap;

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {menuItems: []};
  }

  render() {
    return <MenuItemContainer menuItems={this.state.menuItems} />;
  }

  componentDidMount() {
    loadMenu().then(menuItems => this.setState({menuItems}));
  }
}

function MenuItemContainer(props) {
  if (!props.menuItems.length) return null;
  
  const renderMenuItem = menuItem =>
    <li key={menuItem.id}><MenuItem {...menuItem} /></li>;
    
  return <ul>{props.menuItems.map(renderMenuItem)}</ul>;
}
MenuItemContainer.defaultProps = {menuItems: []};

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: false};
    this.toggle = this.toggle.bind(this);
  }

  render() {
    let isLastChild = this.props.children ? false : true;
    let {isOpen} = this.state;
    if(openMenuId.startsWith(this.props.id)){isOpen = true;}
    return (
      <Fragment>
        <Button onClick={this.toggle}>{this.props.name}</Button>
        <Fragment>
          {isLastChild ? <Input type="checkbox" checked={openMenuId === this.props.id} value={this.props.id} /> : ''}
        </Fragment>
        <Collapse isOpen={isOpen}>
          <MenuItemContainer menuItems={this.props.children} />
        </Collapse>
      </Fragment>
    );
  }

  toggle() {
    this.setState(({isOpen}) => ({isOpen: !isOpen}));
  }
}

ReactDOM.render(<Menu />, document.getElementById("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>

<div id="root"></div>
0 голосов
/ 14 марта 2020

Предполагая, что вам нужно открывать определенное меню только в начале, вы можете установить MenuItem компонент для ожидания логического свойства defaultOpen и использовать его для установки начального isOpen.

Тогда все мы нужно установить это свойство в menuItems при загрузке.

import React from 'react'
import { Button, Collapse, Input } from 'reactstrap';
import 'bootstrap/dist/css/bootstrap.min.css';

const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);

const openMenuId = "3.1.1.1";

const {Component, Fragment} = React;

function setDefaultOpen(menuItems, openMenuId) {
  if(!menuItems) return
  const openMenuItem = menuItems.find(item => openMenuId.startsWith(item.id))
  if(!openMenuItem) return
  openMenuItem.defaultOpen = true
  setDefaultOpen(openMenuItem.children, openMenuId)
}

export default class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {menuItems: []};
  }

  render() {
    return <MenuItemContainer menuItems={this.state.menuItems} />;
  }

  componentDidMount() {
    loadMenu().then(menuItems => {
      setDefaultOpen(menuItems, openMenuId)
      this.setState({menuItems})
    });
  }
}

function MenuItemContainer(props) {
  if (!props.menuItems.length) return null;

  const renderMenuItem = menuItem =>
    <li key={menuItem.id}><MenuItem {...menuItem} /></li>;

  return <ul>{props.menuItems.map(renderMenuItem)}</ul>;
}
MenuItemContainer.defaultProps = {menuItems: []};

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: props.defaultOpen};
    this.toggle = this.toggle.bind(this);
  }

  render() {
    let isLastChild = this.props.children ? false : true;
    return (
      <Fragment>
        <Button onClick={this.toggle}>{this.props.name}</Button>
        <Fragment>
          {isLastChild ? <Input type="checkbox" value={this.props.id} /> : ''}
        </Fragment>
        <Collapse isOpen={this.state.isOpen}>
          <MenuItemContainer menuItems={this.props.children} />
        </Collapse>
      </Fragment>
    );
  }

  toggle() {
    this.setState(({isOpen}) => ({isOpen: !isOpen}));
  }
}

Если вам нужна возможность открывать пункт меню после первоначального рендеринга, то вам нужно сделать MenuItem контролируемым компонентом.

т.е. поднимите состояние isOpen до родителя и передайте его компоненту MenuItem в качестве подпорки вместе с функцией обратного вызова, которую он будет вызывать при щелчке, передавая свой идентификатор в качестве аргумента. Функция обратного вызова в parent будет переключать свойство isOpen элемента с указанным идентификатором в его состоянии.

0 голосов
/ 10 марта 2020

просто добавьте класс .actve или любой другой, который вам нужен, и стилизуйте его согласно вашему требованию, а затем добавьте script, если вы используете обычный js, а затем document.querySelector("youElementClassOrId").classList.toggle("idOrClassYouWantToToggle"). Я надеюсь, это будет работать

...