Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bootstrap Dropdown in Elm

I am quite new to Elm (and to frontend development in general), so I hope my questions are not too obvious, but I would like to know how I can reuse UI frameworks like Bootstrap 3 in Elm, JS included.

More precisely, my goal for today is to more or less replicate the example in the Bootstrap's Navbar documentation, whereby a dropdown button is contained within a navbar. So far, using Html and Html.Attributes, I was able to build the page and style it properly, but the actual dropdown behavior does not work: when I click on the caret button, nothing happens. I guessed the reason for that is that the Javascript connected to the dropdown is not being executed.

Now, my questions are:

  1. How can I reuse components like dropdowns, including their related JS, from Elm code?
  2. What would be the preferred (more idiomatic) way to do this in Elm? I have seen packages like circuithub's elm-bootstrap-dropdown, which I understand being a (in)complete rewrite in Elm of the same JS functionality, so I wonder: is the Elm way to rewrite everything from scratch? I would be very surprised about that...

Thank you all

like image 229
mdm Avatar asked Apr 20 '16 08:04

mdm


2 Answers

I'm currently using Bootstrap extensively in a real-life Elm project and it's working great.

I don't think you want to integrate it into Elm too much by replicating stuff in Elm. In my experience you will always be one (or two) steps behind the current Bootstrap version.

It can be a bit challenging to get everything setup though but once done it runs flawlessly. To get you going I'm posting my index.html file I use to load the Elm application. It is a one page application with client side routing so that's why I'm sending in the window.location.pathname into the application. It has nothing to do with using Bootstrap. I'm including it because I really wanted to post the file "as-is" more or less.

Basically we're simply loading Bootstrap CSS and JS from a content distribution network directly into the browser before we even load and start the Elm application.

index.html:

<!DOCTYPE HTML>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->

  <title>My Awesome Application Title</title>

  <!-- Bootstrap: Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">

  <!-- Bootstrap: Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">

  <!-- Application styles -->
  <!-- link rel="stylesheet" href="normalize.css" -->
  <!-- link rel="stylesheet" href="skeleton.css" -->
  <link rel="stylesheet" href="app.css">

  <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
  <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
  <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
  <![endif]-->
</head>
<body>
  <!-- Bootstrap: jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>

  <!-- Bootstrap: Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>

  <!-- Application: Javascript -->
  <!-- script type="text/javascript" src="vendor.js"></script -->
  <script type="text/javascript" src="native.js"></script>
  <script type="text/javascript" src="app.js"></script>

  <!-- Fire up the Elm application -->
  <script type="text/javascript">
    // Pass along the current pathname as "path" to the Elm application.
    //
    // This way the frontend router can decide what "page" to display.
    //
    var app = Elm.fullscreen(Elm.App, {path: window.location.pathname});

    // Alternatively, embed the elm app in an element like this:
    // var div = document.getElementById('myDiv');
    // Elm.embed(Elm.Index, div);
  </script>
</body>
</html>

Once we've started the Elm application it is a simple manner of generating Bootstrapified HTML and CSS just as you would in any other project using Bootstrap.

To give you an idea of how to implement the Navigation bar here is a simple excerpt from my Navigation bar code in Elm:

-- Navigation bar ------------------------------------------------------------
--
-- TODO: Adapt to more responsive design. Stack navigation in mobile-friendly
--       layout and minimize user/instrument information.
--

-- Page link buttons

pageLinks : Context -> List Html
pageLinks (link, model, _) =
  List.map (\p ->
    li  [ classList [ ("active", model.page == p) ] ]
        [ link (pageToRoute p) (pageName p) ]) visiblePages

-- Dropdown menu with available options

dropdownSelectOption : Context -> Html
dropdownSelectOption (_, model, address) =
  let
    -- Option selection row (displayed in dropdown menu)
    iRow i =
      let
        title_ = case i.macId of
          Just macId -> macId   -- primarily use macId if available
          Nothing    -> i.uuid  -- otherwise, fallback on using the uuid
        -- Determine if the option is selected and/or online/offline
        isSelected = isActiveOption model i.uuid
      in
        li  [ title title_ ]
            [ a [ href "#", onClick address (SelectOption i.uuid) ]
                [ text i.displayName,
                  span [ classList [("pull-right glyphicon glyphicon-remove-sign status-offline", i.status == Offline),
                                    ("pull-right glyphicon glyphicon-ok-sign status-online", i.status == Online),
                                    ("pull-right glyphicon glyphicon-question-sign status-unknown", i.status == Unknown)],
                         title (optionStatusToString i.status)
                       ] [],
                  span [ classList [("pull-right glyphicon glyphicon-eye-open selected", isSelected)],
                         title (OptionSelectedTooltip |> t)
                       ] []
                ]
            ]
    -- Map rows on list of options
    iSelectList = List.map iRow model.options
  in
    li  [ class "dropdown", title (OptionSelectTooltip |> t) ]
        [ a [ class "dropdown-toggle",
              href "#",
              attribute "data-toggle" "dropdown",
              attribute "role" "button",
              attribute "aria-haspopup" "true",
              attribute "aria-expanded" "false" ]
            [ OptionSelectionMenu |> tt,
              span [ class "caret" ] []
            ],
          ul  [ class "dropdown-menu dropdown-menu-right" ]
              (List.concat [
                [ li [ class "dropdown-menu-label" ]
                     [ span [] [ OptionSelectLabel |> tt ] ],
                  li [ class "divider", attribute "role" "separator" ] [] ],
                iSelectList])
        ]

-- The complete navigation bar

navigationBar : Context -> Html
navigationBar ctx =
  ul  [ class "nav nav-pills pull-right" ]
     ([ (pageLinks ctx),
      [ (dropdownSelectOption ctx) ]
      ] |> List.concat)

Obviously as you can see there are a lot of functions that I didn't include in this example (for IP reasons) but it can give you an idea of how to go about doing it. (E.g. the tt function translates to a String which then is converted by text. It is a translation module to support different languages).

It is all really pretty straight forward if you just keep the Bootstrap internals in the JS/CSS space outside of Elm. In my experience it hasn't messed up the Elm diffing algorithms as suggested might be a problem.

Good luck! Elm really is a great language and paradigm for writing frontend code once you wrap your head around it.

like image 63
Anders Hansson Avatar answered Oct 23 '22 15:10

Anders Hansson


I don't think there is a perfectly clear answer for you, but here are some thoughts:

  • Elm can do almost everything that Javascript can, and that certainly includes dropdowns. But it does it in such a different way that you can't reuse JS code if you want to stay idiomatic.

  • you are not getting the dropdown because either

    • you have not loaded the bootstrap js; or
    • the BS js is loading fine but before the dom elements it wants to latch onto are present (because Elm adds them in its view method). In this case, you can use a Tick to trigger a new action that when it is received (on the next loop) you can use a port to trigger bootstrap to look again.
  • the real problem you can face mixing JS with Elm is that if the JS rewrites some of the DOM it can mess with Elm's diffing algorithms and leave you will all sorts of hard to debug issues.

If your dropdowns are not too complex in practise, then I'd suggest looking at bootstraps JS and seeing what it needs to attach / remove from the dom and try to emulate that in Elm. Perhaps the library you mention has done much of the work already in practise.

like image 20
Simon H Avatar answered Oct 23 '22 17:10

Simon H