10

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?

MaiaVictor
  • 51,090
  • 44
  • 144
  • 286
  • In the Elm Architecture, every component has a dependency to its direct children, but neither to its parents nor to its indirect children. That's reusability: you can use any component in any other component, and coupling will be restricted to only the parent communicating with its children. To achieve the modularity you might mean, you have to extract the common functionality in your components so that there are generic init-update-view functions that take a record representing the varying functionality of your components. – thSoft Dec 02 '15 at 10:39

4 Answers4

3

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.

Chad Gilbert
  • 36,115
  • 4
  • 89
  • 97
  • Sorry but none of those answers explain the most important bit: how is that modularizable? I.e., how are you able to include the app inside another? What kind of state pumbling is necessary? – MaiaVictor Dec 01 '15 at 16:41
  • 1
    @Viclib - Take a look at the [Elm Architecture Tutorial](https://github.com/evancz/elm-architecture-tutorial/). Example 1 builds a simple component, then the following examples build upon that component in a modular way. The same could be done with the example I posted, but the Tutorial is much more detailed. – Chad Gilbert Dec 01 '15 at 16:48
  • 1
    the example on the Elm Architecture Tutorial is not modularized. The class that uses the counter needs to manually store the state of the counter on its own model. After enough layers, the top-level model of your site will become a giant flat ADT with a mention to each mode of every child model. Imagine: "data MySiteAdt = ........ state_of_that_button_inside_that_other_div_inside_the_menu : IsSelected ....". That is not how modularization works. The parent app doesn't need to have many copies of the inner app's model specification. – MaiaVictor Dec 01 '15 at 17:13
  • True, the modularization described in the tutorial bubbles up so you have to take child state into consideration in all parents. But that's using StartApp. If you ditched StartApp and went staight to ports, you could achieve more granular modularization – Chad Gilbert Dec 01 '15 at 18:05
  • What do you mean by going straight to ports? – MaiaVictor Dec 01 '15 at 18:06
  • StartApp is a library that hides the underlying complexities of ports, foldp, and signals. It's more of a helper to get started quickly so there are lots of examples with it, but as you point out, it does break down with nested components. Applications that require greater complexity will most likely not even use StartApp. I haven't ventured far into that territory but [this post](https://yobriefca.se/blog/2015/08/02/deconstructing-your-first-elm-app/) offers insight into what actually goes on underneath StartApp. – Chad Gilbert Dec 01 '15 at 18:22
1

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
    }
Apanatshka
  • 5,958
  • 27
  • 38
  • 2
    Sorry but none of those answers explain the most important bit: how is that modularizable? I.e., how are you able to include the app inside another? What kind of state pumbling is necessary? – MaiaVictor Dec 01 '15 at 16:41
1

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

afcastano
  • 548
  • 3
  • 17
0

Here is another version of the same thing :)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import StartApp.Simple

type alias Model = Maybe String -- the id of the selected span

type Action = ButtonClick String

update : Action -> Model -> Model
update action model =
  case action of
    ButtonClick id ->
      Just id


view : Signal.Address Action -> Model -> Html
view address model =
  let
    renderButton id' label' =
      let
        selectedClass =
          case model of
            Just modelId -> if modelId == id' then " selected" else ""
            Nothing -> ""
      in
      span [ id id', class ("button" ++ selectedClass), onClick address (ButtonClick id') ] [ text label' ]
  in
  div []
    [ renderButton "foo" "Foo"
    , renderButton "bar" "Bar"
    , renderButton "tot" "Tot"
    ]


main =
  StartApp.Simple.start { model = Nothing, update = update, view = view }
devdave
  • 688
  • 5
  • 6
  • Sorry but none of those answers explain the most important bit: how is that modularizable? I.e., how are you able to include the app inside another? What kind of state pumbling is necessary? – MaiaVictor Dec 01 '15 at 16:41
  • 2
    You modularize Elm apps using nesting. So, you have a top level App components containing update and view functions. You can then nest a 'sub-module' inside this by giving it an entry in the top level model record for its state, a ChildAction type, an entry in the update function that sends ChildAction actions onto the child's update method and a Signal.forwardTo to tag actions coming out of the child. To be quite honest, its a lot of repeated boilerplate at the moment to make it work. I'm searching for a way to abstract this but haven't found anything elegant yet. – devdave Dec 01 '15 at 17:02