I recently solved an old layout problem with a new tool. In this post, I’ll tell you about a challenge my team faced when placing dropdown menus in certain areas of an Ember app and how we used Ember Wormhole to tackle it.
A Simple Component
Say you’re building a reusable UI widget. It’s a little card, and on that card is a dropdown menu:
(In this case, the widget is an Ember.js component, and the dropdown menu is provided by Foundation 5.)
The menu is populated dynamically with information known inside the component, so we naturally placed it right in the component’s template:
<div class="card-thing">
<div class="name" >{{name}}</div>
<div class="menu-button" data-dropdown="drop1-{{elementId}}" aria-controls="drop1-{{elementId}}" aria-expanded="false">⚙</div>
<ul id="drop1-{{elementId}}" class="f-dropdown" data-dropdown-content aria-hidden="true" tabindex="-1">
<li><a href="#">This is a link</a></li>
<li><a href="#">This is another</a></li>
<li><a href="#">Yet another</a></li>
</ul>
</div>
The dropdown menu is absolutely positioned. Foundation styles the element directly; the <ul>
looks like this when it’s open:
<ul class="f-dropdown open f-open-dropdown"
data-dropdown-content=""
aria-hidden="false"
tabindex="-1"
id="drop1-ember386"
style="position: absolute; left: -4.39062px; top: 158px;">
Misbehavior Deep Inside the UI
Imagine that you’re rendering a few components deep inside some overflow:scroll
containers. Maybe there’s a responsive grid of cards inside a collapsible drawer, which is inside another collapsible drawer. But I digress; here’s a simplified version of what can happen:
The component is effectively larger when the menu is open. The size of the scrollable region outgrows its viewport, and we get a scrollbar. Yuck.
The Problem at Hand
In short, I’m trying to reconcile two conflicting requirements about where the dropdown should go.
- CSS leads me to place the absolutely-positioned element outside the `overflow:scroll` container. We don’t want the menu to affect the component’s geometry.
- The hierarchy of the UI leads me to implement the dropdown menu inside the card component, deep inside the `overflow:scroll` container(s). That’s where the information used to populate the menu is available, and where its actions are handled. Even if information flow didn’t require this placement, I’d still want to keep logical proximity between these related concerns and to abstract the menu details away from users of the card component.
Is there some way we can have both?
Into the Wormhole
Yapplabs’ Ember Wormhole provides a generalized solution to this kind of problem. It’s a component that renders its contents elsewhere on the page while preserving Ember data binding, action bubbling, etc. It’s also really simple to use:
{{#ember-wormhole to="another-dimension" menuClicked="menuClicked"}}
<ul id="drop1-{{elementId}}" class="f-dropdown" data-dropdown-content aria-hidden="true" tabindex="-1">
{{#each menuOptions as |option|}}
<li><a href="#" {{action "menuClicked" option}}>{{option}}</a></li>
{{/each}}
</ul>
{{/ember-wormhole}}
Specify the other end of the wormhole with something like this:
<div id="another-dimension"></div>
, and voila!
Conclusion
Here’s how it looks in action. Notice that the menu <ul>
is rendered outside the card’s DOM.
The sample project used in this post is available on GitHub.
I first learned of Ember Wormhole via ember-modal-dialog, which I also heartily recommend.
The web development ecosystem has evolved gradually over time. Nobody saw web components coming when overflow:scroll
was specified in 1998. Piling technologies on top of technologies can seem a little crazy, but today, I’m glad I found this one.
Why not go with overflow: visible then?
Hi Vladimir,
Thanks for the comment! You’re right: in my contrived example, changing overflow:scroll to overflow:visible is enough to fix it.
The situation where this happened in our app is much more complicated, and overflow:scroll was not enough. It involves a collapsible drawer and responsive grids, and while it may have been _possible_ to solve with traditional CSS, it was _easy_ to solve with ember-wormhole.