Generando HTML en F#

Cosas simples en fsharp

Hola! esta es la cuarta entrada en "Cosas simples en F#"

Hoy hablaremos acerca de como podemos producir cadenas de texto que contienen HTML en F# (utiles para reportes, pdfs o similares). Producir HTML en F# es muy sencillo para ser honesto, hay muchas librerias que te permiten hacerlo, hablaremos de algunas que he usado con anterioridad.

Los primeros dos son DSL's para construir HTML, el ultimo es un lenguaje de scripting para .NET (aunque tambien existe Giraffe.Razor pero el dia de hoy no lo mencionare)

Giraffe

En cuanto a versiones (sabores como me gusta decirle) de DSL para HTML en F# Giraffe es la mas tradicional y probablemente el sabor menos apreciado hoy en dia por la comunidad. Sin embargo eso no disminuye su utilidad, Giraffe.ViewEngine utiliza nodos de XML como su bloque lego central, es decir puedes producir tanto HTML como XML (lo cual puede ser util en algunos contextos asi que puntos extras).

La estructura de cada etiqueta de este DSL es la siguiente:

  • etiqueta [(* lista de atributos *)] [(* lista de nodos *)]

por ejemplo para crear <div class="mi-clase"></div> seria algo como esto:

  • div [ _class "mi-clase" ] []

Los atributos tienen un prefijo de un guion bajo _ para prevenir choques con palabras reservadas de F# (como class y type).

Vamos a ver una pagina sencilla con un 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

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

En este caso, asignamos los resultados de la funcion de html a la variable vista, luego renderizamos el contenido como una cadena de texto. Si has seguido esta serie de posts, te daras una idea de que puedes usar las funciones del namespace System.IO para producir un archivo de HTML con ese contenido

Si estas usando Saturn/Giraffe como tu framework web, no es necesario hacer el render manual, estos frameworks tienen funciones utiltarias que se encargan de ello (como lo veremos en el siguiente post).

Tambien puedes crear funciones para pre-definir aspectos de tus vistas o incluso sobre-escribir valores en caso de que lo consideres necesario, por ejemplo:

#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"]
            ]
        ]
    ]

// notese que usamos htmlNode en esta ocasion en lugar de htmlDocument
let document = RenderView.AsString.htmlNode mySection

printfn "%s" document

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

Nota: yield! es una palabra reservada que asigna los contenidos de alguna secuencia (IEnumerable<T>) a la secuencia actual. Por ejemplo: let a = [1;2;3] let b = [yield! a; 4;5;6], b ahora contiene [1;2;3;4;5;6]

Asi que para crear nuevas etiquetas colo es necesario hacer una funcion nueva que acepte atributos y nodos!

Feliz

El DSL original de Feliz fue hecho por Zaid Ajaj (Quien por cierto, produce EXCELENTE contenido OSS en F#, deberian revisar su perfil de github una maravilla 🤌🏼) para ser usado en aplicaciones de Fable que para fines practicos es un DSL encima del React.js si revisamos en el editor, veremos que view es de tipo ReactElement Pero no te preocupes, no hay Javascript aqui, solo es el tipo de dato, el DSL de Feliz mejora mucho la legibilidad y reduce la cantidad de caracteres que debemos escribir para producir HTML tambien en el backend, no solo en el frontend.

No soy el master en composicion de vistas con Feliz/React asi que toma el siguiente ejemplo con su correspondiente granito de arena, en lo personal te recomendaria que revisaras el Libro de Elmish que contiene patrones precisamente para esta clase de casos.

#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

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

Cuando abres el namespace Feliz.ViewEngine tienes acceso a las clases estaticas Html y prop, estas contienen las etiquetas y los atributos que puedes necesitar para construir HTML, si te sientes cansado de escribir Html.etiqueta y prop.atributo puedes usar open type Html y open type prop que funcionan como si abrieras estas como si fuera un namespace.

#r "nuget: Feliz.ViewEngine"

open Feliz.ViewEngine
// notese el open type
open type Html
open type prop

let view =
    // ya no tuvimos que escribir Html.html!
    html [
        head [ title "Feliz" ]
        body [
            header [ text "Feliz" ]
        ]
    ]

let document = Render.htmlDocument view

printfn "%s" document

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

usando open type nos salvamos de unos cuantos tecleos, pero podemos tener algunos problemas de choques con los nombres, en casos como esos, siempre puedes volver a escribir Html.etiqueta y prop.atributo en caso de ser necesario.

Vamos a continuar con el componente Card para ver como podemos componer nuestras vistas (como si fueran legos) con diferentes elementos (componentes) en si. En el caso de Feliz, children es una propiedad para alojar a los hijos de algun componente padre y como F# es un lenguaje de tipado estricto no puedes combinar diferentes tipos de datos asi que tendremos que pasar el contenido a children en lugar de ponerlo todo en props.

#r "nuget: Feliz.ViewEngine"

open Feliz.ViewEngine
open type Html
open type prop

// card personalizada, no puedes personalizar sus clases, pero si sus hijos
let card (content: ReactElement seq) =
    article [
        className "card is-green"
        children content
    ]
// usando yield! podemos colocar cualquier otra propiedad (tanto children como cualquier `prop.*`)
let cardFooter content =
    footer [
        className "card-footer is-rounded"
        yield! content
    ]

let slotedHeader (content: ReactElement seq) =
    header [
        className "card-header"
        // pasamos el contenido directo a los hijos
        children content
    ]

let customizableHeader content =
    header [
        className "card-header"
        // hagase garras y ponga lo que quiera
        yield! content
    ]

let card1 =
    div [
        card [
            // solo permitimos hijos, mas no propiedades
            slotedHeader [
                h1 [ text "This is my custom card"]
                // className "" nel, no se arma
            ]
            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 [
            // En este caso nuestro encabezado personalizado nos permite
            // pasar propiedades asi como elementos
            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

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

Pero... aqui va a suceder algo interesante, a diferencia de Giraffe.ViewEngine, Feliz no remueve propiedades existentes, asi que nuestro header va a quedar algo asi:

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

Y en el caso de HTML, la ultima pripiedad siempre gana, asi que ten esto en cuenta cuando quieras sobre-escribir algo o algo no se vea como deberia. Tambien puede ser un factor determinante de como es que podrias usar yield!.

Scriban

Si tu, como yo no puedes simplemente deshacerte de HTML por que... pos por que si, entonces Scriban es una excelente alternativa para ti, ya que te permite escribir HTML como en la mayoria de lenguajes para templates existentes (handlebars, mustache, liquid templates, por ejemplo) y solo necesitas llenarlo de datos al final

#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 = "Zapatos"; price = 20.50; description = "Los Zapatos mas Zapatezcos que veras..."}
        { name = "Papas"; price = 1.50; description = "Las papas mas papezcas que veras..." }
        { name = "Cars"; price = 10.3; description = "Los Carros mas Carrozos que veras..." }
    ]

printfn "%s" result

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

Si has usado Jinja, moustache, handlebars, como lo mencionaba la sintaxis te sera similar, basicamente solo tienes que definir HTML y hacerle un parsing/render con una fuente de datos (en caso de que uses variables en el template)

Scriban tiene un monton de utilidades en su lenguaje de scripting (como esos pipes | para truncar el texto)

En caso de que quieras componer vistas en Scriban, el acercamiento que se tiene que dar es muy, muy diferente.

#r "nuget: Scriban"

open System
open Scriban

type Product =
    { name: string;
      price: float;
      details : {| description: string |} }
// crea un fragmento/componente en una cadena de texto con html
let detailDiv =
    """
    <details>
        <summary> {{ product.details.description | string.truncate 15 }} <summary>
        {{ product.details.description }}
    </details>
    """

let renderProducts products =
    let html =
        /// aqui usamos una funcion de F# `sprintf`
        /// y {{ "%s" | object.eval_template }}
        /// en este caso usamos F# para pre procesar ese 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 = "Zapatos"
          price = 20.50
          details =
            {| description = "Los Zapatos mas Zapatezcos que veras..." |} }
        { name = "Papas"
          price = 1.50
          details =
            {| description = "Las papas mas papezcas que veras..."  |} }
        { name = "Carros"
          price = 10.3
          details =
            {| description = "Los Carros mas Carrozos que veras..."  |} }
    ]

printfn "%s" result

Para correr esto, copia el contenido en algun archivo con terminacion .fsx (como script.fsx) y teclea en la terminal:

  • dotnet fsi run script.fsx > NOTA: para esto es necesario tener instalado el SDK de .NET

Ahora, toma en cuenta que en este caso usamos F# para pre-procesar el template antes de ahora si renderizar el template con Scriban, lo cual es propenso a problemas por que un string aqui, otro alla, y ya nos perdimos lo del maracuya, en casos como estos es simplemente mas sencillo que escribas tus archivos HTML y le dejes el resto a Scriban y el modelo que le pases para que pueda hacer lo que esta hecho para hacer. Si ya conoces como es template y los requerimientos son claros probablemente este sea el mejor acercamiento de los tres en mi opinion.

Ahora, ten en cuenta de que si lo necesitas, puedes pasar multiples fragmentos de HTML en cadenas de texto y hacer una especie de layout (como en los patrones MVC) y renderizar todo al final, aunque no me consta que tanto pueda incurrir en el desenoeño (aunque Scriban en su github dicen que son extremadamente veloces)

Y Para Cerrar...

Asi que ahi lo tienen! HTML en F#, facil y sencillo no? espero que les haya sido de utilidad, quiza pueden usar esto para alguna utilidad de consola para convertir JSON a HTML! o quiza para el proximo reporte de las ventas del año pasado o algun otro caso de uso similar.

Is there something wrong? Raise an issue!
Or if it's simpler, find me in Threads!