/HTMLKit

A type-safe DSL that renders dynamic HTML templates in Swift

Primary LanguageSwiftMIT LicenseMIT

HTMLKit logo

HTMLKit

Render dynamic HTML templates in a typesafe and performant way! By using Swift's powerful language features and a pre-rendering algorithm, HTMLKit will render insanely fast templates but also catch bugs that otherwise might occur with other templating options.

Getting Started

Add the following in your Package.swift file

.package(url: "https://github.com/vapor-community/HTMLKit.git", from: "2.0.0"),

You can use the following providers in order to use HTMLKit with Vapor 3 and for Vapor 4

for instance for Vapor 4, you have to add the HTMLKit vapor provider like this

.package(name: "HTMLKitVaporProvider", url: "https://github.com/MatsMoll/htmlkit-vapor-provider.git", from: "1.0.0"),
...
.product(name: "HTMLKit", package: "HTMLKit"),
.product(name: "HTMLKitVaporProvider", package: "HTMLKitVaporProvider"),

Usage

View the different tags and types defined in HTMLKit on the Wiki page.

To create a HTML template, conform to the HTMLTemplate protocol.

struct TemplateData {
    let name: String
    let handle: String
    let title: String?
}

struct SimpleTemplate: HTMLTemplate {

    @TemplateValue(TemplateData.self)
    var context

    var body: HTML {
        Document(type: .html5) {
            Head {
                Title { context.title }
                Author { context.name }
                    .twitter(handle: context.handle)
                Viewport(.acordingToDevice)
            }
            Body {
                Unwrap(context.title) { title in
                    P { title }
                }
            }
        }
    }
}

// For Vapor 4, you certainly want to preload the template in configure to optimize rendering.
// in configure.swift
import HTMLKitVaporProvider
...
public func configure(_ app: Application) throws {
    ...
    try app.htmlkit.add(view: SimpleTemplate())
    ...
}

// In one of your controllers
import HTMLKit
...
func get(req: Request) throws -> EventLoopFuture<View> {
    return Simple.query(on: req.db)
        .filter(...)
        .first()
        .unwrap(or: Abort(.notFound))
        .flatMap {
            SimpleTemplate().render(with: TemplateData(name: $0.name, ...), for: req)
        }
}

This will render something like this

<!DOCTYPE html>
<html>
    <head>
        <title>Some Title</title>
        <meta property='og:title' content='Some Title'>
        <meta name='twitter:title' content='Some Title'>
        <meta name='author' content='Mats'>
        <meta name='twitter:creator' content='@SomeTwitterHandle'>
        <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    </head>
    <body>
        <p>Some Title</p>
    </body>
</html>

And to create a HTML component, just conform to the HTMLComponent protocol.

public struct Alert: HTMLComponent {

    let isDismissable: Conditionable // This is a protocol that makes it possible to optimize if's
    let message: HTML

    public var body: HTML {
        Div {
            message

            IF(isDismissable) {
                Button {
                    Span { "&times;" }
                        .aria(for: "hidden", value: true)
                }
                .type(.button)
                .class("close")
                .data("dismiss", value: "alert")
                .aria("label", value: "Close")
            }
        }
        .class("alert alert-danger bg-danger")
        .modify(if: isDismissable) {
            $0.class("fade show")
        }
        .role("alert")
    }
}

This can then be used in another template or a static html page like:

struct SomePage: HTMLPage {

    var body: HTML {
        Div {
            Alert(isDismissable: false) {
                H3 { "Some Title" }
            }
        }
    }
}

Mixing HTMLKit with Leaf

You can easily mix Leaf and HTMLKit in the same project.

struct RenderingConfig: Content {
    let message: String
    let useHTMLKit: Bool
}

func renderLogin(on req: Request) -> Future<View> {
    let query = try req.query.decode(RenderingConfig.self)
    if config.useHTMLKit {
        return LoginPage().render(with: config, on: req)
    } else {
        return req.view().render("login-page", with: config)
    }
}

Localization

Much like SwiftUI, you can localize text by passing the localization key as a parameter.

...
var body: HTML {
    P("hello.world")
        .class("text-white")
}
...

This requires the Lingo framework, so you will also need to register the Lingo struct on the renderer.

var renderer = HTMLRenderer()
try renderer.registerLocalization(atPath: "workDir", defaultLocale: "en")

And if the locale changes based on some user input, then you can change the used locale in the template. This also effects how dates are presented to the user.

struct LocalizedDateView: HTMLTemplate {

    struct Context {
        let date: Date
        let locale: String
    }

    var body: HTML {
        Div {
            P {
                context.date
                    .style(date: .short, time: .short)
            }
            P {
                context.date
                    .formatted(string: "MM/dd/yyyy")
            }
        }
        .enviroment(locale: context.locale)
    }
}

Useful Resources to Get Started

Known Issues

  • Linux compiler can sometimes struggle with compiling the code when a combination of complex use of generics, default values or deeply nested function builders are used.