Generating HTML in F#

Angel Daniel Muñoz | April 26, 2022

 

We’ll discuss creating HTML strings (useful to create reports, pdfs etc.). Creating HTML in F# is quite simple since there are a bunch of supporting libraries. We'll review a few of those

The first two are DSLs in F# to build HTML, and the last one is an HTML scripting language for .NET (there's also Giraffe.Razor but that wouldn’t be discussed in this blog)

Giraffe

When it comes to HTML DSL, this is the most "traditional" one since it's not a flavor liked by many these days, but that doesn't take away its usefulness. Giraffe View Engine uses XmlNode as its building block, so you can produce XML as well with Giraffe View Engine!

The structure of a node function in Giraffe is as follows

  •  tagName [(* attribute list *)] [(* node list *)]

For example, creating <div class="my-class"></div> would be something like this

 

 

  • div [ _class "my-class" ] []

Attributes are prefixed with an underscore _ to prevent clashing with reserved words in F#
Let's look at a simple page with a header

 

 #r "nuget: Giraffe.ViewEngine"

open Giraffe.ViewEngine


let view =
    html [] [
        head [] [ title [] [ str "Giraffe" ] ]
        body [] [ header [] [ str "Giraffe" ] ]
    ]
let document = RenderView.AsString.htmlDocument view

printfn "%s" document

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

We assign the result of the html function to view, and then render the document as a string. We can then follow up and create a file with the IO API's we discussed in anearlier blog.

If you're using Saturn/Giraffe as your web framework you don't need to do the manual render. tThese frameworksprovide helpers that take care of this

You can also create functions to pre-define aspects of your views and override values if you deem necessary

 

 #r "nuget: Giraffe.ViewEngine"

open Giraffe.ViewEngine


let card attributes = 
    article [ yield! attributes; _class "card is-green"]

let cardFooter attributes =
    footer [ yield! attributes; _class "card-footer is-rounded"]

let cardHeader attributes =
    header [ yield! attributes; _class "card-header no-icons"]

let mySection = 
    div [] [
        card [ _id "my-card" ] [
            cardHeader [] [
                h1 [] [ str "This is my custom card"]
                img [ _src "https://some-image.com"; _class "card-header-image" ]
            ]

            p [] [ str "this is the body of the card" ]

            cardFooter [ _data "my-attr" "extra attributes" ] [
                p [] [ str "This is my footer"]
            ]
        ]
    ]

// note that we used htmlNode this time instead of htmlDocument
let document = RenderView.AsString.htmlNode mySection

printfn "%s" document
 

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

So, creating new "tags" is basically just creating a new function that accepts attributes and contents.

Feliz

The original Feliz DSL was made by @zaidajaj to use in Fable applications which are built generallyon top of React.js.

You can also check the Elmish book for more information about Fable and Feliz

 

 #r "nuget: Feliz.ViewEngine"

open Feliz.ViewEngine

let view = 
    Html.html [
        Html.head [ Html.title "Feliz" ]
        Html.body [
            Html.header [ prop.text "Feliz" ]
        ]
    ]

let document = Render.htmlDocument view

printfn "%s" document

 

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

 

The Feliz.ViewEngine namespace provides access to the Html and prop types. These have all the tags and attributes thatmay be required to construct HTML content. You can also open type the classes and access all of their static methods

 

 #r "nuget: Feliz.ViewEngine"

open Feliz.ViewEngine
open type Html
open type prop

let view = 
    html [
        head [ title "Feliz" ]
        body [
            header [ text "Feliz" ]
        ]
    ]

let document = Render.htmlDocument view

printfn "%s" document
 

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

Let's continue with the card component to see how you can compose different view elements. There are a few differences when you use the Feliz DSL, and these are primarily because of how the React API is defined. Properties cannot be mixed with children, so you have to pass prop.children to be able to define child elements for your components

 

 #r "nuget: Feliz.ViewEngine"

open Feliz.ViewEngine
open type Html
open type prop

// custom card, but you can't customize it's classes only the children elements
let card (content: ReactElement seq) = 
    article [
        className "card is-green"
        children content
    ]
// this card footer will allou you to define any property  of the footer element
let cardFooter content =
    footer [
        className "card-footer is-rounded"
        yield! content
    ]

let slotedHeader (content: ReactElement seq) = 
    header [
        className "card-header"
        // pass the contents directly to the children property
        children content
    ]

let customizableHeader content = 
    header [
        className "card-header"
        // allow any property to be set
        yield! content
    ]

let card1 = 
    div [
        card [
            // our slottedHeader only allows to pass children not props
            slotedHeader [
                h1 [ text "This is my custom card"]
                // className "" <- can't do this
            ]
            p [ text "this is the body of the card" ]
            cardFooter [
                custom("data-my-attr", "extra attributes")
                children (p [text "This is my footer"])
            ]
        ]
    ]

let card2 = 
    div [
        card [
            /// our customizable header allows us
            /// to pass properties as well as children elements
            customizableHeader [
                children (h1 [ text "This is my custom card"])
                className "custom class" 
            ]
            p [ text "this is the body of the card" ]
            cardFooter [
                custom("data-my-attr", "extra attributes")
                children (p [text "This is my footer"])
            ]
        ]
    ]

let r1 = Render.htmlView card1
let r2 = Render.htmlView card2

printfn "%s\n\n%s" r1 r2

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

Unlike Giraffe.ViewEngine, Feliz doesn't strip existing props so our header will end up like this


<header class="card-hearder" >
   <h1>This my custom card</h1>
</header>

In HTML, the last defined property always wins, so depending on your intent you might need properties or children and that might be the deciding factor between using one or the other.

Scriban

There are also text-based alternatives like Scriban which allow you to just write an html file and just fill it with data

 


#r "nuget: Scriban"

open Scriban

type Product = { name: string; price: float; description: string }

let renderProducts products = 
    let html = 
        """
        <ul id='products'>
        ❴❴ for product in products ❵❵
          <li>
            <h2> ❴❴  product.name ❵❵</h2>
                 Price: ❴❴  product.price ❵❵
                 ❴❴  product.description | string.truncate 15 ❵❵
          </li>
        ❴❴ end ❵❵
        </ul>
        """
    let result = Template.Parse(html)
    result.Render({| products = products |})

let result =
    renderProducts [
        { name = "Shoes"; price = 20.50; description = "The most shoes you'll ever see"}
        { name = "Potatoes"; price = 1.50; description = "The most potato you'll ever see" }
        { name = "Cars"; price = 10.3; description = "The most car you'll ever see" }
    ]

printfn "%s" result

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

If you have ever used jinja, moustache, handlebars, liquid and similar template engines, this will look familiar.We define an HTML string (which can be read from an HTML file on disk) we parse it, and then render it with a data source (in case we need one)

Scriban has plentyof helpers in its scripting language (likepipes that truncate a string to 15 characters)
If you want to compose components in the scriban templates, a different approach is required

 


#r "nuget: Scriban"

open System
open Scriban

type Product = 
    { name: string;
      price: float; 
      details : {| description: string |} }
// create a fragment/component html string
let detailDiv = 
    """
    <details>
        <summary> ❴❴ product.details.description | string.truncate 15 ❵❵ <summary>
        ❴❴ product.details.description ❵❵
    </details>
    """

let renderProducts products = 
    let html = 
        // here with the help of sprintf
        // and 
        // we use F# to pre-process the template
        sprintf
            """
            <ul id='products'>
            ❴❴ for product in products ❵❵
              <li>
                <h2>❴❴ product.name ❵❵></h2>
                     Price: ❴❴ product.price ❵❵
                     ❴❴ "%s" | object.eval_template ❵❵
              </li>
             ❴❴ end ❵❵
            </ul>
            """
            detailDiv
    let result = Template.Parse(html)
    result.Render({| products = products |})

let result =
    renderProducts [
        { name = "Shoes"
          price = 20.50
          details = 
            {| description = "The most shoes you'll ever see" |} }
        { name = "Potatoes"
          price = 1.50
          details =
            {| description = "The most potato you'll ever see"  |} }
        { name = "Cars"
          price = 10.3
          details =
            {| description = "The most car you'll ever see"  |} }
    ]

printfn "%s" result

To Run this, copy this content into a file named script.fsx (or whatever name you prefer) and type:

  • dotnet fsi run script.fsx

Whenusing F#, you can modify the strings before passing them to the final template. This approach is best suited to templates that are already set and and are not super dynamic. Plenty of dynamic operations with the scriban scripting capabilities can be performed inside the template.

I required, you can parse and compile multiple HTML templates and then pass them together to a layout and do a final render.

Closing Thoughts

Generating HTML isn't complex and use cases such asbuilding a resume from JSON to HTML, or building a report of prior year sales are relatively straightforward.

 

About Encora

Fast-growing tech companies partner with Encora to outsource product development and drive growth. Contact us to learn more about our software engineering capabilities.

Contact us

 

Insight Content

Categories

Share this Post

Featured Insights

Fill Out Later