This is an alternative site for discovering Elm packages. You may be looking for the official Elm package site instead.

ModularDesign

Assemble your UI from modular, modifiable HTML components

The ModularDesign package provides an alternative, non-standard API for generating HTML and building reactive user interfaces in Elm. The package is built on top of the standard VirtualDom and Html libraries, so the underlying JavaScript implementation is no different.

The main disadvantage of the standard API is that once a chunk of HTML has been constructed, e.g.,

welcomeMessage =
  div [] [ p [] [ text "Hello, World!" ] ]

there is no direct way of looking inside that chunk to get information about its elements or their attributes. For example, it would not be possible to pass welcomeMessage to a function that would add a style attribute to the p element or change the text to "Hello, Universe!" and return the result. With the standard libraries, to make either of these modifications, we would need to re-write the nested Html function calls with modified arguments or insert conditionals that would change the arguments passed to the function in response to data. This limitation takes away some of the appeal of using a functional style of programming for front-end web development.

The ModularDesign library solves this problem by creating a set of types that provide a representation of the HTML DOM in Elm, allowing access to each node's internal data. In the Modular Design API, an HtmlElement is a record that encodes an element's tag, assigned class names, other assigned attributes, and, when applicable, its internal text, event handlers, and/or the type of input it captures. The union type HtmlTree defines a recursive tree where each node contains an HtmlElement and some nodes also contain a list of child HtmlTree nodes. This data structure allows an HtmlTree to be passed to a function that will access its internal data, build a modified HtmlTree, and return the result, just as one can do with any other Elm type.

With the Modular Design API, the code to produce welcomeMessage may be written like this:

welcomeMessage =
  container "div" [textWrapper "p" "Hello, world!"]

Or, using functional operators, like this:

welcomeMessage =
  "Hello, world!"
    |> textWrapper "p"
    |> wrapList
    |> container "div"

Suppose that we would like to be able to change the style of the text after this chunk of HTML has been encoded and assigned to a variable name. We can do this by adding a CSS class to the p element as follows:

welcomeMessage
  |> modifyMatchingTag ("p", addClass "large-bold-text")

Note, however, that if there were multiple p elements in the tree, this function call would add the class "large-bold-text" to all of them. An alternative is to define the id attribute of the element we wish to modify and then use the function modifyMatchingId:

welcomeMessage =
  "Hello, world!"
    |> textWrapper "p"
    |> withId "messageText"
    |> wrapList
    |> container "div"

welcomeMessage
  |> modifyMatchingId ("messageText", addClass "large-bold-text")

The text of the message can be modified in a similar way:

welcomeMessage
  |> modifyMatchingId ("messageText", withText "Hello, Universe!")

And so on.

Full working examples can be found here.

The core package library includes basic constructors for HtmlTree nodes, sets of functions for modifying the element records of root nodes and internal nodes, a function to render an HtmlTree to VirtualDom, and various helpers. A separate package module, ModularDesign.FormInput provides an API for capturing, accessing, and validating form input.

New in this release (2.0.0):

  • Function names for constructors have been standardized: Names starting with with replace then contents of the corresponding record field; names starting with add append new content after existing content.

  • Support for markdown syntax using the textAsMarkdown function

  • ModularDesign.Stylesheet provides a framework for generating CSS rules and import directives, allowing you to embed a global stylesheet in your HtmlTree.

  • ModularDesign.Operators includes some custom infix operators for use with the Modular Design framework

  • Generic helper functions can now be found in ModularDesign.Helpers

Component and pattern libraries for UI design are planned for future releases.

HTML DOM Representation

type alias HtmlElement msg = { htmlTag : String , attributes : List (String, String) , actions : List (String, msg) , classes : List String , styles : List (String, String) , text : Maybe String , markdown : Bool , observer : Maybe (Attribute msg) , inputType : Maybe (Json.Value -> TypedInput) }

Represents a HTML element with the following record fields:

  • htmlTag: A valid HTML tag. When rendering to VirtualDom, the tag is passed as a string argument to the Html.node function.

  • attributes: A list of name-value pairs representing HTML attributes. The Html.Attributes function corresponding to name will be called and, where necessary, the value will be converted from a string to the appropriate type (note that errors in converting a boolean string to a Bool default to False). Any name for which there is no corresponding function will be passed to Html.Attributes.attribute along with its value, creating a custom attribute.

  • actions: A list of action-message pairs. As defined here, "actions" include all events that do not capture form input. Following the typical pattern of an Elm program, a "message" is a user-defined type that tells the program what updates to perform on the model via pattern matching.

  • classes: A list of class names. The list is concatinated into one string and passed to Html.Attributes.class.

  • styles: A list of name-value pairs representing CSS properties. The list is passed to Html.Attributes.style. It is generally better practice to set CSS classes on elements and the define styles in a CSS file, but the style attribute can be used to override class defaults.

  • text: A string of text, or Nothing. When rendered as HTML, text will be inserted after the element tag and before any child elements.

  • markdown: A boolean value indicating whether the element's text should be parsed as markdown

  • observer: An Html.Attribute encoding an event handler that captures form input, or Nothing. An "observer" differs from an "action" in that it captures one or more input values, and so requires a Json Decoder to read that input. The built-in observers in the Html.Events library are onInput and onCheck. Custom observers may be created using the Html.Events.on function, which takes an event name (as a string, without the "on" prefix) and a Decoder as arguments. The Modular Design API also includes the helper functions captureOnSubmit, fieldDecoder, and formDecoder, which make it easier to construct observers for capturing form input.

  • inputType: A constructor that accepts a Json Value and returns TypedInput, or Nothing. The constructor may be one of: StringInput, IntInput, FloatInput, BoolInput, NullInput, CustomInput. This record field is ignored when rendering the element to VirtualDom; its purpose is to allow form data to be aggregated while preserving type specifications on input fields, such that type checking can occur downstream in the program.

type TypedInput = StringInput Json.Value | IntInput Json.Value | FloatInput Json.Value | BoolInput Json.Value | NullInput Json.Value | CustomInput Json.Value

Represents a JavaScript value with a type specification. Used to implement type-checking in functions for capturing and reading form input. See the ModularDesign.FormInput documentation to find out how this works.

type HtmlTree msg = Leaf (HtmlElement msg) | Stem (HtmlElement msg) (List (HtmlTree msg))

Represents a node in the DOM tree that may have some children (a Stem) or no children (a Leaf).

Rendering an HtmlTree to VirtualDom

assembleHtml : HtmlTree msg -> Html msg

To render HTML in the browser, an HtmlTree must be converted to a VirtualDom.Node (note that Html.Html is an alias for VirtualDom.Node). Calling assembleHtml on an HtmlTree recurses down the tree, constructing the VirtualDom representation node by node.

Node Constructors

leaf : String -> HtmlTree msg

Create a Leaf node with no attributes and no text.

leaf "br"   --> <br>
textWrapper : String -> String -> HtmlTree msg

Create a Leaf node with text and no attributes.

"Hello, world!"
  |> textWrapper "p"

--> <p>Hello, world!</p>
container : String -> List (HtmlTree msg) -> HtmlTree msg

Create a Stem node with no attributes and no text.

"Hello, world!"
  |> textWrapper "p"
  |> container "div"

--> <div><p>Hello, world!</p></div>

Modifying the Root Node

appendNodes : List (HtmlTree msg) -> HtmlTree msg -> HtmlTree msg

Append child nodes to the root node of an HtmlTree, replacing any existing children, and return the result. The main use of this function is to convert a Leaf to a Stem, which is helpful when nesting text elements.

"Hello, world!"
  |> textWrapper "p"
  |> appendNodes
    [ leaf "br"
    , "Awesome!" |> textWrapper "strong"
    ]
  |> container "div"

--> <div><p>Hello, world!<br><strong>Awesome!</strong></p></div>
withTag : String -> HtmlTree msg -> HtmlTree msg

Modify the HTML tag of the element at the root node of an HtmlTree. Replaces the existing tag.

welcomeMessage =
  "Hello, world!"
    |> textWrapper "p"

welcomeMessage
  |> withTag "span"
withAttributes : List (String, String) -> HtmlTree msg -> HtmlTree msg

Add a list of attributes (name-value pairs) to the element at the root node of an HtmlTree, replacing any existing attributes

welcomeMessage
  |> withAttributes
    [ ("id", "welcomeMessage")
    , ("title", "Hello again!")
    ]
addAttribute : (String, String) -> HtmlTree msg -> HtmlTree msg

Add a new attribute (name-value pair) to the element at the root node of an HtmlTree. If the new attribute has the same name as an existing attribute, the new value replaces the old one; otherwise, existing attributes are retained.

welcomeMessage
  |> addAttribute ("id", "welcomeMessage")
withActions : List (String, msg) -> HtmlTree msg -> HtmlTree msg

Add a list of actions to the element at the root node of an HtmlTree, replacing any existing actions. Actions must be encoded as action-message pairs. As defined here, "actions" include all events that do not capture form input. Following the typical pattern of an Elm program, a "message" is a user-defined type that tells the program what updates to perform on the model via pattern matching.

"Click here and see what happens!"
  |> textWrapper "p"
  |> withAttributes
    [ ("hidden", toString model) ]
  |> withActions
    [ ("click", HideMessage) ]

See examples/Actions.elm for a full working example.

addAction : (String, msg) -> HtmlTree msg -> HtmlTree msg

Add a new action to the element at the root node of an HtmlTree. If a new action-message pair has the same action as an existing one, the new message replaces the old one; otherwise, existing action-message pairs are retained.

myTextElement
  |> addAction ("click", HideMessage)
withClasses : List String -> HtmlTree msg -> HtmlTree msg

Add a list of class names to the element at the root node of an HtmlTree, replacing any existing class assignments.

welcomeMessage =
  "Hello, world!"
    |> textWrapper "p"
    |> withClasses
      [ "large-text"
      , "align-center"
      ]
addClass : String -> HtmlTree msg -> HtmlTree msg

Add a new class assignment to the element at the root node of an HtmlTree, retaining any existing class assignments.

welcomeMessage
    |> addClass "align-center"
removeClass : String -> HtmlTree msg -> HtmlTree msg

Remove a class name from the element at the root node of an HtmlTree.

welcomeMessage
  |> removeClass "large-text"
withStyles : List (String, String) -> HtmlTree msg -> HtmlTree msg

Add a list of style declarations (name-value pairs) to the element at the root node of an HtmlTree, replacing any existing styles. Style declarations added in this way are defined via the element's style attribute, which means they override style declarations assigned to tag, class, and id selectors in global stylesheets.

welcomeMessage =
  "Hello, world!"
    |> textWrapper "p"
    |> withStyles
      [ ("font-size", "2em")
      , ("text-align", "center")
      ]

See ModularDesign.Stylesheet for a more general approach to defining CSS rules and generating a global stylesheet in Elm.

addStyle : (String, String) -> HtmlTree msg -> HtmlTree msg

Add a new style declaration (name-value pair) to the element at the root node of an HtmlTree. If a new style has the same name as an existing style, the new value replaces the old one; otherwise, existing style declarations are retained.

welcomeMessage
    |> addStyle ("text-align", "center")
withText : String -> HtmlTree msg -> HtmlTree msg

Add text to the element at the root node of an HtmlTree, replacing any existing text

welcomeMessage =
  leaf "p"
    |> withText "Hello, world!"
addText : String -> HtmlTree msg -> HtmlTree msg

Add new text to the element at the root node of an HtmlTree, appended after any existing text.

welcomeMessage
    |> addText "!!"
prependText : String -> HtmlTree msg -> HtmlTree msg

Add new text to the element at the root node of an HtmlTree, prepended before any existing text.

welcomeMessage
    |> prependText "#"
textAsMarkdown : HtmlTree msg -> HtmlTree msg

Flag the text at the root node of an HtmlTree as markdown; when assembleHtml is called, the text will be rendered using Markdown.toHtml

withObserver : Attribute msg -> HtmlTree msg -> HtmlTree msg

Add an observer to the element at the root node of an HtmlTree, encoded as an Html.Attribute. An "observer" differs from an "action" in that it captures one or more input values, and so requires a Json Decoder to read that input. The built-in observers in the Html.Events package are onInput and onCheck. Custom observers may be created using the Html.Events.on function, which takes an event name (as a string, without the "on" prefix) and a Decoder as arguments. The Modular Design API also includes the helper functions captureOnSubmit, fieldDecoder, and formDecoder, which make it easier to construct observers for capturing form input.

leaf "input"
  |> withAttributes
    [ ("type", "checkbox")
    , ("checked", toString model)
    ]
  |> withObserver (Events.onCheck Checked)

See examples/Checkboxes.elm and examples/RadioButtons.elm for full working examples.

setInputType : (Json.Value -> TypedInput) -> HtmlTree msg -> HtmlTree msg

Set an input type for the root node of an HtmlTree. May be one of: StringInput, IntInput, FloatInput, BoolInput, NullInput, CustomInput.

leaf "input"
  |> withAttributes
    [ ("type", "text")
    , ("id", "birthYear")
    ]
  |> setInputType IntInput

See examples/FieldDecoder.elm for a full working example.

withId : String -> HtmlTree msg -> HtmlTree msg

Convenience function to add an id attribute to the root element of an HtmlTree. Calls addAttribute.

welcomeMessage
  |> withId "welcomeMessage"

Modifying Internal Nodes

modifyMatchingId : (String, HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg

Given a string representing an id and a modify function that accepts an HtmlTree and returns a modified HtmlTree, apply the modify function to every node in the tree whose root element has a matching id. Note that HTML elements should be assigned unique id strings, so in theory the modify function should only be applied to one node.

welcomeMessage =
  "Hello, world!"
    |> textWrapper "p"
    |> withId "messageText"
    |> wrapList
    |> container "div"

welcomeMessage
  |> modifyMatchingId
    ( "messageText"
    , withText "Hello, Universe!"
    )
modifyMatchingTag : (String, HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg

Given a string representing an HTML tag and a modify function that accepts an HtmlTree and returns a modified HtmlTree, apply the modify function to every node in the tree whose root element has a matching HTML tag.

  page
    |> modifyMatchingTag
      ( "button"
      , addAttribute ("disabled", "True")
      )
modifyAll : (HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg

Apply the modify function to every node in the tree.

newAttribute =
  wrapList ("hidden", "True")

page
  |> modifyAll (withAttributes newAttribute)
modifyAny : (HtmlElement msg -> Bool, HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg

Given an expression generator that accepts an HtmlElement and returns a Bool and a modify function that accepts and HtmlTree and returns a modified HtmlTree, apply the modify function to every node in the tree for which the resulting expression evaluates to True.

let
  disabledIsTrue =
    getAttrValue "disabled"
      >> Maybe.withDefault "false"
      >> String.toLower
      >> (==) "true"

in
  page
    |> modifyAny
      ( disabledIsTrue
      , addAttribute ("disabled", "false")
      )

Accessing HtmlElement Records

rootElement : HtmlTree msg -> HtmlElement msg

Given an HtmlTree, return the HtmlElement at its root node.

listElements : HtmlTree msg -> List (HtmlElement msg)

Given an HtmlTree, return a list containing every HtmlElement in the tree. This flattens the tree to provide more convenient access to record fields.

getElementsByTag : String -> HtmlTree msg -> List (HtmlElement msg)

Given an HtmlTree, return a list containing every HtmlElement with an htmlTag matching the first argument.

myPage
  |> getElementsByTag "a"

Accessing Record Fields

getAttrValue : String -> HtmlElement msg -> Maybe String

Given an HtmlElement, return the value for the attribute whose name matching the first argument, or Nothing if no attribute with that name has been defined.

myElement
  |> getAttrValue "disabled"
getId : HtmlElement msg -> Maybe String

Given an HtmlElement, return the value of its id attribute, or Nothing if the id attribute has not been defined.

hasValue : String -> String -> HtmlElement msg -> Bool

Given an HtmlElement, lookup the attribute whose name matches the first argument; if its value matches the second argument, return True; if the value does not match, or the name is not found, return False.

if (rootElement myButton |> "disabled" `hasValue` "True") then
  myButton
    |> addAttribute ("disabled", "False")
else
  myButton
module ModularDesign exposing
  ( HtmlElement, TypedInput(..), HtmlTree(..), assembleHtml, leaf, textWrapper
  , container , appendNodes, withTag, withAttributes, addAttribute, withActions
  , addAction, withClasses, addClass, removeClass, withStyles, addStyle
  , withText, addText, prependText, textAsMarkdown, withObserver, setInputType
  , withId, modifyMatchingId, modifyMatchingTag, modifyAll, modifyAny
  , rootElement, listElements, getElementsByTag, getAttrValue, getId, hasValue
  )

{-|

## Assemble your UI from modular, modifiable HTML components

The `ModularDesign` package provides an alternative, non-standard API for
generating HTML and building reactive user interfaces in Elm. The package is
built on top of the standard `VirtualDom` and `Html` libraries, so the
underlying JavaScript implementation is no different.

The main disadvantage of the standard API is that once a chunk of HTML has been
constructed, e.g.,

    welcomeMessage =
      div [] [ p [] [ text "Hello, World!" ] ]

there is no direct way of looking inside that chunk to get information about its
elements or their attributes. For example, it would not be possible to pass
`welcomeMessage` to a function that would add a style attribute to the `p`
element or change the text to "Hello, Universe!" and return the result. With the
standard libraries, to make either of these modifications, we would need to
re-write the nested `Html` function calls with modified arguments or insert
conditionals that would change the arguments passed to the function in response
to data. This limitation takes away some of the appeal of using a functional
style of programming for front-end web development.

The `ModularDesign` library solves this problem by creating a set of types that
provide a representation of the HTML DOM in Elm, allowing access to each node's
internal data. In the Modular Design API, an `HtmlElement` is a record that
encodes an element's tag, assigned class names, other assigned attributes, and,
when applicable, its internal text, event handlers, and/or the type of input it
captures. The union type `HtmlTree` defines a recursive tree where each node
contains an `HtmlElement` and some nodes also contain a list of child `HtmlTree`
nodes. This data structure allows an `HtmlTree` to be passed to a function that
will access its internal data, build a modified `HtmlTree`, and return the
result, just as one can do with any other Elm type.

With the Modular Design API, the code to produce `welcomeMessage` may be
written like this:

    welcomeMessage =
      container "div" [textWrapper "p" "Hello, world!"]

Or, using functional operators, like this:

    welcomeMessage =
      "Hello, world!"
        |> textWrapper "p"
        |> wrapList
        |> container "div"

Suppose that we would like to be able to change the style of the text after this
chunk of HTML has been encoded and assigned to a variable name. We can do this
by adding a CSS class to the `p` element as follows:

    welcomeMessage
      |> modifyMatchingTag ("p", addClass "large-bold-text")

Note, however, that if there were multiple `p` elements in the tree, this
function call would add the class "large-bold-text" to all of them. An
alternative is to define the `id` attribute of the element we wish to modify and
then use the function `modifyMatchingId`:

    welcomeMessage =
      "Hello, world!"
        |> textWrapper "p"
        |> withId "messageText"
        |> wrapList
        |> container "div"

    welcomeMessage
      |> modifyMatchingId ("messageText", addClass "large-bold-text")

The text of the message can be modified in a similar way:

    welcomeMessage
      |> modifyMatchingId ("messageText", withText "Hello, Universe!")

And so on.

Full working examples can be found
[here](https://github.com/danielnarey/elm-modular-design/tree/master/examples).

The core package library includes basic constructors for `HtmlTree` nodes, sets
of functions for modifying the element records of root nodes and internal nodes,
a function to render an `HtmlTree` to `VirtualDom`, and various helpers. A
separate package module, `ModularDesign.FormInput` provides an API for
capturing, accessing, and validating form input.

**New in this release (2.0.0):**

- Function names for constructors have been standardized: Names starting with
`with` replace then contents of the corresponding record field; names
starting with `add` append new content after existing content.

- Support for markdown syntax using the `textAsMarkdown` function

- `ModularDesign.Stylesheet` provides a framework for generating CSS rules and
import directives, allowing you to embed a global stylesheet in your `HtmlTree`.

- `ModularDesign.Operators` includes some custom infix operators for use with
the Modular Design framework

- Generic helper functions can now be found in `ModularDesign.Helpers`

Component and pattern libraries for UI design are planned for future releases.


# HTML DOM Representation
@docs HtmlElement, TypedInput, HtmlTree

# Rendering an `HtmlTree` to `VirtualDom`
@docs assembleHtml

# Node Constructors
@docs leaf, textWrapper, container

# Modifying the Root Node
@docs appendNodes, withTag, withAttributes, addAttribute, withActions
@docs addAction, withClasses, addClass, removeClass, withStyles, addStyle
@docs withText, addText, prependText, textAsMarkdown, withObserver
@docs setInputType, withId

# Modifying Internal Nodes
@docs modifyMatchingId, modifyMatchingTag, modifyAll, modifyAny

# Accessing `HtmlElement` Records
@docs rootElement, listElements, getElementsByTag

# Accessing Record Fields
@docs getAttrValue, getId, hasValue

-}

import ModularDesign.Operators exposing (..)
import ModularDesign.Helpers exposing (wrapList)
import Internal.AttributeLookup as Lookup
import Internal.Util as Util
import Html exposing (Html, Attribute)
import Html.Attributes as Attr
import Html.Events as Events
import Json.Decode as Json exposing (Decoder)
import Dict exposing (Dict)
import Markdown
import List
import String


--TYPE DECLARATIONS

{-| Represents a HTML element with the following record fields:

- __htmlTag__: A valid [HTML tag](http://www.w3schools.com/tags/). When
rendering to `VirtualDom`, the tag is passed as a string argument to the
[`Html.node`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html#node)
function.

- __attributes__: A list of *name-value* pairs representing
[HTML attributes](http://www.w3schools.com/tags/ref_attributes.asp). The
[`Html.Attributes`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes)
function corresponding to *name* will be called and, where necessary, the
*value* will be converted from a string to the appropriate type (note that
errors in converting a boolean string to a `Bool` default to `False`). Any
*name* for which there is no corresponding function will be passed to
[`Html.Attributes.attribute`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#attribute)
along with its *value*, creating a custom attribute.

- __actions__: A list of *action-message* pairs. As defined here, "actions"
include all events that __do not__ capture form input. Following the typical
pattern of an Elm program, a "message" is a user-defined type that tells the
program what updates to perform on the model via pattern matching.

- __classes__: A list of class names. The list is concatinated into one string
and passed to
[Html.Attributes.class](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#class).

- __styles__: A list of *name-value* pairs representing
[CSS properties](http://www.w3schools.com/cssref/).
The list is passed to
[Html.Attributes.style](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#style).
It is generally better practice to set CSS classes on elements and the define
styles in a CSS file, but the style attribute can be used to override class
defaults.

- __text__: A string of text, or `Nothing`. When rendered as HTML, text will be
inserted after the element tag and before any child elements.

- __markdown__: A boolean value indicating whether the element's text should
be parsed as [markdown](https://en.wikipedia.org/wiki/Markdown)

- __observer__: An
[`Html.Attribute`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html#Attribute)
encoding an event handler that captures form input, or `Nothing`. An "observer"
differs from an "action" in that it captures one or more input values, and so
requires a `Json`
[`Decoder`](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#Decoder)
to read that input. The built-in observers in the `Html.Events` library
are
[`onInput`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#onInput)
and
[`onCheck`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#onCheck).
Custom observers may be created using the
[`Html.Events.on`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#on)
function, which takes an
[event name](http://www.w3schools.com/jsref/dom_obj_event.asp) (as a string,
without the "on" prefix) and a
[`Decoder`](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#Decoder)
as arguments. The Modular Design API also includes the helper functions
`captureOnSubmit`, `fieldDecoder`, and `formDecoder`, which make it easier
to construct observers for capturing form input.

- __inputType__: A constructor that accepts a `Json` `Value` and returns
`TypedInput`, or `Nothing`. The constructor may be one of: `StringInput`,
`IntInput`, `FloatInput`, `BoolInput`, `NullInput`, `CustomInput`. This record
field is ignored when rendering the element to `VirtualDom`; its purpose is to
allow form data to be aggregated while preserving type specifications on input
fields, such that type checking can occur downstream in the program.
-}
type alias HtmlElement msg =
  { htmlTag : String
  , attributes : List (String, String)
  , actions : List (String, msg)
  , classes : List String
  , styles : List (String, String)
  , text : Maybe String
  , markdown : Bool
  , observer : Maybe (Attribute msg)
  , inputType : Maybe (Json.Value -> TypedInput)
  }


{-| Represents a JavaScript value with a type specification. Used to implement
type-checking in functions for capturing and reading form input. See the
`ModularDesign.FormInput` documentation to find out how this works.
-}
type TypedInput
  = StringInput Json.Value
  | IntInput Json.Value
  | FloatInput Json.Value
  | BoolInput Json.Value
  | NullInput Json.Value
  | CustomInput Json.Value


{-| Represents a node in the DOM tree that may have some children (a `Stem`) or
no children (a `Leaf`).
-}
type HtmlTree msg
  = Leaf (HtmlElement msg)
  | Stem (HtmlElement msg) (List (HtmlTree msg))


--CONSTRUCTOR FUNCTIONS

{-| Create a `Leaf` node with no attributes and no text.

    leaf "br"   --> <br>
-}
leaf : String -> HtmlTree msg
leaf htmlTag =
  Leaf
    { htmlTag = htmlTag
    , attributes = []
    , actions = []
    , classes = []
    , styles = []
    , text = Nothing
    , markdown = False
    , observer = Nothing
    , inputType = Nothing
    }


{-| Create a `Leaf` node with text and no attributes.

    "Hello, world!"
      |> textWrapper "p"

    --> <p>Hello, world!</p>
-}
textWrapper : String -> String -> HtmlTree msg
textWrapper htmlTag textString =
  Leaf
    { htmlTag = htmlTag
    , attributes = []
    , actions = []
    , classes = []
    , styles = []
    , text = Just textString
    , markdown = False
    , observer = Nothing
    , inputType = Nothing
    }


{-| Create a `Stem` node with no attributes and no text.

    "Hello, world!"
      |> textWrapper "p"
      |> container "div"

    --> <div><p>Hello, world!</p></div>
-}
container : String -> List (HtmlTree msg) -> HtmlTree msg
container htmlTag childList =
  Stem
    { htmlTag = htmlTag
    , attributes = []
    , actions = []
    , classes = []
    , styles = []
    , text = Nothing
    , markdown = False
    , observer = Nothing
    , inputType = Nothing
    }
    childList


--RENDERING HTML

{-| To render HTML in the browser, an `HtmlTree` must be converted to a
[`VirtualDom.Node`](http://package.elm-lang.org/packages/elm-lang/virtual-dom/latest/VirtualDom#Node)
(note that `Html.Html` is an alias for `VirtualDom.Node`). Calling
`assembleHtml` on an `HtmlTree` recurses down the tree, constructing the
`VirtualDom` representation node by node.
-}
assembleHtml : HtmlTree msg -> Html msg
assembleHtml leafOrStem =
  let
    renderText (maybeText, isMarkdown) =
      case maybeText of
        Just someText ->
          if isMarkdown then
            someText
              |> Markdown.toHtml []
              |> wrapList

          else
            someText
              |> Html.text
              |> wrapList

        Nothing ->
          []

  in
    case leafOrStem of
      Leaf someElement ->
        (someElement.text, someElement.markdown)
          |> renderText
          |> (someElement |> renderNode)

      Stem someElement childList ->
        childList
          .|> assembleHtml
          |> (++) ((someElement.text, someElement.markdown) |> renderText)
          |> (someElement |> renderNode)


{-| Constructs a `VirtualDom.Node` from an `HtmlElement` and a list of child
nodes. Called recursively by `assembleHtml` on each node of the tree.
-}
renderNode : HtmlElement msg -> List (Html msg) -> Html msg
renderNode someElement childList =
  let
    classAttribute =
      if someElement.classes |> List.isEmpty then
        []

      else
        someElement.classes
          |> String.join " "
          |> Attr.class
          |> wrapList

    styleAttribute =
      if someElement.styles |> List.isEmpty then
        []

      else
        someElement.styles
          |> Attr.style
          |> wrapList

    toAction (event, msg) =
      msg
        |> Json.succeed
        |> Events.on event

    resolveObserver maybeObserver =
      case maybeObserver of
        Just maybeObserver ->
          maybeObserver
            |> wrapList

        Nothing ->
          []

    attributes =
      classAttribute ++ styleAttribute
        |++ (someElement.attributes .|> Lookup.toAttribute)
        |++ (someElement.actions .|> toAction)
        |++ (someElement.observer |> resolveObserver)

  in
    (someElement.htmlTag, attributes, childList)
      @@@|> Html.node


--MANIPULATING NODES

{-| Append child nodes to the root node of an `HtmlTree`, replacing any existing
children, and return the result. The main use of this function is to convert a
`Leaf` to a `Stem`, which is helpful when nesting text elements.

    "Hello, world!"
      |> textWrapper "p"
      |> appendNodes
        [ leaf "br"
        , "Awesome!" |> textWrapper "strong"
        ]
      |> container "div"

    --> <div><p>Hello, world!<br><strong>Awesome!</strong></p></div>
-}
appendNodes : List (HtmlTree msg) -> HtmlTree msg -> HtmlTree msg
appendNodes newChildList leafOrStem =
  case leafOrStem of
    Leaf someElement ->
      Stem someElement newChildList

    Stem someElement childList ->
      Stem someElement newChildList


{-| Apply an update function to the element at the root node of an `HtmlTree`
-}
modifyRoot : (HtmlElement msg -> HtmlElement msg) -> HtmlTree msg -> HtmlTree msg
modifyRoot updateFunction leafOrStem =
  case leafOrStem of
    Leaf someElement ->
      Leaf (updateFunction someElement)

    Stem someElement childList ->
      Stem (updateFunction someElement) childList


{-| Modify the HTML tag of the element at the root node of an `HtmlTree`.
Replaces the existing tag.

    welcomeMessage =
      "Hello, world!"
        |> textWrapper "p"

    welcomeMessage
      |> withTag "span"
-}
withTag : String -> HtmlTree msg -> HtmlTree msg
withTag newTag leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | htmlTag =
          newTag
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a list of attributes (*name-value* pairs) to the element at the root
node of an `HtmlTree`, *replacing* any existing attributes

    welcomeMessage
      |> withAttributes
        [ ("id", "welcomeMessage")
        , ("title", "Hello again!")
        ]
-}
withAttributes : List (String, String) -> HtmlTree msg -> HtmlTree msg
withAttributes attributeList leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | attributes =
          attributeList
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a new attribute (*name-value* pair) to the element at the root
node of an `HtmlTree`. If the new attribute has the same *name* as an existing
attribute, the new *value* replaces the old one; otherwise, existing attributes
are retained.

    welcomeMessage
      |> addAttribute ("id", "welcomeMessage")
-}
addAttribute : (String, String) -> HtmlTree msg -> HtmlTree msg
addAttribute newAttribute leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | attributes =
          someElement.attributes
            |> Util.removeMatchingKeys [ fst newAttribute ]
            |:: newAttribute
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a list of actions to the element at the root node of an `HtmlTree`,
*replacing* any existing actions. Actions must be encoded as *action-message*
pairs. As defined here, "actions" include all events that __do not__ capture
form input. Following the typical pattern of an Elm program, a "message" is a
user-defined type that tells the program what updates to perform on the model
via pattern matching.

    "Click here and see what happens!"
      |> textWrapper "p"
      |> withAttributes
        [ ("hidden", toString model) ]
      |> withActions
        [ ("click", HideMessage) ]

See [examples/Actions.elm](https://github.com/danielnarey/elm-modular-design/tree/master/examples)
for a full working example.
-}
withActions : List (String, msg) -> HtmlTree msg -> HtmlTree msg
withActions actionList leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | actions =
          actionList
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a new action to the element at the root node of an `HtmlTree`. If a new
*action-message* pair has the same *action* as an existing one, the new
*message* replaces the old one; otherwise, existing *action-message* pairs are
retained.

    myTextElement
      |> addAction ("click", HideMessage)

-}
addAction : (String, msg) -> HtmlTree msg -> HtmlTree msg
addAction newAction leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | actions =
          someElement.actions
            |> Util.removeMatchingKeys [ fst newAction ]
            |:: newAction
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a list of class names to the element at the root node of an `HtmlTree`,
*replacing* any existing class assignments.

    welcomeMessage =
      "Hello, world!"
        |> textWrapper "p"
        |> withClasses
          [ "large-text"
          , "align-center"
          ]
-}
withClasses : List String -> HtmlTree msg -> HtmlTree msg
withClasses classList leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | classes =
          classList
      }

  in
    leafOrStem
      |> modifyRoot updateFunction



{-| Add a new class assignment to the element at the root node of an `HtmlTree`,
*retaining* any existing class assignments.

    welcomeMessage
        |> addClass "align-center"
-}
addClass : String -> HtmlTree msg -> HtmlTree msg
addClass newClass leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | classes =
          someElement.classes ++ [ newClass ]
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a list of style declarations (*name-value* pairs) to the element at the
root node of an `HtmlTree`, *replacing* any existing styles. Style declarations
added in this way are defined via the element's `style` attribute, which means
they override style declarations assigned to tag, class, and id selectors in
global stylesheets.

    welcomeMessage =
      "Hello, world!"
        |> textWrapper "p"
        |> withStyles
          [ ("font-size", "2em")
          , ("text-align", "center")
          ]

See `ModularDesign.Stylesheet` for a more general approach to defining CSS rules
and generating a global stylesheet in Elm.
-}
withStyles : List (String, String) -> HtmlTree msg -> HtmlTree msg
withStyles styleList leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | styles =
          styleList
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add a new style declaration (*name-value* pair) to the element at the root
node of an `HtmlTree`. If a new style has the same *name* as an
existing style, the new *value* replaces the old one; otherwise, existing
style declarations are retained.

    welcomeMessage
        |> addStyle ("text-align", "center")
-}
addStyle : (String, String) -> HtmlTree msg -> HtmlTree msg
addStyle newStyle leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | styles =
          someElement.styles
            |> Util.removeMatchingKeys [ fst newStyle ]
            |:: newStyle
      }

  in
    leafOrStem
      |> modifyRoot updateFunction

{-| Remove a class name from the element at the root node of an `HtmlTree`.

    welcomeMessage
      |> removeClass "large-text"
-}
removeClass : String -> HtmlTree msg -> HtmlTree msg
removeClass classToRemove leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | classes =
          someElement.classes
            |> Util.removeMatchingEntries [ classToRemove ]
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add text to the element at the root node of an `HtmlTree`, *replacing*
any existing text

    welcomeMessage =
      leaf "p"
        |> withText "Hello, world!"
-}
withText : String -> HtmlTree msg -> HtmlTree msg
withText newText leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | text =
          Just newText
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add new text to the element at the root node of an `HtmlTree`, *appended
after* any existing text.

    welcomeMessage
        |> addText "!!"
-}
addText : String -> HtmlTree msg -> HtmlTree msg
addText newText leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | text =
          someElement.text
            ?= ""
            |++ newText
            |> (\n -> Just n)
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add new text to the element at the root node of an `HtmlTree`, *prepended
before* any existing text.

    welcomeMessage
        |> prependText "#"
-}
prependText : String -> HtmlTree msg -> HtmlTree msg
prependText newText leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | text =
          newText
            |++ (someElement.text ?= "")
            |> (\n -> Just n)
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Flag the text at the root node of an `HtmlTree` as
[markdown](https://en.wikipedia.org/wiki/Markdown); when
`assembleHtml` is called, the text will be rendered using
[`Markdown.toHtml`](package.elm-lang.org/packages/evancz/elm-markdown/latest/Markdown#toHtml)
-}
textAsMarkdown : HtmlTree msg -> HtmlTree msg
textAsMarkdown leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | markdown =
          True
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Add an observer to the element at the root node of an `HtmlTree`, encoded as
an [`Html.Attribute`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html#Attribute).
An "observer" differs from an "action" in that it captures one or more input
values, and so requires a `Json`
[`Decoder`](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#Decoder)
to read that input. The built-in observers in the `Html.Events` package
are
[`onInput`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#onInput)
and
[`onCheck`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#onCheck).
Custom observers may be created using the
[`Html.Events.on`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Events#on)
function, which takes an
[event name](http://www.w3schools.com/jsref/dom_obj_event.asp) (as a string,
without the "on" prefix) and a
[`Decoder`](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#Decoder)
as arguments. The Modular Design API also includes the helper functions
`captureOnSubmit`, `fieldDecoder`, and `formDecoder`, which make it easier
to construct observers for capturing form input.

    leaf "input"
      |> withAttributes
        [ ("type", "checkbox")
        , ("checked", toString model)
        ]
      |> withObserver (Events.onCheck Checked)

See
[examples/Checkboxes.elm](https://github.com/danielnarey/elm-modular-design/tree/master/examples)
and
[examples/RadioButtons.elm](https://github.com/danielnarey/elm-modular-design/tree/master/examples)
for full working examples.
-}
withObserver : Attribute msg -> HtmlTree msg -> HtmlTree msg
withObserver newObserver leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | observer =
          Just newObserver
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Set an input type for the root node of an `HtmlTree`. May be one of:
`StringInput`, `IntInput`, `FloatInput`, `BoolInput`, `NullInput`,
`CustomInput`.

    leaf "input"
      |> withAttributes
        [ ("type", "text")
        , ("id", "birthYear")
        ]
      |> setInputType IntInput

See
[examples/FieldDecoder.elm](https://github.com/danielnarey/elm-modular-design/tree/master/examples)
for a full working example.
-}
setInputType : (Json.Value -> TypedInput) -> HtmlTree msg -> HtmlTree msg
setInputType someInputType leafOrStem =
  let
    updateFunction someElement =
      { someElement
      | inputType =
          Just someInputType
      }

  in
    leafOrStem
      |> modifyRoot updateFunction


{-| Convenience function to add an `id` attribute to the root element of an
`HtmlTree`. Calls `addAttribute`.

    welcomeMessage
      |> withId "welcomeMessage"
-}
withId : String -> HtmlTree msg -> HtmlTree msg
withId idString leafOrStem =
  leafOrStem
    |> addAttribute ("id", idString)


--MODIFY CHILD NODES

{-| Given a string representing an `id` and a modify function that accepts an
`HtmlTree` and returns a modified `HtmlTree`, apply the modify function to
every node in the tree whose root element has a matching `id`. Note that HTML
elements should be assigned unique `id` strings, so in theory the modify
function should only be applied to one node.

    welcomeMessage =
      "Hello, world!"
        |> textWrapper "p"
        |> withId "messageText"
        |> wrapList
        |> container "div"

    welcomeMessage
      |> modifyMatchingId
        ( "messageText"
        , withText "Hello, Universe!"
        )
-}
modifyMatchingId : (String, HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg
modifyMatchingId (idString, modifyFunction) leafOrStem =
  let
    expressionGenerator =
      getId
        >> Maybe.withDefault "NoID"
        >> (==) idString

  in
    leafOrStem
      |> modifyAny (expressionGenerator, modifyFunction)


{-| Given a string representing an HTML tag and a modify function that accepts an
`HtmlTree` and returns a modified `HtmlTree`, apply the modify function to
every node in the tree whose root element has a matching HTML tag.

      page
        |> modifyMatchingTag
          ( "button"
          , addAttribute ("disabled", "True")
          )
-}
modifyMatchingTag : (String, HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg
modifyMatchingTag (tagString, modifyFunction) leafOrStem =
  let
    expressionGenerator =
      (\n -> n.htmlTag)
        >> (==) tagString

  in
    leafOrStem
      |> modifyAny (expressionGenerator, modifyFunction)


{-| Apply the modify function to every node in the tree.

    newAttribute =
      wrapList ("hidden", "True")

    page
      |> modifyAll (withAttributes newAttribute)
-}
modifyAll : (HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg
modifyAll modifyFunction leafOrStem =
  leafOrStem
    |> modifyAny (always True, modifyFunction)


{-| Given an expression generator that accepts an `HtmlElement` and returns a
`Bool` and a modify function that accepts and `HtmlTree` and returns a modified
`HtmlTree`, apply the modify function to every node in the tree for which the
resulting expression evaluates to `True`.

    let
      disabledIsTrue =
        getAttrValue "disabled"
          >> Maybe.withDefault "false"
          >> String.toLower
          >> (==) "true"

    in
      page
        |> modifyAny
          ( disabledIsTrue
          , addAttribute ("disabled", "false")
          )
-}
modifyAny : (HtmlElement msg -> Bool, HtmlTree msg -> HtmlTree msg) -> HtmlTree msg -> HtmlTree msg
modifyAny (expressionGenerator, modifyFunction) leafOrStem =
  case leafOrStem of
    Leaf someElement ->
      if someElement |> expressionGenerator then
        Leaf someElement
          |> modifyFunction

      else
        Leaf someElement

    Stem someElement childList ->
      if someElement |> expressionGenerator then
        childList
          .|> modifyAny (expressionGenerator, modifyFunction)
          |> Stem someElement
          |> modifyFunction

      else
        childList
          .|> modifyAny (expressionGenerator, modifyFunction)
          |> Stem someElement


--HELPERS FOR ACCESSING ELEMENT RECORDS

{-| Given an `HtmlTree`, return the `HtmlElement` at its root node.
-}
rootElement : HtmlTree msg -> HtmlElement msg
rootElement leafOrStem =
  case leafOrStem of
    Leaf someElement ->
      someElement

    Stem someElement childList ->
      someElement


{-| Given an `HtmlTree`, return a list containing every `HtmlElement` in the
tree. This flattens the tree to provide more convenient access to record
fields.
-}
listElements : HtmlTree msg -> List (HtmlElement msg)
listElements leafOrStem =
  let
    generateElementList elementList leafOrStem =
      case leafOrStem of
        Leaf someElement ->
          someElement
            |> wrapList

        Stem someElement childList ->
          childList
            .|> generateElementList elementList
            |> List.concat
            |> (::) someElement

  in
    leafOrStem
      |> generateElementList []


{-| Given an `HtmlTree`, return a list containing every `HtmlElement` with an
`htmlTag` matching the first argument.

    myPage
      |> getElementsByTag "a"
-}
getElementsByTag : String -> HtmlTree msg -> List (HtmlElement msg)
getElementsByTag forTag leafOrStem =
  leafOrStem
    |> listElements
    |> List.filter (\n -> n.htmlTag == forTag)


{-| Given an `HtmlElement`, return the *value* for the attribute whose *name*
matching the first argument, or `Nothing` if no attribute with that *name* has
been defined.

    myElement
      |> getAttrValue "disabled"
-}
getAttrValue : String -> HtmlElement msg -> Maybe String
getAttrValue attrName someElement =
  someElement.attributes
    |> Dict.fromList
    |> Dict.get attrName


{-| Given an `HtmlElement`, return the *value* of its `id` attribute, or
`Nothing` if the `id` attribute has not been defined.
-}
getId : HtmlElement msg -> Maybe String
getId someElement =
  someElement
    |> getAttrValue "id"


{-| Given an `HtmlElement`, lookup the attribute whose *name* matches the first
argument; if its *value* matches the second argument, return `True`; if the
*value* does not match, or the *name* is not found, return `False`.

    if (rootElement myButton |> "disabled" `hasValue` "True") then
      myButton
        |> addAttribute ("disabled", "False")
    else
      myButton
-}
hasValue : String -> String -> HtmlElement msg -> Bool
hasValue attrName attrValue someElement =
  case someElement |> getAttrValue attrName of
    Just someValue ->
      attrValue == someValue

    Nothing ->
      False