Recursive contextual components

Let's kick off our nested dropdown by building out a skeleton List and Item component that yield each other.

Summary

To start off our nested dropdown, let's look at an HTML representation of a list with sublists:

<ul class='list-reset flex'>
  <li class='relative p-4 bg-grey-light hover:bg-yellow cursor-pointer'>
    Menu item A

    <ul class='list-reset absolute pin-l mt-4 bg-white shadow'>
      <li class='p-4'>Subitem A 1</li>
      <li class='p-4'>Subitem A 2</li>
      <li class='p-4'>Subitem A 3</li>
    </ul>
  </li>

  <li class='relative p-4 bg-grey-light hover:bg-yellow cursor-pointer'>
    Menu item B

    <ul class='list-reset absolute pin-l mt-4 bg-white shadow'>
      <li class='p-4'>Subitem B 1</li>
      <li class='p-4'>Subitem B 2</li>
    </ul>
  </li>

  <li class='p-4 bg-grey-light hover:bg-yellow cursor-pointer'>
    Menu item C
  </li>
</ul>

In HTML we can render unordered lists within list items, as many levels deep as we like. Our goal will be to end up with something similar for our nested dropdown component.

Let's think about what our API should look like. We want to render a dropdown list that has many items, and each one of those items can optionally render another nested dropdown list, just like in HTML.

It might look something like this:

<DropdownList as |List|>
  <List.item as |Item|>
    Menu A

    <Item.sublist as |List|>
      <List.item>
        Submenu A 1
      </List.item>
      <List.item>
        Submenu A 2
      </List.item>
    </Item.sublist>
  </List.item>

  <List.item>
    Menu B
  </List.item>
</DropdownList>

This API mirrors HTML's <ul> and <li> tags, but uses contextual components to associate each item with its parent list.

Let's create the <DropdownList> component. It has a tag of ul and yields out a <DropdownListItem>:

export default Component.extend({
  tagName: 'ul'
});
{{yield (hash
  sublist=(component 'dropdown-list')
)}}

Now our <DropdownListItem>, which has a tag of li and yields out a sublist, which just renders another instance of <DropdownList>:

export default Component.extend({
  tagName: 'ul'
});
{{yield (hash
  item=(component 'dropdown-list-item')
)}}

So List yields Item, and Item yields List, and we can keep rendering these infinitely deep.

Now that we have the skeleton set up, we're ready to start working on the actual behavior.

View the full diff on GitHub.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.