Builder pattern for UI in Elm
Elm goodness.
If you stick this in the playground, it type checks and works.
module Main exposing (main)
import Browser
import Html exposing (Attribute, Html)
import Html exposing (div)
import Html.Attributes exposing (class)
import Html.Events
main =
Browser.sandbox { init = init (), update = update, view = view }
-- Builder
type Builder m = Builder
{ node : String
, attrs : List (Attribute m)
, children : List (Html m)
}
build : String -> List (Attribute m) -> List (Html m) -> Builder m
build node ass cs = Builder { node = node, attrs = ass, children = cs}
attrs : List (Attribute m) -> Builder m -> Builder m
attrs ass (Builder b) = Builder {b | attrs = b.attrs ++ ass}
children : (List (Html m) -> List (Html m)) -> Builder m -> Builder m
children f (Builder b) = Builder {b | children = f b.children}
done : Builder m -> Html m
done (Builder b)
= Html.node b.node b.attrs b.children
map : (m -> n) -> Builder m -> Builder n
map f (Builder b) =
let
newAttrs = List.map (Html.Attributes.map f) b.attrs
newChildren = List.map (Html.map f) b.children
in Builder { node = b.node, attrs = newAttrs, children = newChildren}
myDiv : () -> Html m
myDiv _ =
build "div" [] []
|> attrs [class "myClass"]
|> children (\kids -> [div [class "wrapper"] kids])
|> done
input : String -> (String -> m) -> Builder m
input value onInput =
build "input" [Html.Attributes.value value, Html.Events.onInput onInput] []
-- Generated code
type Form = Form
{ name : String
, age : String
}
newForm : {name : String, age : String} -> Form
newForm f = Form f
type Field
= Name
| Age
-- exposed getter so client can react to this
get : Field -> Form -> String
get f (Form form) = case f of
Name -> form.name
Age -> form.age
type alias FormMsg = {field: Field, string: String}
updateForm : Form -> FormMsg -> Form
updateForm (Form old) ({field, string}) =
case field of
Name -> Form {old | name = string}
Age -> Form {old | age = string}
-- have to pass in form to handle state, but you can have a configurable component
viewField : Form -> Field -> Builder FormMsg
viewField (Form form) f = case f of
Name -> input form.name (FormMsg f) |> attrs [class "input-name"]
Age -> input form.age (FormMsg f) |> attrs [class "input-age"]
-- Client (consuming the generated code)
type alias Model = { form : Form }
type Msg
= Clicked -- Another component alongside the form
| Child FormMsg
init : () -> Model
init _ = { form = newForm {name = "Jack", age = "28"}}
update : Msg -> Model -> Model
update msg model =
case msg of
Clicked -> model
Child childMsg -> {model | form = updateForm model.form childMsg}
wrapChildMsg : FormMsg -> Msg
wrapChildMsg m = Child m
view : Model -> Html Msg
view {form} =
let
nameInput = viewField form Name
|> attrs [class "subtle"]
|> map wrapChildMsg
|> done
ageInput = viewField form Age
|> attrs [class "subtle"]
|> map wrapChildMsg
|> done
in
div
[class "container"]
[ nameInput
, ageInput
]