Sithu
Sithu

Reputation: 4862

How to conditionally group HTML tags in React

I have template rendering in a loop like this

{
    category.menuIds.map((menuId, idx) => (
        <TemplateMenu
            index={idx}
            key={menuId}
            menu={this.props.menus[menuId]}
            menuLayout={category.menu_layout}
        />
    ));
}

It renders a list of <div> as below:

<div id="m0" class="Menu">...</div>
<div id="m1" class="Menu">...</div>
<div id="m2" class="Menu">...</div>
<div id="m3" class="Menu">...</div>
<div id="m4" class="Menu">...</div>
<div id="m5" class="Menu">...</div>
<div id="m6" class="Menu">...</div>
<div id="m7" class="Menu">...</div>
<div id="m8" class="Menu">...</div>
<div id="m9" class="Menu">...</div>

I want to group every 4 <div> upon the condition this.props.index % 5 !== 0 to get HTML below:

<div id="m0" class="Menu">...</div>
<div class="MenuRight">
    <div id="m1" class="Menu">...</div>
    <div id="m2" class="Menu">...</div>
    <div id="m3" class="Menu">...</div>
    <div id="m4" class="Menu">...</div>
</div>
<div id="m5" class="Menu">...</div>
<div class="MenuRight">
    <div id="m6" class="Menu">...</div>
    <div id="m7" class="Menu">...</div>
    <div id="m8" class="Menu">...</div>
    <div id="m9" class="Menu">...</div>
</div>

Any help would be appreciated.

[Edit]

index doesn't matter. It can be the same as it is now or relative to the group. I just described it in the example code to make it more clear.

At the condition this.props.index % 5 === 0, the element <div class="Menu"> would be standalone, else the other elements would be grouped with <div class="MenuRight">.

Upvotes: 0

Views: 441

Answers (3)

PR7
PR7

Reputation: 1914

You can reduce the categories.menuIds array to a 2D array similar to the mentioned structure. For ex. [[0], [1, 2, 3, 4], [5], [6, 7, 8, 9]...]. Now even index contains MenuItem [0] and odd index contain MenuRight [1, 2, 3, 4]. Now we can conditionally render this using Array.map().

Working Example : Demo Link

First Reduce the categories.menuIds array

const menuIds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];

const transformed = menuIds.reduce((acc, curr, idx) => {
  if (idx % 5 === 0) {
    const i = Math.floor(idx / 5) * 2;
    acc[i] = [curr];
  } else {
    const parentIdx = Math.floor(idx / 5) * 2 + 1;
    acc[parentIdx] = acc[parentIdx] || [];
    acc[parentIdx] = [...acc[parentIdx], curr];
  }

  return acc;
}, []);

console.log(transformed);

Render this in React using Array.map()

<div className="App">
  {transformed.map((item, idx) => {
    return (
      <>
        {idx % 2 === 0 && (
          <div key={idx} className="Menu">
            {item}
          </div>
        )}
        {idx % 2 === 1 && (
          <div key={idx} className="MenuRight">
            {item.map((menuRight, i) => (
              <div key={i} className="Menu">
                {menuRight}
              </div>
            ))}
          </div>
        )}
      </>
    );
  })}
</div>

Upvotes: 1

Thalles Cezar
Thalles Cezar

Reputation: 179

It would be complex to regroup the tags after they've already been mapped to a flat list of TemplateMenu, as each element is not aware of its position according to their parent's logic.

To solve your problem I would group the elements before mapping them into the final elements, and then rendering them into a single TemplateMenu or the grouped MenuRight.

For example, this is the quickest approach I could think to group them, but of course there are many other algorithms you might prefer:

const groupedIds = menuIds.reduce((groups: any[], id: any, index: number) => {
    if (index % 5 === 0) {
      groups[Math.floor(index / 5) * 2] = [id];
    } else {
      const groupIndex = Math.floor(index / 5) * 2 + 1;
      if (groups[groupIndex]) {
        groups[groupIndex].push(id);
      } else {
        groups[groupIndex] = [id];
      }
    }

    return groups;
  }, []);

This would result in an array of ids like [[0],[1,2,3,4],[5],[6,7...]...]. Then you can use these groups to render them in different ways.

Here is a working example with a problem derived from yours (also on CodeSandbox):

function TemplateMenu({ index, id }: { index: number; id: any }) {
  return (
    <div id={`m${index}`} className="Menu">
      {id}
    </div>
  );
}

/*export default*/ function App() {
  const menuIds = ["single 1", 2, 3, 4, 5, "single 2", "a", "b"];

  const groupedIds = menuIds.reduce((groups: any[], id: any, index: number) => {
    if (index % 5 === 0) {
      groups[Math.floor(index / 5) * 2] = [id];
    } else {
      const groupIndex = Math.floor(index / 5) * 2 + 1;
      if (groups[groupIndex]) {
        groups[groupIndex].push(id);
      } else {
        groups[groupIndex] = [id];
      }
    }

    return groups;
  }, []);

  return (
    <div className="App">
      {/*{menuIds.map((id, idx) => {
        return <TemplateMenu key={id} index={idx} id={id} />;
      })}*/}
      {groupedIds.map((group, idx) => {
        if (group.length === 1) {
          return <TemplateMenu key={group[0]} index={idx} id={group[0]} />;
        } else {
          return (
            <div className="MenuRight">
              {group.map((id, idx) => {
                return <TemplateMenu key={id} index={idx} id={id} />;
              })}
            </div>
          );
        }
      })}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
.Menu {
    outline: 1px solid red;
}

.MenuRight {
    outline: 1px solid blue;
    padding: 4px;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1074989

You can do this with a simple loop, which to me is by far the clearest way; see comments:

const {menuIds} = category;
const elements = [];
// Include every 5th element directly, including the 0th one;
// group the (up to) four following it
let index = 0;
while (index < menuIds.length) {
    // Use this entry directly, note we increment index after using it
    // in the `TemplateMenu`
    const {menuId} = menuIds[index];
    elements.push(
        <TemplateMenu
            index={index++}
            key={menuId}
            menu={this.props.menus[menuId]}
            menuLayout={category.menu_layout}
        />
    );

    // Follow it with up to four in a `<div class="MenuRight">`
    const following = menuIds.slice(index, index + 4);
    if (following.length) {
        // Note we increment `index` as we go
        elements.push(
            <div className="MenuRight">
                {following.map(({menuId}) => (
                    <TemplateMenu
                        index={index++}
                        key={menuId}
                        menu={this.props.menus[menuId]}
                        menuLayout={category.menu_layout}
                    />
                ))}
            </div>
        );
    }
}

// ...use {elements}...

Live Example:

const {useState} = React;

const TemplateMenu = ({index, menu, menuLayout}) => <div className="TemplateMenu">{menu}</div>;

class Example extends React.Component {
    render() {
        const category = {
            menuIds: [
                {menuId: 0},
                {menuId: 1},
                {menuId: 2},
                {menuId: 3},
                {menuId: 4},
                {menuId: 5},
                {menuId: 6},
                {menuId: 7},
                {menuId: 8},
                {menuId: 9},
            ],
            menu_layout: "example",
        };
        const {menuIds} = category;
        const elements = [];
        // Include every 5th element directly, including the 0th one;
        // group the (up to) four following it
        let index = 0;
        while (index < menuIds.length) {
            // Use this entry directly, note we increment index after using it
            // in the `TemplateMenu`
            const {menuId} = menuIds[index];
            elements.push(
                <TemplateMenu
                    index={index++}
                    key={menuId}
                    menu={this.props.menus[menuId]}
                    menuLayout={category.menu_layout}
                />
            );

            // Follow it with up to four in a `<div class="MenuRight">`
            const following = menuIds.slice(index, index + 4);
            if (following.length) {
                // Note we increment `index` as we go
                elements.push(
                    <div className="MenuRight">
                        {following.map(({menuId}) => (
                            <TemplateMenu
                                index={index++}
                                key={menuId}
                                menu={this.props.menus[menuId]}
                                menuLayout={category.menu_layout}
                            />
                        ))}
                    </div>
                );
            }
        }

        // ...use {elements}...
        return <div>{elements}</div>;
    }
};

const menus = [
    "Menu 0",
    "Menu 1",
    "Menu 2",
    "Menu 3",
    "Menu 4",
    "Menu 5",
    "Menu 6",
    "Menu 7",
    "Menu 8",
    "Menu 9",
];
ReactDOM.render(
    <Example menus={menus} />,
    document.getElementById("root")
);
.TemplateMenu {
    border: 1px solid green;
    margin: 4px;
}
.MenuRight {
    margin: 4px;
    padding: 4px;
    border: 1px solid red;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Upvotes: 1

Related Questions