Josh Kelley Josh Kelley - 2 months ago 17
Javascript Question

Numbering children

I have a menu component, and I want to be able to number the menu items within this component. (Menu item numbers are then used to track the focused menu item and to respond to number keypad keystrokes.)

My current implementation looks something like this:

class Menu extends React.Component {
numberChildren() {
return _.chain(React.Children.toArray(this.props.children))
.filter()
.map((child, i) => React.cloneElement(child, {
number: i + 1
}))
.value();
}

render() {
return <div className="menu">{this.numberChildren()}</div>;
}
};


I'm using React.cloneElement to inject a
number
prop, and I'm using lodash's
filter
to exclude any null children.

This is enough to handle menus such as this:

<Menu>
<MenuItem>Salad</MenuItem>
<MenuItem>Grilled Chicken</MenuItem>
{!this.props.isOnDiet && <MenuItem>Ice Cream</MenuItem>}
</Menu>


However, it fails for more complicated children:

<Menu>
<MenuItem>Salad</MenuItem>
<SoupOfTheDayMenuItem/>
<MenuItem>Grilled Chicken</MenuItem>
</Menu>


SoupOfTheDayMenuItem
is a wrapper component that returns either a single
MenuItem
(which should also be enumerated) or
null
.

Even if
SoupOfTheDayMenuItem
returns null, it's still truthy at the time lodash's
filter
is called, so it still gets number 2.

Is there a way to say "only inject this property for children that actually get rendered" or "get me the set of children that actually rendered somthing"? Or is there a better way of designing all of this?

Answer

I assume SoupOfTheDayMenuItem is a wrapper for MenuItem or returns null.
So you can try to filter out those children which have props.children falsy which would mean SoupOfTheDayMenuItem returned null.

This should do the job:

numberChildren() {
    return _.chain(React.Children.toArray(this.props.children))
      .filter(child => child.type.displayName == 'MenuItem' || child.props.children)
      .map((child, i) => React.cloneElement(child, {
          number: i + 1
      }))
      .value();
  }