package dream-html

  1. Overview
  2. Docs

Form handling

module Form : sig ... end

Typed, extensible HTML form decoder with error reporting for form field validation failures. Powerful chained decoding functionality–the validation of one field can depend on the values of other decoded fields.

val form : 'a Form.t -> ?csrf:bool -> Dream.request -> [> 'a Dream.form_result | `Invalid of (string * string) list ] Dream.promise

Type-safe wrapper for Dream.form. Similarly to that, you can match on the result:

type new_user = { name : string; email : string }
let new_user =
  let open Dream_html.Form in
  let+ name = required string "name"
  and+ email = required string "email" in
  { name; email }

(* POST /users *)
let post_users req =
  match%lwt Dream_html.form new_user req with
  | `Ok { name; email } -> (* ... *)
  | `Invalid errors -> Dream.json ~code:422 ( (* ...render errors... *) )
  | _ -> Dream.empty `Bad_Request
  • since 3.8.0
val query : 'a Form.t -> Dream.request -> [> `Ok of 'a | `Invalid of (string * string) list ]

Type-safe wrapper for Dream.all_queries. Can be used to decode the query parameters into a typed value.

  • since 3.8.0

HTML and other markup

include module type of Pure_html

Core types

These are the types of the final values which get rendered.

type attr

E.g. id="toast".

type node

Either a tag, a comment, or text data in the markup.

Output

val to_string : node -> string
val to_xml : ?header:bool -> node -> string

Same as to_string but render void tags as XML-style self-closing tags.

  • parameter header

    print the XML header string if true. This is to allow both use cases where the XML code is embedded inside HTML, and standalone XML documents. Default is false. Since 3.6.0.

  • since 3.3.0.
val pp : Format.formatter -> node -> unit
val pp_xml : Format.formatter -> ?header:bool -> node -> unit

Same as pp but render void tags as XML-style self-closing tags.

  • parameter header

    print the XML header string if true. This is to allow both use cases where the XML code is embedded inside HTML, and standalone XML documents. Default is false. Since 3.6.0.

  • since 3.3.0.

Constructing nodes and attributes

type 'a to_attr = 'a -> attr

Attributes can be created from typed values.

type 'a string_attr = ('a, unit, string, attr) format4 -> 'a

Special handling for string-value attributes so they can use format strings i.e. string interpolation.

type std_tag = attr list -> node list -> node

A 'standard' tag with attributes and children.

type void_tag = attr list -> node
type 'a text_tag = attr list -> ('a, unit, string, node) format4 -> 'a

Tags which can have attributes but can contain only text. The text can be formatted.

val attr : string -> attr

attr name is a new attribute which does not carry any payload. E.g.

let required = attr "required"
  • since 0.1.0.
val string_attr : string -> ?raw:bool -> _ string_attr

string_attr name fmt is a new string-valued attribute which allows formatting i.e. string interpolation of the value. Note, the fmt argument is required due to the value restriction.

val uri_attr : string -> _ string_attr

Convenience for attributes whose values should be URIs. Takes care of both URI-encoding and attribute escaping, as recommended in https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#common-mistake.

Examples

a [href "/blog?tags=iamsafe\"></a><script>alert('Pwned')</script>"] [txt "Tags: tag1 | tag2"]
==>
<a href="/blog?tags=iamsafe%22%3E%3C/a%3E%3Cscript%3Ealert('Pwned')%3C/script%3E">Tags: tag1 | tag2</a>

a [href "/foo?a=1&b=2 3&c=4<5&d=6>5"] [txt "Test"]
==>
<a href="/foo?a=1&amp;b=2%203&amp;c=4%3C5&amp;d=6%3E5">Test</a>
val bool_attr : string -> bool to_attr
val float_attr : string -> float to_attr
val int_attr : string -> int to_attr
val std_tag : string -> std_tag
val void_tag : string -> void_tag
val text_tag : string -> ?raw:bool -> _ text_tag

Build a tag which can contain only text.

val uri_tag : string -> _ text_tag

Build a tag which can contain only a URI. The URI is escaped with the same rules as a uri_attr.

  • since 3.10.0
val txt : ?raw:bool -> ('a, unit, string, node) format4 -> 'a

A text node inside the DOM e.g. the 'hi' in <b>hi</b>. Allows string interpolation using the same formatting features as Printf.sprintf:

b [] [txt "Hello, %s!" name]

Or without interpolation:

b [] [txt "Bold of you."]

HTML-escapes the text value. You can use the ~raw param to bypass escaping:

let user_input = "<script>alert('I like HTML injection')</script>" in
txt ~raw:true "%s" user_input
val comment : string -> node

A comment that will be embedded in the rendered HTML, i.e. <!-- comment -->. The text is HTML-escaped.

val concat : node -> node list -> node

concat node list is the list of nodes joined together into a single node, with each element separated by node.

  • since 3.10.0

Accessors for tags

val (+@) : node -> attr -> node

Add an attribute to a tag.

let toast msg = p [id "toast"] [txt "%s" msg]
let toast_oob = toast "ok." +@ Hx.swap_oob "true"
  • raises Invalid_argument

    if the node is not a tag (i.e. if it is a text or comment node).

  • since 0.0.3.
val (-@) : node -> string -> node

Remove an attribute from a tag.

  • raises Invalid_argument

    if the node is not a tag (i.e. if it is a text or comment node).

  • since 0.0.3.
val (.@[]) : node -> string -> string

Get the value of an existing attribute.

let toast = p [id "toast"] [txt "OK."]
let toast_id = toast.@["id"]
  • raises Invalid_argument

    if the node is not a tag (i.e. if it is a text or comment node).

  • raises Not_found

    if the tag does not have the given attribute.

  • since 0.0.3.
val is_null : node -> bool

Get whether a node is null (empty) or not. Useful for conditional rendering of UIs when you are passed in a node and you don't know if it's empty or not.

  • since 1.2.0.
val is_null_ : attr -> bool

Get whether an attribute is null (empty) or not.

  • since 1.2.0.

HTML

module HTML : sig ... end

All standard HTML attributes and tags. Some attributes and tags have the same name, e.g. style. To disambiguate them, attributes have a _ (underscore) suffix.

SVG

module SVG : sig ... end

MathML

module MathML : sig ... end

ARIA

RSS and Atom

module Atom : sig ... end
module RSS : sig ... end

RSS support

htmx

module Hx : sig ... end

htmx support

val respond : ?status:[< Dream.status ] -> ?code:int -> ?headers:(string * string) list -> node -> Dream.response Dream.promise
val send : ?text_or_binary:[< Dream.text_or_binary ] -> ?end_of_message:[< Dream.end_of_message ] -> Dream.websocket -> node -> unit Dream.promise

Type-safe wrapper for Dream.send.

  • since 3.2.0.
val set_body : Dream.response -> node -> unit

Type-safe wrapper for Dream.set_body. Sets the body to the given node and sets the Content-Type header to text/html.

val write : Dream.stream -> node -> unit Dream.promise

Type-safe wrapper for Dream.write.

val csrf_tag : Dream.request -> node

Convenience to add a CSRF token generated by Dream into your form. Type-safe equivalent of Dream.csrf_tag.

form
  [action "/foo"]
  [csrf_tag req; input [name "bar"]; input [type_ "submit"]]

Type-safe routing

Bidirectional paths with type-safe path segment parsing and printing using OCaml's built-in format strings, and fully plug-and-play compatible with Dream routes.

type (_, _) path

A path that can be used for routing and can also be printed as an attribute value.

  • since 3.9.0
type ('r, 'p) route = ('r, 'p) path -> (Dream.request -> 'r) -> Dream.route

Wrapper for a Dream route that represents the ability to parse path parameters and pass them to the handler function with the correct types.

  • since 3.9.0
val path : ('r, unit, Dream.response Dream.promise) format -> ('p, unit, string, attr) format4 -> ('r, 'p) path

path request_fmt attr_fmt is a router path. The dream-html.ppx provides a more convenient way.

Without PPX: let order = path "/orders/%s" "/orders/%s"

With PPX: let%path order = "/orders/%s"

Refer to the PPX documentation for instructions on using it.

⚠️ Due to the way Dream's router works, all parameter captures happen between / characters and the end of the path (or the ? character, whichever comes first). Eg, /foo/%s/bar/%d is valid, but /foo/%s.%s (note the dot character) is not a valid capture.

⚠️ If a route is matched but the data type does not match, a 400 Bad Request response will be returned. The following type conversion specs are supported:

%s capture a string and pass it to the handler

%*s capture the rest of the path and pass the captured length and string to the handler

%c capture a char

%d or %i capture an int

%x capture a hexadecimal int

%X capture an uppercase hexadecimal int

%o capture an octal int

%ld capture an int32

%Ld capture an int64

%f capture a float

%B capture a bool

⚠️ We are actually using Dream's built-in router, not our own, and Dream's router doesn't distinguish between parameter types. So, to Dream both /%s and /%d are the same path. It will route the request to whichever happens to be first in the route list, and that one will succeed or fail depending on its type and the request target.

  • since 3.9.0
val path_attr : 'p string_attr -> (_, 'p) path -> 'p

path_attr attr path is an HTML attribute with the path parameters filled in from the given values. Eg,

let%path order = "/orders/%s"

open Dream_html
open HTML

a [path_attr href order "yzxyzc"] [txt "My Order"]

Renders: <a href="/orders/yzxyzc">My Order</a>

Use this instead of hard-coding your route URLs throughout your app, to make it easy to refactor routes with minimal effort.

  • since 3.9.0
val pp_path : (_, _) path Fmt.t

pp_path is a pretty-printer for path values. For a path like path "/foo" "/foo", it will print out /foo.

  • since 3.9.0
val get : (_, _) route

Type-safe wrappers for Dream.get and so on. Using the PPX, eg:

let%path order = "/orders/%s"

let get_order = get order (fun request order_id ->
  ...
  a [path_attr href order order_id] [txt "Your order"]
  ...
)
  • since 3.9.0
val post : (_, _) route
  • since 3.9.0
val put : (_, _) route
  • since 3.9.0
val delete : (_, _) route
  • since 3.9.0
val head : (_, _) route
  • since 3.9.0
val connect : (_, _) route
  • since 3.9.0
val options : (_, _) route
  • since 3.9.0
val trace : (_, _) route
  • since 3.9.0
val patch : (_, _) route
  • since 3.9.0
val any : (_, _) route
  • since 3.9.0
val use : Dream.middleware list -> Dream.route list -> Dream.route

use middlewares routes is a route that is composed of all the given routes with the middlewares attached to them.

  • since 3.9.0
val static_asset : (Dream.response Dream.promise, _) path -> Dream.route

static_asset path is a route that handles serving the static file at the path. Importantly, it sets an immutable cache header which remains valid for a year.

⚠️ Be sure that the resource has a unique identifier because it will be cached immutably. The dreamwork CLI tool automates this for you. See Dreamwork.

  • since 3.9.2

Dreamwork

TL;DR:

  1. Run dreamwork setup
  2. Populate the static/assets/ subdirectory with files
  3. Run dune build
  4. Add Static.routes to the app's main Dream router
  5. Use eg img [path_attr src Static.Assets.icon_png] which will render with a revision hash based on the file contents, calculated at build time

dreamwork is a CLI tool that helps set up and manage static file paths and routes with proper content-based version hashes. The static files will live inside a dune component called static and in the static/assets subdirectory. Suppose you have the following directory tree:

static / dune assets / css / app.css js / app.js

The dune file defines a library component that will make the following module available:

module Static : sig
  val routes : Dream.route
  (** This route will serve all of the following paths. *)

  module Assets : sig
    module Css : sig
      val app_css : (Dream.response Dream.promise, attr) path
    end

    module Js : sig
      val app_js : (Dream.response Dream.promise, attr) path
    end
  end
end

So, you can just stick Static.routes in your router and it will correctly serve the files from the filesystem with an immutable cache of 1 year; and you can use Static.Assets.Css.app_css and so on in your dream-html markup code and it will correctly render with a ?rev=... query parameter that uniquely identifies this revision of the file with a content-based hash for cache-busting purposes:

link [rel "stylesheet"; path_attr href Static.Assets.Css.app_css
(*
<link
  rel="stylesheet"
  href="/static/assets/css/app.css?rev=17fb8161afc85df86156ea1f3744c8a2"
>
*)
script [path_attr src Static.Assets.Js.app_js] ""
(*
<script src="/static/assets/js/app.js?rev=677645e5ac37d683c5039a85c41c339f">
</script>
*)

You control the directory subtree under assets; the dreamwork CLI just helps you define the dune component that generates the above module structure. The module structure mirrors the directory tree structure.

The entry point to dreamwork is the dreamwork setup command, which creates static/, assets/, and dune. In the dune file it defines a code generation rule which uses the dreamwork static command to generate the OCaml code.

So, you just need to run dreamwork setup to initialize the directory structure and code generation. After that, you can add and remove any files inside assets/ as you want and on the next dune build the Static module structure will be updated accordingly.

  • since 3.9.2

Live reload support

module Livereload : sig ... end

Live reload script injection and handling. Adapted from Dream.livereload middleware. This version is not a middleware so it's not as plug-and-play as that, but on the other hand it's much simpler to implement because it uses type-safe dream-html nodes rather than parsing and printing raw HTML. See below for the 3-step process to use it.

OCaml

Innovation. Community. Security.