diff --git a/README.md b/README.md
index bcc4dbb..3ee27fe 100644
--- a/README.md
+++ b/README.md
@@ -141,11 +141,95 @@ let doc (person : Person) =
]
```
+### Forms
+
+Forms are the lifeblood of HTML applications. A basic form using the markup module would like the following:
+
+```fsharp
+Elem.form [ Attr.method "post"; Attr.action "/submit" ] [
+ Elem.label [ Attr.for' "name" ] [ Text.raw "Name" ]
+ Elem.input [ Attr.id "name"; Attr.name "name"; Attr.typeText ]
+
+ Elem.input [ Attr.typeSubmit ]
+]
+```
+
+Expanding on this, we can create a more complex form involving multiple inputs and input types as follows:
+
+```fsharp
+Elem.form [ Attr.method "post"; Attr.action "/submit" ] [
+ Elem.label [ Attr.for' "name" ] [ Text.raw "Name" ]
+ Elem.input [ Attr.id "name"; Attr.name "name" ]
+
+ Elem.label [ Attr.for' "bio" ] [ Text.raw "Bio" ]
+ Elem.textarea [ Attr.name "id"; Attr.name "bio" ] []
+
+ Elem.label [ Attr.for' "hobbies" ] [ Text.raw "Hobbies" ]
+ Elem.select [ Attr.id "hobbies"; Attr.name "hobbies"; Attr.multiple ] [
+ Elem.option [ Attr.value "programming" ] [ Text.raw "Programming" ]
+ Elem.option [ Attr.value "diy" ] [ Text.raw "DIY" ]
+ Elem.option [ Attr.value "basketball" ] [ Text.raw "Basketball" ]
+ ]
+
+ Elem.fieldset [] [
+ Elem.legend [] [ Text.raw "Do you like chocolate?" ]
+ Elem.label [] [
+ Text.raw "Yes"
+ Elem.input [ Attr.typeRadio; Attr.name "chocolate"; Attr.value "yes" ] ]
+ Elem.label [] [
+ Text.raw "No"
+ Elem.input [ Attr.typeRadio; Attr.name "chocolate"; Attr.value "no" ] ]
+ ]
+
+ Elem.fieldset [] [
+ Elem.legend [] [ Text.raw "Subscribe to our newsletter" ]
+ Elem.label [] [
+ Text.raw "Receive updates about product"
+ Elem.input [ Attr.typeCheckbox; Attr.name "newsletter"; Attr.value "product" ] ]
+ Elem.label [] [
+ Text.raw "Receive updates about company"
+ Elem.input [ Attr.typeCheckbox; Attr.name "newsletter"; Attr.value "company" ] ]
+ ]
+
+ Elem.input [ Attr.typeSubmit ]
+]
+```
+
+A simple but useful _meta_-element `Elem.control` can reduce the verbosity required to create form outputs. The same form would look like:
+
+```fsharp
+Elem.form [ Attr.method "post"; Attr.action "/submit" ] [
+ Elem.control "name" [] [ Text.raw "Name" ]
+
+ Elem.controlTextarea "bio" [] [ Text.raw "Bio" ] []
+
+ Elem.controlSelect "hobbies" [ Attr.multiple ] [ Text.raw "Hobbies" ] [
+ Elem.option [ Attr.value "programming" ] [ Text.raw "Programming" ]
+ Elem.option [ Attr.value "diy" ] [ Text.raw "DIY" ]
+ Elem.option [ Attr.value "basketball" ] [ Text.raw "Basketball" ]
+ ]
+
+ Elem.fieldset [] [
+ Elem.legend [] [ Text.raw "Do you like chocolate?" ]
+ Elem.control "chocolate" [ Attr.id "chocolate_yes"; Attr.typeRadio ] [ Text.raw "yes" ]
+ Elem.control "chocolate" [ Attr.id "chocolate_no"; Attr.typeRadio ] [ Text.raw "no" ]
+ ]
+
+ Elem.fieldset [] [
+ Elem.legend [] [ Text.raw "Subscribe to our newsletter" ]
+ Elem.control "newsletter" [ Attr.id "newsletter_product"; Attr.typeCheckbox ] [ Text.raw "Receive updates about product" ]
+ Elem.control "newsletter" [ Attr.id "newsletter_company"; Attr.typeCheckbox ] [ Text.raw "Receive updates about company" ]
+ ]
+
+ Elem.input [ Attr.typeSubmit ]
+]
+```
+
### Merging Attributes
The markup module allows you to easily create components, an excellent way to reduce code repetition in your UI. To support runtime customization, it is advisable to ensure components (or reusable markup blocks) retain a similar function "shape" to standard elements. That being, `XmlAttribute list -> XmlNode list -> XmlNode`.
-This means that you will inevitably end up needing to combine your predefined `XmlAttribute list` with a list provided at runtime. To facilitate this, the `Attr.merge` function will group attributes by key, and concatenate the values in the case of `KeyValueAttribute`.
+This means that you will inevitably end up needing to combine your predefined `XmlAttribute list` with a list provided at runtime. To facilitate this, the `Attr.merge` function will group attributes by key, and intelligently concatenate the values in the case of additive attributes (i.e., `class`, `style` and `accept`).
```fsharp
open Falco.Markup
diff --git a/src/Falco.Markup/Elem.fs b/src/Falco.Markup/Elem.fs
index 1861766..ceae3a4 100644
--- a/src/Falco.Markup/Elem.fs
+++ b/src/Falco.Markup/Elem.fs
@@ -9,146 +9,407 @@ module Elem =
let createSelfClosing (tag : string) (attr : XmlAttribute list) =
SelfClosingNode (tag, attr)
+ //
// Main root
+
+ /// `` element
let html = create "html"
+ //
// Document metadata
+
+ /// `` element
let base' = createSelfClosing "base"
+
+ /// `
` element
let head = create "head"
+
+ /// `` element
let link = createSelfClosing "link"
+
+ /// `` element
let meta = createSelfClosing "meta"
+
+ /// `MycatisGrumpy!"
+
+type Product =
+ { Name : string
+ Price : float
+ Description : string }
+
+[]
+let ``Should produce valid html doc for large result`` () =
+ let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
+
+ let products =
+ [ 1..25000 ]
+ |> List.map (fun i -> { Name = sprintf "Name %i" i; Price = i |> float; Description = lorem})
+
+ let elem product =
+ Elem.li [] [
+ Elem.h2 [] [ Text.raw product.Name ]
+ Text.rawf "Only %f" product.Price
+ Text.raw product.Description ]
+
+ let productElems =
+ products
+ |> List.map elem
+ |> Elem.ul [ Attr.id "products" ]
+
+ let doc =
+ Elem.html [] [
+ Elem.body [] [
+ Elem.div [ Attr.class' "my-class" ] [ productElems ] ] ]
+
+ let render = renderHtml doc
+ render |> fun s -> s.Substring(0, 27) |> should equal ""
+ render |> fun s -> s.Substring(s.Length - 14, 14) |> should equal ""
+
+[]
+let ``Attr.merge should combine two XmlAttribute lists`` () =
+ Attr.merge
+ [ Attr.class' "ma2"; Attr.id "el" ]
+ [ Attr.id "some-el"; Attr.class' "bg-red"; Attr.readonly ]
+ |> should equal [ Attr.class' "ma2 bg-red"; Attr.id "some-el"; Attr.readonly ]
+
+[]
+let ``Attr.merge should work with bogus "class" NonValueAttr`` () =
+ Attr.merge
+ [ Attr.class' "ma2" ]
+ [ Attr.id "some-el"; Attr.class' "bg-red"; NonValueAttr("class") ]
+ |> should equal [ Attr.class' "ma2 bg-red"; Attr.id "some-el" ]
+
+[]
+let ``Attr.merge should combine two XmlAttribute lists factoring additive attributes`` () =
+ Attr.merge
+ [ Attr.name "test"; Attr.class' "ma2"; Attr.style "background: red"; Attr.accept "image/jpeg" ]
+ [ Attr.name "expected"; Attr.class' "pa2"; Attr.style "color: white"; Attr.accept "image/png" ]
+ |> should equal [ Attr.name "expected"; Attr.class' "ma2 pa2"; Attr.style "background: red; color: white"; Attr.accept "image/jpeg, image/png" ]
\ No newline at end of file
diff --git a/test/Falco.Markup.Tests/Falco.Markup.Tests.fsproj b/test/Falco.Markup.Tests/Falco.Markup.Tests.fsproj
index c5977b7..c4ca4ff 100644
--- a/test/Falco.Markup.Tests/Falco.Markup.Tests.fsproj
+++ b/test/Falco.Markup.Tests/Falco.Markup.Tests.fsproj
@@ -7,7 +7,8 @@
-
+
+
diff --git a/test/Falco.Markup.Tests/Tests.fs b/test/Falco.Markup.Tests/TestHelperTests.fs
similarity index 54%
rename from test/Falco.Markup.Tests/Tests.fs
rename to test/Falco.Markup.Tests/TestHelperTests.fs
index 6ca8591..cc79dc1 100644
--- a/test/Falco.Markup.Tests/Tests.fs
+++ b/test/Falco.Markup.Tests/TestHelperTests.fs
@@ -1,4 +1,4 @@
-module Falco.Tests.Markup
+module Falco.Tests.TestHelpersTests
open System
open Falco.Markup
@@ -6,140 +6,6 @@ open Falco.Markup.Svg
open FsUnit.Xunit
open Xunit
-[]
-let ``Text.empty should be empty`` () =
- renderNode Text.empty |> should equal String.Empty
-
-[]
-let ``Text.raw should not be encoded`` () =
- let rawText = Text.raw ""
- renderNode rawText |> should equal "
"
-
-[
]
-let ``Text.raw should not be encoded, but template applied`` () =
- let rawText = Text.rawf "%s
" "falco"
- renderNode rawText |> should equal "falco
"
-
-[]
-let ``Text.enc should be encoded`` () =
- let encodedText = Text.enc ""
- renderNode encodedText |> should equal "<div>"
-
-[
]
-let ``Text.comment should equal HTML comment`` () =
- let rawText = Text.comment "test comment"
- renderNode rawText |> should equal ""
-
-[]
-let ``Self-closing tag should render with trailing slash`` () =
- let t = Elem.createSelfClosing "hr" []
- renderNode t |> should equal "
"
-
-[]
-let ``Self-closing tag with attrs should render with trailing slash`` () =
- let t = Elem.createSelfClosing "hr" [ Attr.class' "my-class" ]
- renderNode t |> should equal "
"
-
-[]
-let ``Standard tag should render with multiple attributes`` () =
- let t = Elem.create "div" [ Attr.create "class" "my-class"; Attr.autofocus; Attr.create "data-bind" "slider" ] []
- renderNode t |> should equal ""
-
-[]
-let ``Script should contain src, lang and async`` () =
- let t = Elem.script [ Attr.src "http://example.org/example.js"; Attr.lang "javascript"; Attr.async ] []
- renderNode t |> should equal ""
-
-[]
-let ``Should produce valid html doc`` () =
- let doc =
- Elem.html [] [
- Elem.body [] [
- Elem.div [ Attr.class' "my-class" ] [
- Elem.h1 [] [ Text.raw "hello" ] ] ] ]
- renderHtml doc |> should equal "hello
"
-
-[]
-let ``Should create valid html button`` () =
- let doc = Elem.button [ Attr.onclick "console.log(\"test\")"] [ Text.raw "click me" ]
- renderNode doc |> should equal "";
-
-[]
-let ``Should produce valid xml doc`` () =
- let doc =
- Elem.create "books" [] [
- Elem.create "book" [] [
- Elem.create "name" [] [ Text.raw "To Kill A Mockingbird" ]
- ]
- ]
-
- renderXml doc |> should equal "To Kill A Mockingbird"
-
-[]
-let ``Should produce valid svg`` () =
- // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text#example
- let doc =
- Templates.svg (0, 0, 240, 80) [
- Elem.style [] [
- Text.raw ".small { font: italic 13px sans-serif; }"
- Text.raw ".heavy { font: bold 30px sans-serif; }"
- Text.raw ".Rrrrr { font: italic 40px serif; fill: red; }"
- ]
- Elem.text [ Attr.x "20"; Attr.y "35"; Attr.class' "small" ] [ Text.raw "My" ]
- Elem.text [ Attr.x "40"; Attr.y "35"; Attr.class' "heavy" ] [ Text.raw "cat" ]
- Elem.text [ Attr.x "55"; Attr.y "55"; Attr.class' "small" ] [ Text.raw "is" ]
- Elem.text [ Attr.x "65"; Attr.y "55"; Attr.class' "Rrrrr" ] [ Text.raw "Grumpy!" ]
- ]
-
- renderNode doc |> should equal ""
-
-type Product =
- { Name : string
- Price : float
- Description : string }
-
-[]
-let ``Should produce valid html doc for large result`` () =
- let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
-
- let products =
- [ 1..25000 ]
- |> List.map (fun i -> { Name = sprintf "Name %i" i; Price = i |> float; Description = lorem})
-
- let elem product =
- Elem.li [] [
- Elem.h2 [] [ Text.raw product.Name ]
- Text.rawf "Only %f" product.Price
- Text.raw product.Description ]
-
- let productElems =
- products
- |> List.map elem
- |> Elem.ul [ Attr.id "products" ]
-
- let doc =
- Elem.html [] [
- Elem.body [] [
- Elem.div [ Attr.class' "my-class" ] [ productElems ] ] ]
-
- let render = renderHtml doc
- render |> fun s -> s.Substring(0, 27) |> should equal ""
- render |> fun s -> s.Substring(s.Length - 14, 14) |> should equal ""
-
-[]
-let ``Attr.merge should combine two XmlAttribute lists`` () =
- Attr.merge
- [ Attr.class' "ma2" ]
- [ Attr.id "some-el"; Attr.class' "bg-red"; Attr.readonly ]
- |> should equal [ Attr.class' "ma2 bg-red"; Attr.id "some-el"; Attr.readonly ]
-
-[]
-let ``Attr.merge should work with bogus "class" NonValeAttr`` () =
- Attr.merge
- [ Attr.class' "ma2" ]
- [ Attr.id "some-el"; Attr.class' "bg-red"; NonValueAttr("class") ]
- |> should equal [ Attr.class' "ma2 bg-red"; Attr.id "some-el" ]
-
module TestHelpersTests =
open Falco.Markup.TestHelpers
@@ -184,7 +50,7 @@ module TestHelpersTests =
let ``Multiple form inputs should return name/value pairs`` () =
let xml = Elem.form [] [
Elem.input [ Attr.name "name"; Attr.value "falco" ]
- Elem.input [ Attr.name "age"; Attr.value "3"; Attr.type' "number" ] ]
+ Elem.input [ Attr.name "age"; Attr.value "3"; Attr.typeNumber ] ]
let nameValues = renderNameValues xml
@@ -205,7 +71,7 @@ module TestHelpersTests =
Elem.input [ Attr.name "name"; Attr.value "falco" ]
Elem.input [ Attr.value "bad" ]
Elem.textarea [] [ Text.raw "bad" ]
- Elem.input [ Attr.name "age"; Attr.value "3"; Attr.type' "number" ] ]
+ Elem.input [ Attr.name "age"; Attr.value "3"; Attr.typeNumber ] ]
let nameValues = renderNameValues xml
@@ -317,20 +183,21 @@ module TestHelpersTests =
[]
let ``Kitchen sink`` () =
let xml = Elem.form [] [
+ Elem.h1 [] [ Text.raw "HJERE" ]
Elem.input [ Attr.name "first_name"; Attr.value "first_name_value" ]
Elem.textarea [ Attr.name "long_text" ] [ Text.raw "long_text_value" ]
Elem.div [] [
- Elem.input [ Attr.type' "radio"; Attr.name "radio"; Attr.value "value1"; Attr.checked' ]
- Elem.input [ Attr.type' "radio"; Attr.name "radio"; Attr.value "value2" ]
- Elem.input [ Attr.type' "radio"; Attr.name "radio"; Attr.value "value3" ]
+ Elem.input [ Attr.typeRadio; Attr.name "radio"; Attr.value "value1"; Attr.checked' ]
+ Elem.input [ Attr.typeRadio; Attr.name "radio"; Attr.value "value2" ]
+ Elem.input [ Attr.typeRadio; Attr.name "radio"; Attr.value "value3" ]
]
Elem.div [] [
- Elem.input [ Attr.type' "checkbox"; Attr.name "checkbox"; Attr.value "value1"; Attr.checked' ]
- Elem.input [ Attr.type' "checkbox"; Attr.name "checkbox"; Attr.value "value2"; Attr.checked' ]
- Elem.input [ Attr.type' "checkbox"; Attr.name "checkbox"; Attr.value "value3" ]
+ Elem.input [ Attr.typeCheckbox; Attr.name "checkbox"; Attr.value "value1"; Attr.checked' ]
+ Elem.input [ Attr.typeCheckbox; Attr.name "checkbox"; Attr.value "value2"; Attr.checked' ]
+ Elem.input [ Attr.typeCheckbox; Attr.name "checkbox"; Attr.value "value3" ]
]
Elem.select [ Attr.name "select" ] [
@@ -345,10 +212,10 @@ module TestHelpersTests =
Elem.option [ Attr.value "option3"; Attr.selected ] [ Text.raw "Option 3" ]
]
- Elem.input [ Attr.type' "submit" ]
+ Elem.input [ Attr.typeSubmit ]
]
let nameValues = renderNameValues xml
nameValues
- |> should equal "first_name=first_name_value&long_text=long_text_value&radio=value1&checkbox=value1&checkbox=value2&select=option2&multiselect=option2&multiselect=option3"
+ |> should equal "first_name=first_name_value&long_text=long_text_value&radio=value1&checkbox=value1&checkbox=value2&select=option2&multiselect=option2&multiselect=option3"
\ No newline at end of file