paris-branch / dancelor

A chancelor for Scottish country dance musicians
https://dancelor.org
0 stars 0 forks source link

Factorise and generalise HTML smart constructors #233

Closed Niols closed 1 year ago

Niols commented 1 year ago

This PR is the first in a series of pull requests aiming at making the module Dancelor_client_html rich enough to retire the modules from Dancelor_client_elements and replace their uses everywhere by a more declarative way to write HTML. This PR focuses on factorising and generalising the HTML helpers.

The previous design

Prior to this PR, for each helper, we used to have two versions, eg.:

val div : ?classes:string list -> node list -> node
val div_lwt : ?classes:string list -> node list Lwt.t -> node

the first one providing the children of node statically and the second one waiting on a promise to provide them. Now in some cases those functions even have two versions of their flags, eg.:

val a : ?href:string -> ?href_lwt:string Lwt.t -> node list -> node
val a_lwt : ?href:string -> ?href_lwt:string Lwt.t -> node list Lwt.t -> node

the first one (href) providing the link statically while the second one waits on a promise to get it.

Its limitations

There are two main limitations to this design. Firstly, this does not apply to all the flags. What if we wanted to provide classes with a promise? Secondly, this would explode with the addition of a third flag. But this is precisely two things that we will need in the future: we will want to add a way for nodes to updates themselves, adding a third kind of nodes looping with an argument such as unit -> node list Lwt.t and we will want to be able to update classes themselves.

A first design idea

My first design idea was to go for something like:

type 'a provider =
  | Const of 'a
  | Lwt of 'a Lwt.t

val div : ?classes:string list -> node list provider -> node

which then also works nicely for flags:

val a : ?href:string provider -> node list provider -> node

and which can easily be generalised later with

type 'a provider = Const of 'a | Lwt of 'a Lwt.t | Loop of unit -> 'a Lwt.t

This design is simple and efficient. It does however have one (actually pretty important) issue which is that it adds a level of parentheses for each use of a smart HTML constructors:

ul (const [
    li (const [
        text (const "Bonjour")
    ])
])

The suggested design

We keep the 'a provider type above but we add functions for each constructor. The smart HTML constructors take an additional argument

type 'a provider =
  | Const of 'a
  | Lwt of 'a Lwt.t

val const : 'a -> 'a provider
val lwt : 'a Lwt.t -> 'a provider

val div : ?classes:string list -> ('node_list -> node list provider) -> 'node_list -> node

which leads to more concise use of the constructors:

ul const [
  li const [
    text const "Bonjour"
  ]
]

On bigger examples, the difference is even more striking.

Final notes

The commits of this PR have been made to tell the story so I suggest simply reading them one by one. The interesting ones are the ones that make changes to Dancelor_client_html.ml*; the others can be quite verbose.

Niols commented 1 year ago

For context, #234 builds on top of the current PR and adds a proof of concept index search written with this HTML module.

Niols commented 1 year ago

That was fun and all but I think we should actually get rid of our HTML smart constructors and rely on already existing infrastructure, for instance with TyXML. I just have to figure out a way to get TyXML to combine well with Lwt.