Over engineering a drop-down component
The drop-down is the most used UI pattern to compress complex functionality into a small space. It’s mostly composed of a trigger and menu. Variations include the trigger as input that also acts as a search for the items from the drop-menu. You may also find drop-downs that look and act like tool-tips and inside of them to include other drop-downs.
The anatomy of a drop-down
+--------------------------------------+
| +--------> Dropdown
| +--------------------------------+ |
| | | |
| | +-----------> DropTrigger
| | | |
| +--------------------------------+ |
| |
| +--------------------------------+ |
| | +-----------> DropMenu
| | +----------------------------+ | |
| | | | | |
| | | | | |
| | +----------------------------+ | |
| | +----------------------------+ | |
| | | | | |
| | | +-------------> DropItem
| | +----------------------------+ | |
| | | |
| +--------------------------------+ |
| |
+--------------------------------------+
A React approach using ref
When I’ve started learning React the Dropdown always seemed to be a challenge to get right. There are a lot of implementations out there, but none of them can handle complex customization. I have to admit I haven’t tried any of the drop-down from the new UI frameworks, but when React was at version 0.9 there where even fewer from which to choose from.
So I’ve implemented my own:
<Dropdown trigger={this.renderTrigger()} ref="dropdown">
<a onClick={this.onSelect.bind(this, 1)}>Option 1</a>
<a onClick={this.onSelect.bind(this, 2)}>Option 2</a>
</Dropdown>
...
onSelect(option) {
this.props.onSelect(option);
this.refs.dropdown.close()
}
But closing the drop-down became more of a repetitive task when we added accessibility to this component by trapping the focus when it was opened. So I’ve asked myself shouldn’t there be an easier way?
Take two: use context
We can use the drop-down's anatomy to compose it in React. The Dropdown
acts as a layout manager by arranging it’s children in their right place and creates the context that DropItem
’s will use. It adds the default behavior like closing the menu when clicking outside of the drop-down and makes sure the trigger will toggle the drop-menu's visibility when clicking it. It can enforce CSS consistency and apply themes as well, but this is bit outside the scope of this post.
To identify each composing block of the Dropdown
, I’ve created some placeholder components to simplify customizing the drop-down to render different elements as the trigger or inside the menu, making it more versatile. These components should have no impact in the Dropdown
’s render, and shouldn’t add any extra markup. I’ve also create a decorator component to manage a drop-item behavior.
The DropMenu
, like the DropTrigger
is just a placeholder for the Dropdown
component to identify and place its children in the right place. There is no use case when these components would be used outside of the Dropdown
. Semantically it wouldn’t make sense to reuse them anywhere else.
This will allow rendering of custom triggers for Select components eg: display the Select
’s value or a Multiselect
’s number of items selected. You can also add other markup in the DropMenu
like a search input to implement a search items functionality etc.
The DropItem
will use the Dropdown
context to close the drop-down, if needed, based on the closeOnClick
prop. It will send a onClick
prop to it’s children, to add on it’s root/wrapping element. This might be considered tight coupling, but the DropItem
has no reason to be outside the Dropdown
.
This solves the problem of a child calling a parent method using a ref. Using context is a better approach to have elegant code that clear separation of concerns.
Thank you for reading. Recommends, comments and tweets are always welcome.
There will be a repository available soon with some examples to prove how this solution can work on different drop-down implementations