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?
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.
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
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With