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.
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.
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.
Represents a node in the DOM tree that may have some children (a Stem
) or
no children (a Leaf
).
HtmlTree
to VirtualDom
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.
Create a Leaf
node with no attributes and no text.
leaf "br" --> <br>
Create a Leaf
node with text and no attributes.
"Hello, world!"
|> textWrapper "p"
--> <p>Hello, world!</p>
Create a Stem
node with no attributes and no text.
"Hello, world!"
|> textWrapper "p"
|> container "div"
--> <div><p>Hello, world!</p></div>
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>
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"
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!")
]
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")
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.
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)
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"
]
Add a new class assignment to the element at the root node of an HtmlTree
,
retaining any existing class assignments.
welcomeMessage
|> addClass "align-center"
Remove a class name from the element at the root node of an HtmlTree
.
welcomeMessage
|> removeClass "large-text"
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.
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")
Add text to the element at the root node of an HtmlTree
, replacing
any existing text
welcomeMessage =
leaf "p"
|> withText "Hello, world!"
Add new text to the element at the root node of an HtmlTree
, appended
after any existing text.
welcomeMessage
|> addText "!!"
Add new text to the element at the root node of an HtmlTree
, prepended
before any existing text.
welcomeMessage
|> prependText "#"
Flag the text at the root node of an HtmlTree
as
markdown; when
assembleHtml
is called, the text will be rendered using
Markdown.toHtml
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.
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.
Convenience function to add an id
attribute to the root element of an
HtmlTree
. Calls addAttribute
.
welcomeMessage
|> withId "welcomeMessage"
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!"
)
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")
)
Apply the modify function to every node in the tree.
newAttribute =
wrapList ("hidden", "True")
page
|> modifyAll (withAttributes newAttribute)
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")
)
HtmlElement
RecordsGiven an HtmlTree
, return the HtmlElement
at its root node.
Given an HtmlTree
, return a list containing every HtmlElement
in the
tree. This flattens the tree to provide more convenient access to record
fields.
Given an HtmlTree
, return a list containing every HtmlElement
with an
htmlTag
matching the first argument.
myPage
|> getElementsByTag "a"
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"
Given an HtmlElement
, return the value of its id
attribute, or
Nothing
if the id
attribute has not been defined.
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