Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you create stateful, modular, self-contained web components in Elm?

Tags:

javascript

elm

Suppose you want to create a UI which has 3 buttons. When you click in one of them, the others are released. In JavaScript, you could write:

var elements = ["Foo","Bar","Tot"].map(function(name){
  var element = document.getElementById(name);
  element.onclick = function(){
    elements.map(function(element){
      element.className = 'button';
    });
    element.className = 'button selected';
  };
  return element;
});
.button {
  border: 1px solid black;
  cursor: pointer;
  margin: 4px;
  padding: 4px;
}
.selected {
  background-color: #DDDDDD;
}
<div>
  <span id='Foo' class='button'>Foo</span>
  <span id='Bar' class='button'>Bar</span>
  <span id='Tot' class='button'>Tot</span>
</div>
  

That is stateful, but not modular, self contained nor pure. In fact, it is so bad the state (a ternary bit) isn't even obvious. You can not inject it inside another model, how many times you want.

Most of the answers provided here so far are stateful, but not modular. The issue is that using that strategy, you can't drop a component inside another without the parent knowing about the children's model. Ideally, that would be abstracted away - the parent shouldn't need to mention the model of the child nodes on its own model, nor manually plumbing state from parent to node should be necessary. If I want to create a list of the app above, I don't want to store the state of each child node on the parent.

How do you create stateful, modular, self-contained web components in Elm?

like image 502
MaiaVictor Avatar asked Dec 01 '15 10:12

MaiaVictor


3 Answers

Elm can satisfy each of those requirements, making your component stateful, modular, self-contained, and pure. Here's an example in Elm using StartApp.Simple (pardon the inline styling):

import StartApp.Simple exposing (start)
import Html exposing (Html, div, span, text)
import Html.Attributes exposing (id, class, style)
import Html.Events exposing (onClick)

type alias Model =
  { elements : List String
  , selected : Maybe String
  }

init : Model
init =
  { elements = [ "Foo", "Bar", "Tot" ]
  , selected = Nothing
  }

type Action
  = Select String

update : Action -> Model -> Model
update action model =
  case action of
    Select s ->
      { model | selected = Just s }

view : Signal.Address Action -> Model -> Html
view address model =
  let 
    btn txt =
      span
        [ id txt
        , buttonStyle txt
        , onClick address <| Select txt
        ] [ text txt ]

    buttonStyle txt =
      style (
        [ ("border", "1px solid black")
        , ("cursor", "pointer")
        , ("margin", "4px")
        , ("solid", "4px")
        ] ++ (styleWhenSelected txt))

    styleWhenSelected txt =
      case model.selected of
        Nothing -> []
        Just s ->
          if s == txt then
            [ ("background-color", "#DDDDDD") ]
          else
            []
  in
    div [] <| List.map btn model.elements


main =
  start
    { model = init
    , update = update
    , view = view
    }

You have a clearly defined, statically typed model, an explicit and limited number of actions that can be performed against that model, and a type-safe html rendering engine.

Take a look at the Elm Architecture Tutorial for more information.

like image 156
Chad Gilbert Avatar answered Oct 31 '22 19:10

Chad Gilbert


I just saw Chad's answer, while I was writing mine. This one also uses the Elm Architecture, but uses you original class names in the Html, and has a "stronger" model. The nice part about the stronger model is that you literally see the three bits like you mentioned in your question. There is also less implicit coupling between the name of the id and the actual button. But it leaves you with some duplicated names that you may or may not want. Depends on how much you want this coupling.

import StartApp.Simple as StartApp
import Html as H exposing (Html)
import Html.Attributes as HA
import Html.Events as HE

type alias Model =
  { foo : Bool
  , bar : Bool
  , tot : Bool
  }

type Action
  = Foo
  | Bar
  | Tot

model : Model
model =
  { foo = False
  , bar = False
  , tot = False
  }

update : Action -> Model -> Model
update clicked _ =
  case clicked of
    Foo -> { model | foo = True }
    Bar -> { model | bar = True }
    Tot -> { model | tot = True }

view : Signal.Address Action -> Model -> Html
view addr { foo, bar, tot } =
  [ foo, bar, tot ]
  |> List.map2 (viewButton addr) buttons
  |> H.div []

buttons : List (String, Action)
buttons =
  [ ("Foo", Foo)
  , ("Bar", Bar)
  , ("Tot", Tot)
  ]

viewButton : Signal.Address Action -> (String, Action) -> Bool -> Html
viewButton addr (id, action) selected =
  H.span
    [ HA.id id
    , HA.classList
      [ ("button", True)
      , ("selected", selected)
      ]
    , HE.onClick addr action
    ]
    [ H.text id
    ]

buttonStyle =

main =
  StartApp.start
    { model = model
    , view = view
    , update = update
    }
like image 43
Apanatshka Avatar answered Oct 31 '22 21:10

Apanatshka


As devdave suggest, nesting is the only way that I have found to modularise components.

I have implemented a similar example which you can see live here: http://afcastano.github.io/elm-nested-component-communication/

The idea is that children expose functions to get the properties of their own model. This functions can in turn call even more nested functions for children components.

Check out the Readme.md of this repo for code examples: https://github.com/afcastano/elm-nested-component-communication

like image 40
afcastano Avatar answered Oct 31 '22 20:10

afcastano