Introduction to Web Programming in F# with Giraffe – Part 3

Introduction

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

In the last post, we created a simple API for managing a Todo list. In this post, we are going to start our journey into HTML views with the Giraffe View Engine.

If you haven't already done so, read the previous posts in this series.

Getting Started

We are going to use the project we created for the last post and add to it.

Running the Sample Code

In the Terminal, type the following to run the project:

dotnet run

Go to your browser and type in the following Url:

https://localhost:5001

You should see some text.

Now try the following Uri and you should see some Json returned:

https://localhost:5001/api

Our Task

We are going to create a simple HTML view of a Todo list. In this post, we will concentrate on getting things on the screen. We will wire in to the backend in the next post.

Rather than rely on my HTML/CSS skills, we are going to start with a pre-built sample: The ToDo list example from w3schools:

https://www.w3schools.com/howto/howto_js_todolist.asp

Configuration

Add a new folder to the project called WebRoot and add a couple of files to the folder: main.js and main.css.

Open the .fsproj file and add the following snippet:

<ItemGroup>
  <Content Include="WebRoot\**\*">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>

The final step to enable these files to be used is to edit the main function:

[<EntryPoint>]
let main _ =
    let contentRoot = Directory.GetCurrentDirectory()
    let webRoot = Path.Combine(contentRoot, "WebRoot")
    Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(fun webHost ->
            webHost
                .UseWebRoot(webRoot)
                .Configure(configureApp)
                .ConfigureServices(configureServices)
                |> ignore)
        .Build()
        .Run()
    0

We have added a couple of lines to tell the code where the WebRoot folder is and have passed that path to a helper function UseWebRoot.

That's the end of the configuration but we need to copy the code from the w3schools site for the css and javascript to our files.

/* Include the padding and border in an element's total width and height */
* {
  box-sizing: border-box;
}

/* Remove margins and padding from the list */
ul {
  margin: 0;
  padding: 0;
}

/* Style the list items */
ul li {
  cursor: pointer;
  position: relative;
  padding: 12px 8px 12px 40px;
  background: #eee;
  font-size: 18px;
  transition: 0.2s;

  /* make the list items unselectable */
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

/* Set all odd list items to a different color (zebra-stripes) */
ul li:nth-child(odd) {
  background: #f9f9f9;
}

/* Darker background-color on hover */
ul li:hover {
  background: #ddd;
}

/* When clicked on, add a background color and strike out text */
ul li.checked {
  background: #888;
  color: #fff;
  text-decoration: line-through;
}

/* Add a "checked" mark when clicked on */
ul li.checked::before {
  content: '';
  position: absolute;
  border-color: #fff;
  border-style: solid;
  border-width: 0 2px 2px 0;
  top: 10px;
  left: 16px;
  transform: rotate(45deg);
  height: 15px;
  width: 7px;
}

/* Style the close button */
.close {
  position: absolute;
  right: 0;
  top: 0;
  padding: 12px 16px 12px 16px;
}

.close:hover {
  background-color: #f44336;
  color: white;
}

/* Style the header */
.header {
  background-color: #f44336;
  padding: 30px 40px;
  color: white;
  text-align: center;
}

/* Clear floats after the header */
.header:after {
  content: "";
  display: table;
  clear: both;
}

/* Style the input */
input {
  margin: 0;
  border: none;
  border-radius: 0;
  width: 75%;
  padding: 10px;
  float: left;
  font-size: 16px;
}

/* Style the "Add" button */
.addBtn {
  padding: 10px;
  width: 25%;
  background: #d9d9d9;
  color: #555;
  float: left;
  text-align: center;
  font-size: 16px;
  cursor: pointer;
  transition: 0.3s;
  border-radius: 0;
}

.addBtn:hover {
  background-color: #bbb;
}

// Create a "close" button and append it to each list item
var myNodelist = document.getElementsByTagName("LI");
var i;
for (i = 0; i < myNodelist.length; i++) {
  var span = document.createElement("SPAN");
  var txt = document.createTextNode("\u00D7");
  span.className = "close";
  span.appendChild(txt);
  myNodelist[i].appendChild(span);
}

// Click on a close button to hide the current list item
var close = document.getElementsByClassName("close");
var i;
for (i = 0; i < close.length; i++) {
  close[i].onclick = function() {
    var div = this.parentElement;
    div.style.display = "none";
  }
}

// Add a "checked" symbol when clicking on a list item
var list = document.querySelector('ul');
list.addEventListener('click', function(ev) {
  if (ev.target.tagName === 'LI') {
    ev.target.classList.toggle('checked');
  }
}, false);

// Create a new list item when clicking on the "Add" button
function newElement() {
  var li = document.createElement("li");
  var inputValue = document.getElementById("myInput").value;
  var t = document.createTextNode(inputValue);
  li.appendChild(t);
  if (inputValue === '') {
    alert("You must write something!");
  } else {
    document.getElementById("myUL").appendChild(li);
  }
  document.getElementById("myInput").value = "";

  var span = document.createElement("SPAN");
  var txt = document.createTextNode("\u00D7");
  span.className = "close";
  span.appendChild(txt);
  li.appendChild(span);

  for (i = 0; i < close.length; i++) {
    close[i].onclick = function() {
      var div = this.parentElement;
      div.style.display = "none";
    }
  }
}

Converting HTML To Giraffe ViewEngine

If you've never seen a DSL like the Giraffe View Engine, it is going to be a bit of a shock but bear with it because it really makes sense!

Let's start with a basic HTML page:

<html>
    <head>
        <title>My Title</title>
        <link rel="stylesheet" href="main.css" />
    </head>
    <body />
</html>

If we convert it to Giraffe View Engine format, we get:

// string -> XmlNode list -> XmlNode
let createPage msg content =
    html [] [
        head [] [
            title [] [ Text msg ]
            link [ _rel "stylesheet"; _href "main.css" ]
        ]
        body [] content
    ]

Most tags have two lists, one for styling and one for content. Some tags, like input, only take the style list. The Giraffe ViewEngine is a Domain Specific Language (DSL) that generates XHTML from F#.

To create the original HTML view, we would call:

createPage "My Title" []

This approach is very different to most view engines which rely on search and replace but are primarily still HTML. The primary advantage of the Giraffe View Engine approach is that you get full type safety when generating views and can use the full power of the F# language.

ToDo Snippet

Our ToDo HTML snippet below has to be converted into Giraffe ViewEngine code:

<div id="myDIV" class="header">
  <h2>My To Do List</h2>
  <input type="text" id="myInput" placeholder="Title...">
  <span onclick="newElement()" class="addBtn">Add</span>
</div>

<ul id="myUL">
  <li>Hit the gym</li>
  <li class="checked">Pay bills</li>
  <li>Meet George</li>
  <li>Buy eggs</li>
  <li>Read a book</li>
  <li>Organize office</li>
</ul>

Let's create a new function to generate our ToDo HTML snippet:

let todoView =
    createPage "My ToDo App" [
        div [ _id "myDIV"; _class "header" ] [
            h2 [] [ Text "My To Do List" ]
            input [ _type "text"; _id "myInput"; _placeholder "Title..." ]
            span [ _class "addBtn"; _onclick "newElement()" ] [ Text "Add" ]
        ]
        ul [ _id "myUL" ] [
            li [] [ Text "Hit the gym" ]
            li [ _class "checked" ] [ Text "Pay bills" ]
            li [] [ Text "Meet George" ]
            li [] [ Text "Buy eggs" ]
            li [] [ Text "Read a book" ]
            li [] [ Text "Organize office" ]
        ]
        script [ _src "main.js"; _type "text/javascript" ] []
    ]

We are nearly ready to run: We only need to tell the router about our new handler. Change the first route in webApp from:

GET >=> route "/" >=> htmlView indexHandler

to:

GET >=> route "/" >=> htmlView todoView

If you now run the app using dotnet run and click on the link (mine goes to https://localhost:5001), you will see the ToDo app.

Next Stage

Rather than hard code the list of ToDos in the view generation, we can load it in on the fly:

let todoList = [
    { Id = Guid.NewGuid(); Description = "Hit the gym"; Created = DateTime.UtcNow; IsCompleted = false }
    { Id = Guid.NewGuid(); Description = "Pay bills"; Created = DateTime.UtcNow; IsCompleted = true }
    { Id = Guid.NewGuid(); Description = "Meet George"; Created = DateTime.UtcNow; IsCompleted = false }
    { Id = Guid.NewGuid(); Description = "Buy eggs"; Created = DateTime.UtcNow; IsCompleted = false }
    { Id = Guid.NewGuid(); Description = "Read a book"; Created = DateTime.UtcNow; IsCompleted = false }
    { Id = Guid.NewGuid(); Description = "Organize office"; Created = DateTime.UtcNow; IsCompleted = true }
]

// Todo -> XmlNode
let showListItem (todo:Todo) =
    let style = if todo.IsCompleted then [ _class "checked" ] else []
    li style [ Text todo.Description ]

let todoView =
    createPage "My ToDo App" [
        div [ _id "myDIV"; _class "header" ] [
            h2 [] [ Text "My ToDo List" ]
            input [ _type "text"; _id "myInput"; _placeholder "Title..." ]
            span [ _class "addBtn"; _onclick "newElement()" ] [ Text "Add" ]
        ]
        ul [ _id "myUL" ] [
            for todo in todoList do showListItem todo
        ]
        script [ _src "main.js"; _type "text/javascript" ] []
    ]

We created a simple list of ToDos, a small helper function to style the list item, and used a list comprehension to generate the items displayed in the list.

The code from this post can be found here:

https://gist.github.com/ianrussellsoftwarepark/13569e9a930086d69082481dabebb1f8

I encourage you to have a look at the documentation and source code for the Giraffe View Engine.

https://github.com/giraffe-fsharp/Giraffe.ViewEngine#documentation

In the next post, we will investigate tying the frontend and backend together.

If you like the Giraffe View Engine, but don't like writing JavaScript, you should investigate the SAFE Stack where everything is F#.

https://safe-stack.github.io/

Summary

I hope that you found this introduction to HTML views in F# with Giraffe and the Giraffe View Engine useful and interesting. We have only scratched the surface of what is possible with Giraffe for creating web pages.

In the next post we will start to extend the functionality we covered in this post to wire up the frontend and backend.

If you have any comments on this series of posts or suggestions for new ones, send me a tweet (@ijrussell) and let me know.

Zurück
Zurück

Introduction to Web Programming in F# with Giraffe – Part 2

Weiter
Weiter

Function Composition in F# with Unfriendly Functions