This library was born out of the Javascript ecosystem where there are these concepts called "islands" in view libraries.
+}This library was born out of the Javascript ecosystem where there are these concepts called "islands" in view libraries.
These islands are basically holes in your HTML where asynchronous components can perform work while the rest of the page is rendered synchronously, this of course requires scripting capabilities in the browser, because as soon as nodes start resolving, they are added to the DOM. In the server we don't have DOM, but we can still leverage the fact that we can place asynchronous work side-by-side with synchronous work.
In server environments, it improves the so called DX (developer experience) as you don't have to coordinate between asynchronous functions to render in a single final place while also improving user experience, as the page can be streamed to the client as soon as a rendered chunk is available, instead of waiting for the whole page to be rendered. Modern browsers support this out of the box.
The rendering process is also cancellable unlike previous approaches where you'd pass a token to each coordinated function to avoid overworking before having the data to render the document in a single pass.
-In Hox, every asynchronous node node is backed by a CancellableValueTask<T>
which is just an alias for CancellationToken -> ValueTask<T>
, this also means that every asynchronous node is aware of the main cancellation token, and when a rendering process is cancelled, rather than starting the asynchronous work, it will just return an empty node. The rest of the rendering process is stopped when processing the next node in the internal stack.
In Hox, every asynchronous node is backed by a CancellableValueTask<T>
which is just an alias for CancellationToken -> ValueTask<T>
, this also means that every asynchronous node is aware of the main cancellation token, and when a rendering process is cancelled, rather than starting the asynchronous work, it will just return an empty node. The rest of the rendering process is stopped when processing the next node in the internal stack.
This also highlights the fact that Hox, more than a templating library is a rendering library, so you can build other kinds of templating/domain specific languages libraries on top of it, that's how we provide the Feliz API for example.
-For cases where every node of the document is synchronous, the rendering process is backed by ValueTask<T>
so synchronous work will be executed synchronously as usual.
For cases where every node of the document is synchronous, the rendering process is backed by ValueTask<T>
so synchronous work will be executed as usual.
In any case, this is a small library that hopes to push the web dev ecosystem in F# forward and spark some ideas in the community.
Special Thanks
-
@@ -62,7 +62,7 @@
- FSharp.Control.TaskSeq - For allowing us to have
IAsyncEnumerable<T>
support. - FParsec - For the building blocks of the CSS selector parsing.
Special Thanks
Hox can also be used in C# and VB.NET projects. Part of the side effect of looking for a simple and and easy DSL is that Hox can be used in any .NET language.
+}Hox can also be used in C# and VB.NET projects. Part of the side effect of looking for a simple and easy DSL is that Hox can be used in any .NET language.
C# Usage
Since Hox is written in F#, much of the API looks like a static class to C# code. This means you have to add using static
references to the Hox namespace.
The following example shows how to use Hox in a C# project.
@@ -100,8 +100,8 @@VB.NET Usage
' do something with the html End Sub -In general, both languages can use Hox up to the same extent as F# except by the Feliz API which uses specific F# types so, you can following the rest of the guides and reference will be useful for you as well.
-The Shadow DOM is a feature that isolates its own DOM tree from the main document DOM, often used with Javascript and custom elements to create what is known as Web Components. +}
The Shadow DOM is a feature that isolates its own DOM tree from the main document DOM, often used with Javascript and custom elements to create what is known as Web Components. Of course this is a simplification but it gives the general idea.
-While Shadow DOM and it's styling isolation features is a great thing, it didn't play well with server side rendering as you'd render the custom element tag but the enhancing of the tag would happen on the client side, which would cause a flash of unstyled content (FOUC).
+While Shadow DOM and its styling isolation features is a great thing, it didn't play well with server side rendering as you'd render the custom element tag but the enhancing of the tag would happen on the client side, which would cause a flash of unstyled content (FOUC).
Declarative Shadow DOM allows you to create these DOM boundaries in a declarative way, which means that the browser can render the content in the right order and you don't have to worry about the FOUC.
Clients can still enhance the elements produced with Declarative Shadow DOM, but it is not required. From a backend's perspective, it is just a DOM tree with scoped styling.
You can learn more about Declarative Shadow DOM in this piece from the Chrome for Developers website: https://developer.chrome.com/docs/css-ui/declarative-shadow-dom
-An elemnt with Declarative Shadow DOM looks like the following
+An element with Declarative Shadow DOM looks like the following
<tag-name>
<template shadowrootmode="open">
<style>
@@ -119,7 +119,7 @@ Sh and Shcs
</template>
</my-element>
-For cases where you want to use slots, things are sliglty more complicated, as you have to create the template with slots and then assign the new content outside the template tag, whis can be cumbersome, so Hox provides an overload that takes the initial template and then gets you a factory to enable shared content.
+For cases where you want to use slots, things are slightly more complicated, as you have to create the template with slots and then assign the new content outside the template tag, which can be cumbersome, so Hox provides an overload that takes the initial template and then gets you a factory to enable shared content.
let myPanel =
sh("my-panel",
fragment(
@@ -175,7 +175,7 @@ Sh and Shcs
<p>I'm on the section of the panel</p>
</my-panel>
-Traditionally F# devs would archive this kind of composition by using functions that take the content as parameters and fill the wholes defined in the templates they produce, however this approach is not friendly to scoping or requires complicated setups to enable scoping, here we're leveraging the browser's built-in support for Declarative Shadow DOM to enable this kind of composition.
+Traditionally F# devs would achieve this kind of composition by using functions that take the content as parameters and fill the wholes defined in the templates they produce, however this approach is not friendly to scoping or requires complicated setups to enable scoping, here we're leveraging the browser's built-in support for Declarative Shadow DOM to enable this kind of composition.
For the C#/VB devs, the story is quite similar
var myElement =
shcs("my-element",
@@ -217,7 +217,7 @@ Built-in elements
section
span
-To create an scoped article
element you can do the following
+To create a scoped article
element you can do the following
open type ScopableElements
@@ -232,7 +232,7 @@ Built-in elements
Note: If you need to create a built-in element that supports slots, then you have to use the sh
function factory instead.
-
+}While not required, it is recommended to check out IcedTasks, FSharp.Control.TaskSeq, to have some extra Computation Expressions that allow using tasks/async/valueTask and IAsyncEnumerable in simple ways.
The library by itself is quite simple and can be reduced to a single type with helper functions.
@@ -90,7 +90,7 @@A few examples
// assuming token is a CancellationToken // assuming Layout.Default() produces a `Node` let node = Layout.Default() - for chunk in Render.start(nod, tokene) do + for chunk in Render.start(nod, token) do printfn $"Produced:\n\n{chunk}" } @@ -149,7 +149,7 @@Attributes
.class
is specified with a.
followed by the value of said class.- [attribute=value]
is specified with a[
followed by the name of the attribute, followed by a required=
even for no-value atributes (likechecked
), after te=
symbol anything will be taken as the string until a]
is found, even break line characters.You can specify attributes in any order or with spacess and break lines in between the attribute declarations, example:
+You can specify attributes in any order or with spaces and break lines in between the attribute declarations, example:
div#main.is-primary
- @@ -193,7 +193,7 @@
div.is-primary#main
Attributes
Nodes and Attribute encoding
By default every node and attribute is encoded to prevent XSS attacks, this means that any special character will be encoded to its HTML entity equivalent, this is done by default.
-For cases where you want to render raw HTML then you should use
+raw
For cases where you want to render raw HTML, then you should use
raw
let rawNode = h("div", raw "<span data-random='my attribute'>Hello World</span>")
Raw nodes will not be encoded, and will be rendered as is, but BE CAREFUL, and please escape any HTML that you store in your database or comes from user input, otherwise you will be vulnerable to XSS attacks.
@@ -215,7 +215,7 @@Fragments
let node = h("ul", items)Asynchronous nodes
-One of the "big deals" is the ability to use asynchronous nodes, just like you would use synchrinous nodes, this bridges a gap between the two worlds and allows you to use the same mental model for both.
+One of the "big deals" is the ability to use asynchronous nodes, just like you would use synchronous nodes, this bridges a gap between the two worlds and allows you to use the same mental model for both.
Also, asynchronous nodes are cold (or also called lazy), this means that they will not be executed until they are requested to render.
open Hox @@ -317,7 +317,7 @@
Asynchronous sequences
}All of the concepts above can be combined to produce complex views, without having to worry about when to put the asynchronous work where in the rendered tree.
-Sometimes you may already have a big codebase written in another DSL, if you want to leverage their DSL and slowly migrate to Hox or a Hox based alternative writing an adapter is not a complex task.
+}Sometimes you may already have a big codebase written in another DSL, if you want to leverage their DSL and slowly migrate to Hox or a Hox based alternative writing an adapter is not a complex task.
Falco.Markup
For example, if you want to use Falco.Markup we already have a sample in our samples directory.
open Falco.Markup @@ -57,7 +57,7 @@
Falco.Markup
open Hox.Rendering // take their DSL attributes and convert them to Hox attributes -let inline xmlAttrToHtmelo(attr: XmlAttribute) : AttributeNode = +let inline xmlAttrToHox(attr: XmlAttribute) : AttributeNode = match attr with | KeyValueAttr(key, value) -> Attribute { name = key; value = value } | NonValueAttr key -> Attribute { name = key; value = "" } @@ -73,13 +73,13 @@Falco.Markup
// Falco.Markup's DSL uses XmlNodes as their core type, so // as long as we can convert an XmlNode to a Hox Node we're good -let rec xmlNodeToHtmelo(fmNode: XmlNode) : Node = +let rec xmlNodeToHox(fmNode: XmlNode) : Node = match fmNode with | ParentNode((tagName, attributes), children) -> attributes |> List.fold foldAttributes - (h(tagName, children |> List.map xmlNodeToHtmelo)) + (h(tagName, children |> List.map xmlNodeToHox)) | TextNode text -> Text text | SelfClosingNode((tagName, attributes)) -> attributes |> List.fold foldAttributes (h tagName) @@ -87,7 +87,7 @@Falco.Markup
In the code above we're just converting Falco.Markup's DSL to Hox's DSL, we're using the
h
function to create a node and then we're adding all of the attributes to it.To render it later on we just need to call this newly created function
@@ -101,7 +101,7 @@let render (fmNode: XmlNode) = - let convertedNode = fmNode |> xmlNodeToHtmelo + let convertedNode = fmNode |> xmlNodeToHox Render.asString(convertedNode)
Giraffe.ViewEngine
// in a similar fashion to Falco.Markup we're just // converting Giraffe.ViewEngine's DSL to Hox's DSL -let inline xmlAttrToHtmelo(attr: XmlAttribute) : AttributeNode = +let inline xmlAttrToHox(attr: XmlAttribute) : AttributeNode = match attr with | KeyValue(key, value) -> Attribute { name = key; value = value } | Boolean key -> Attribute { name = key; value = "" } @@ -229,7 +229,7 @@Moving out of Hox
}Hox is meant to be a building block for HTML rendering so, it is extensible enough to either migrate to it or away from it.
-While the Hox DSL is the favored flavor for Hox, we understand that developers prefer the type safety of the F# language. For this reason, we've added a Feliz-flavored API to Hox.
+}While the Hox DSL is the favored flavor for Hox, we understand that developers prefer the type safety of the F# language. For this reason, we've added a Feliz-flavored API to Hox.
-open Hox.Feliz let view = @@ -62,7 +62,7 @@ ] ]
For the most part, the Feliz DSL works as you would expectit excepto from attributes. In Hox, attributes are part of the element's children unlike the traditional Feliz DSL, where they are another Node in the tree. To make this work, we've added a
+Attr
module which contains helper functions to deal with attributes.For the most part, the Feliz DSL works as you would expect it except for attributes. In Hox, attributes are part of the element's children unlike the traditional Feliz DSL, where they are another Node in the tree. To make this work, we've added a
Attr
module which contains helper functions to deal with attributes.Main differences with Hox DSL
First and foremost, the syntax is obviously different. However there are some other differences that are worth noting.
@@ -119,7 +119,7 @@
Asynchronous nodes
}) ] -In case that neither Hox or Feliz fit your needs, you can always write your own DSL.
+}In case that neither Hox or Feliz fit your needs, you can always write your own DSL.
This can be accomplished in any dotnet language using the functions in the
Hox.NodeOps
module.This module exposes two simple functions
@@ -55,7 +55,7 @@
AddAttribute
These functions already contain all the functionality required to compose nodes together, regardless of the underlyng type of node.
-The following example shows how to create a more traditiona F# DSL with the two list style.
+The following example shows how to create a more traditional F# DSL with the two list style.
-open Hox open Hox.Core @@ -142,7 +142,7 @@ }) ]
For simplicity the operators
+<+
to add child nodes and<+.
to add attributes are available in theHox.NodeOps.Operators
module.For simplicity, the operators
<+
to add child nodes and<+.
to add attributes are available in theHox.NodeOps.Operators
module.C# & VB.NET
C# users can leverage extension methods to create a more fluent DSL however, due to the differences in the type system with F#, the DSL will have to use the Hox DSL functions, instead of the core types. Also, keep in mind that the DSL's are often easier made in F# so you might want to consider using F# for your DSL, however if you identify an opportunity to improve the APIs for C# users, please feel free to raise an issue.
Let's imagine we want a strongly typed DSL with a more C# feel or flavor.
@@ -264,7 +264,7 @@C# & VB.NET
public static T Type<T>(this T input, InputType type) where T : Input => input with { Node = NodeOps.addAttribute(input.Node, attribute("type", type.ToString().ToLowerInvariant())) }; -Hox is an async HTML rendering library for F#.
+}Hox is an async HTML rendering library for F#.
Hox is an extensible HTML rendering library, it provides a set of functions to work with Nodes and compose them together as well as a couple of async rendering functions that support cancellation.
The core features are:
@@ -55,7 +55,7 @@
- Side-by-side Asynchronous Nodes, you can add sync or async nodes into sync/async parents/sibling nodes.
- Cancellable sync/async Rendering process that leverages ValueTasks.
- Render to a single string or
-IAsyncEnumerable<string>
.- A simplistic core DSL based on css selector parsing to generate nodes.
+- A simplistic core DSL based on CSS selector parsing to generate nodes.
A couple of opt-in extra supported features are:
@@ -66,7 +66,7 @@
The core bits are somewhat low level building blocks to enable these kinds of features and possibly more in the future.
Ready? Let's get started!
-WIP
-WIP
+WIP
-WIP
+