masaj salonu masaj salonları ümraniye escort izmir escort
Home » Advertising » Writing a Filterable Drop-Down Menu in Elm

Writing a Filterable Drop-Down Menu in Elm

Image title

Elm is a fun language for writing front-end programs. In this post, we will share how we solved the problem of implementing a filterable drop-down menu with it!

At PhraseApp, we came to the conclusion that we wanted to change something about our front-end stack. After evaluating several different frameworks, we decided that we would try out elm.

For our application, we needed a drop-down menu with a text field, such that the user can filter the displayed menu entries by typing in a query string. We usually use the JavaScript library selectize.js, and, of course, we could have embedded this component in our elm application (using ports). But we thought that writing such a drop-down menu is actually an interesting problem, and we wanted to see how we could solve it in elm.

You can find our final result on github along with a small live demo.

In this blog post, we would like to share the solution we came up with and especially try to give some reasons as to why we did it in this way. Also, we try to explain some implementation details which might be interesting, too.

What Are the Needed Features?

So let’s recap what our drop-down menu should offer:

  • The opened menu should have a maximum height, and it should be scrollable if there are a lot of items to be displayed.
  • The user should be able to select entries by clicking on them, and each entry should come into focus if we hover over it with the mouse.
  • Also, the user should be able to focus entries using the up and down keys and actually select them by pressing enter.
  • When the menu is open, typing in something should filter the list of displayed entries.
  • We also need to be able to display dividers in the menu, which are not selectable.

What Solutions Are Out There?

The elm community is small, but still, there are some (very good) solutions out there! For example, there is the popular package elm-autocomplete, which solves the problem of a filterable menu which is also navigatable with the keyboard in a very general way. And there are several drop-down packages one finds when looking in the elm package database.

So why did we not use elm-autocomplete?

This package does not include the text field for entering the query into the package. This was done for a very good reason, namely to provide a more flexible solution. The payoff is that one has to add global keydown-subscriptions to handle keyboard navigation since only HTML elements which have come into focus can fire keypress-events. In our use-case, we always have an input text field, so the need for global subscriptions seemed a bit unwieldy (one could, of course, argue that having the additional subscription boilerplate is not too much to ask).

We guess that elm-autocomplete was not designed with a scrollable menu in mind. When calling its view or update functions, one has to provide a maximum number of menu entries which should be displayed. In our case, this would have always been the total number of entries which matched the filtering string. So this part of the API did not seem to be a good fit for our problem.

And last but not least, it is never a bad idea to make a fresh start and try to come up with another solution to an old problem, later comparing the results with what’s already out there.

And it turns out, writing packages in elm is a really satisfying process and you always learn something!

What Should the API Look Like?

It is always a good idea to spend a decent amount of time on thinking about what your API should look like:

  • What part of the state should live in the application?
  • Which parts have to be handled by the package?
  • How can the user customize the appearance or behavior of the drop-down menu?

We decided that the (unfiltered) list of menu entries should be stored in the main model. In our application, we actually have several drop-down menus which share a common set of entries, and if the user selects an entry in one of them, this entry should disappear in the other menus.

Also, the eventually selected entry should not be stored in the state of the dropdown menu. We have to be able to change the selection within our application and having setters and getters is generally a very bad idea, as it clashes with the “one source of truth” paradigm.

On the other hand, the open/closed state of the menu and the entered query for filtering the menu entries should be managed by our drop-down package. Also, the current mouse/keyboard focus is certainly not something we want to deal with on the application level.

Another thing we had to think about was: how do we represent the menu entries? We decided that our dropdown menu should be generic over the entry type. So the application has to provide the drop-down menu with a  List (Entry a). Here a stands for the (selectable) menu entries and the API provides constructors.

entry : a - Entry a
 
divider : String - Entry a

One is provided for the actual selectable entries, and one for the dividers. This way, the user of the package has full flexibility on how menu entries can be modeled: maybe it can be just a list of strings, but perhaps it has to be something more complicated, like a record which stores a title, some description, and, optionally, an image.

Going this more generic route, we need to tell the drop-down menu how it can render an individual entry, i.e. providing a function like a - Html msg, and how it can filter the entries with a given query, which boils down to a function, like so:  String - List a - List a .

We did not do something like the code below for providing the possibility to add nonselectable dividers, as we did not want to introduce this extra nesting layer.

sections : List (Section a)
 
type alias Section a =
    { title : String
    , entries : List a
    }

We could have also given the dividers a generic type, but for now, we just need them to make captions for structuring the list of menu entries. And it is fairly easy to extend the implementation.

So, apart from the opaque state type, type State a, and an opaque message type, type Msg a, the drop-down exposes only a view and an update function, with the following signatures:

view : ViewConfig a model - model - Html (Msg a)
 
update : UpdateConfig a msg model - model - ( State a, Cmd (Msg a), Maybe msg )

Here, model is the model type of our main application msg and is the
main message type. If you compare this to other packages, like, elm-sortable-table, for example, you may wonder why we do not provide the State a in the view and update function. And also, how do view and update know what the menu entries are and what the current selection is?

decided that instead of giving view and update several more arguments, which would probably be more the TEA way, we just provide ViewConfig a model and UpdateConfig a msg model which include functions for retrieving this data from the current model. Namely, these configurations are created in the following way:

sharedConfig :
    { toLabel : a - String
    , state : model - State a
    , entries : model - List (Entry a)
    , selection : model - Maybe a
    , id : String
    }
    - SharedConfig a model
 
viewConfig :
    SharedConfig a model
    - { placeholder : String
       , container : List (Html.Attribute Never)
       , input : Bool - Bool - List (Html.Attribute Never)
       , toggle : Bool - Html Never
       , menu : List (Html.Attribute Never)
       , ul : List (Html.Attribute Never)
       , entry : a - Bool - Bool - HtmlDetails Never
       , divider : String - HtmlDetails Never
       }
    - ViewConfig a model
 
updateConfig :
    SharedConfig a model
    - { select : Maybe a - msg }
    - UpdateConfig a msg model

So, for example, if our main model is given with the following: 

type alias Model =
    { entries : List (Entry String)
    , selection : Maybe String
    , menu : State String
    ...
    }

We would create the shared configuration in the following way: 

sharedConfig =
    Selectize.sharedConfig
        { toLabel = entry - entry
        , state = .menu
        , entries = .entries
        , selection = .selection
        , id = "our-menu"
        }

The update configuration only adds the information and how the drop-down menu can ask the main application to change the selection. This might look like a setter function, but it is conceptually different.

We made the decision that the selection state should live in the main application model and not in the drop-down menu state. But the drop-down menu should have a way to alter the selection (after all, that is the whole purpose of the drop-down menu). We could have changed the drop-downs update function to return a tuple which includes a Maybe a, indicating that we ask the application to change the selection. But then we (as the user of the package) would have to make sure that this new selection is stored in the main model. But what if we forget to do this? Or what if a change of the selection also requires some other business logic to be performed?

The point is: from the perspective of the drop-down menu, changing (or, better, asking for a change of) the selection is a side effect. So our drop-down update better returns a Maybe msg, since messages are the way to communicate effects in elm.

It also gives the user the chance to separate the update boilerplate of the drop-down menu from the selection change logic. This is good because the first one really is just the necessary boilerplate one has to write, the second one is part of the actual business logic.

Implementing Style-Independent Scrolling

We want our drop-down menu to be navigatable using up- and down arrow keys. Since the menu also uses overflow-y: scroll, we have to scroll the menu when the next entry lies outside of the current menu viewport. There is already a package for issuing scrolling commands. Awesome!

But wait! If we want to scroll properly, we need to know the height of all menu entries, and the elm architecture does not provide an obvious way of fetching data from the DOM. So what are our possibilities?

One way is tagging each entry with an ID and setting up some ports on the JavaScript side.

port fetchHeight : String - Cmd msg
 
port height : (( String, Int ) - msg) - Sub msg

The listener of fetchHeight looks up the DOM-element with the provided ID and sends its height to the height port. This is certainly a good way to do it, but it requires some JavaScript logic to be set up and we wanted our package to be elm only.

Luckily, there is another way to achieve this! There is a place in the elm architecture where one can actually analyze the rendered DOM tree, namely when decoding JSON events. So, whenever you are in the situation that:

  • you need information about the rendered DOM
  • you need that information after some event was fired
  • the element, this event was attached to, is close to the DOM element whose information you need 

you can use this DOM decoding trick, to retrieve, for example, the width and height of some element.

So what we did was attach a custom decoder onto the focus event of the text field, which also fetches the heights of all menu entries. To do this properly, we just have to make sure that the menu already existed in the DOM tree before the text field was focused. We achieved this by always rendering the menu container but hiding it with position: absolute if it was closed. Note that the parent node then needs position: relative and overflow: hidden.

Then, when we handle the up- and down-keys, we also fetch the current scroll position of the menu and use the previously fetched entry heights to compute to where we want to scroll our menu.

We just have to make sure that these heights are re-computed when the filtering is changed so that the list of menu heights and the list of filtered entries is always in sync.

Conclusions

We are really happy how this package turned out, and one cannot stress enough that doing this in elm was a big reason why. Elm just takes care of all the annoying parts of programming and lets you focus on solving the actual problem.

Still, there are some things which can be improved:

  • We tried to make the dropdown menu (especially the scrolling) as fast as possible, but there is probably still some space left for optimization. One reason was, that we did not see a simple way of benchmarking the view function.
  • You still have to give each drop-down menu a globally unique id, which is not ideal. Perhaps, it would be a good thing if you had a special type type Nodewhich represents a rendered dom node, alongside a way of decoding these nodes from JSON values, i.e. something like targetDecoder : Decoder Node. The scrolling api then could look like toY : Node - Float - Task Error (). It would be interesting to know if having something like this is a good idea and actually possible!

Leave a Reply

Your email address will not be published. Required fields are marked *

*
*

cover letter