Hello there, lovely swift community!
So, I ended up building yet another Swift DSL package for HTML.
I know, but hear me out (getting-it-off-the-chest story inside ;)
I am on a personal mission to seriously use Swift for web apps, and I am close to a wonderful dev experience with Hummingbird + Tailwind + HTMX.
I really want typesafe HTML templating, so things like Leaf or mustache are not no the table for me.
Also, I really want SwiftUI-like composition, but full control over the HTML and CSS (to style it with tailwind) - so otherwise excellent projects like Tokamak or SwiftWebUI are just not what I am looking for.
That leaves us with packages like Plot, HTMLKit, Swim, and similar ones - which are all very nice.
I really like Plot and started with it, but it always nagged me that it is using any Component
instead of some Component
for its composition feature (same as all the other packages). I know - not exactly a "real" problem - but I just felt so sorry about all the wasted allocations of array after array of existental containers, just to get a bit of HTML text out.
So I thought, out of pure curiosity, what would it look like to make this fully generic? I hacked together result builder using a variadic tuple thing and things fell into place quite nicely.
I liked how the API felt so much that I decided to push through and polish it up to be a releasable package - and here we are.
Here is a copy of the readme
Elementary: HTML Templating in Pure Swift
struct MainPage: HtmlDocument {
var title: String = "Elementary"
var head: some Html {
meta(.name(.description), .content("Typesafe HTML in modern Swift"))
}
var body: some Html {
main {
h1 { "Features" }
FeatureList(features: [
"HTML in pure Swift",
"SwiftUI-inspired composition",
"Lightweight and fast",
"Framework agnostic and unopinionated",
])
a(.href("https://github.com/sliemeobn/elementary"), .class("fancy-style")) {
"Learn more"
}
}
}
}
struct FeatureList: Html {
var features: [String]
var content: some Html {
ul {
for feature in features {
li { feature }
}
}
}
}
Play with it
Check out the Hummingbird example app.
Lightweight and fast
Elementary renders straight to text, ideal for serving generated HTML from a Hummingbird or Vapor server app.
Any element can be rendered individually, ideal for htmx.
let html = div(.class("pretty")) { "Hello" }.render()
// <div class="pretty">Hello</div>
let fragment = FeatureList(features: ["Anything conforming to Html can be rendered"]).render()
// <ul><li>Anything conforming to Html can be rendered</li></ul>
Optionally you can render formatted output, or stream the rendered HTML without collecting it in a string first.
print(
div {
p(.class("greeting")) { "Hi mom!" }
p { "Look how pretty." }
}.renderFormatted()
)
// <div>
// <p class="greeting">Hi mom!</p>
// <p>Look how pretty.</p>
// </div>
// Have HTML arrive at the browser before the full page is rendered out.
MainPage().render(into: responseStream)
Elementary has zero dependencies (not even Foundation) and does not use runtime reflection or existential containers (there is not a single any
in the code base).
By design, it does not come with a layout engine, reactive state tracking, or built-in CSS styling: it just renders HTML.
Clean and composable
Structure your HTML with a SwiftUI-inspired composition API.
struct List: Html {
var items: [String]
var importantIndex: Int
var content: some Html {
// conditional rendering
if items.isEmpty {
p { "No items" }
} else {
ul {
// list rendering
for (index, item) in items.enumerated() {
// seamless composition of elements
ListItem(text: item, isimportant: index == importantIndex)
}
}
}
}
}
struct ListItem: Html {
var text: String
var isimportant: Bool = false
var content: some Html {
// conditional attributes
li { text }
.attributes(.class("important"), when: isimportant)
}
}
First class attribute handling
Elementary utilizes Swift's powerful generics to provide an attribute system that knows what goes where. Every element knows which Tag it is for.
As in HTML, attributes go right after the "opening tag".
// staying close to HTML syntax really helps
div(.data("hello", value: "there")) {
a(.href("/swift"), .target(.blank)) {
img(.src("/swift.png"))
span(.class("fancy")) { "Click Me" }
}
}
Attributes can also be altered by using the modifier syntax, this allows for easy handling of conditional attributes.
div {
p { "Hello" }
.attributes(.id("maybe-fancy"))
.attributes(.class("fancy"), when: isFancy)
}
By exposing the tag type of content
, attributes will fall through and be applied correctly.
struct Button: Html {
var text: String
// by exposing the HtmlTag type information...
var content: some Html<HtmlTag.input> {
input(.type(.button), .value(text))
}
}
div {
// ... Button will know it really is an <input> element ...
Button(text: "Hello")
.attributes(.autofocus) // ... and pass on any attributes
}
As a sensible default, class and style attributes are merged (with a blank space or semicolon respectively). All other attributes are overwritten by default.
Work in progress
The list of built-in attributes is quite short still, but adding them is quite easy (and can be done in external packages as well).
Feel free to open a PR with additional attributes that are missing from the model.
Motivation and other packages
Plot, HTMLKit, and Swim are all excellent packages for doing a similar thing.
My main motivation for Elementary was to create an experience like these, but
- stay true to HTML tag names and conventions (including the choice of lowercase types)
- avoid allocating an intermedate structure and go straght to text
- using generics to stay away from allocating a ton of lists of existential
any
s - have a list of attributes go before the content block
- provide attribute fallthrough and merging
- zero dependencies on other packages
Tokamak is an awesome project and very inspiring. It can produce HTML, but it's main focus is on a very different beast. Check it out!
swift-html and swift-dom will produce HTML nicely, but they use a different syntax for composing HTML elements.
Future directions
- Try out a hummingbird + elementary + htmx + tailwind stack for fully-featured web apps (without too much client javascript or wasm)
- Experiment with an
AsyncHtml
type, that can includeawait
in bodies and stream html and an async sequence - Experiment with embedded swift for wasm and bolt a lean state tracking/reconciler for reactive DOM manipulation on top