Swiftpack.co - Package - vapor-community/HTMLKit

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-beta.3"),

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

Usage

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)
            }
            Body {
                Unwrap(context.title) { title in
                    P { title }
                }
            }
        }
    }
}

...
try SimpleTemplate().render(with: .init(...), for: req)

This will render somehing 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'>
    </head>
    <body>
        <p>Some Title</p>
    </body>
</html>

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

public struct Alert: HTMLComponent {

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

    public var body: HTML {
        Div {
            message
            
            IF(isDisimissable) {
                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: isDisimissable) {
            $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 presentet 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.

Github

link
Stars: 172

Used By

Total: 0

Releases

- 2020-01-15 20:38:01

Performance improvements

Changed to using reduce(into: ) in more places, like when escaping variables for HTML. This cut the render time of 10 000 test templates almost in half. From 1.29 seconds to 0.82 seconds.

Added a new modifier

Div {
    "May be modify"
}
.modify(unwrap: someOptional) { value, node in
    // This will only be added if the value is defined
    node.class(value)
}

Other bug-fixes

Some improvements when using modify(if: )

2.0.0-beta.2 - 2019-12-30 10:58:58

Made passing a TemplateValue around easier, since there are now fewer generics.

// HTMLKit 1.0.0-beta.1
struct BaseTemplate<T>: HTMLComponent {

    let context: TemplateValue<T, BaseTemplateContent>

    var body: HTML { ... }
}

// HTMLKit 1.0.0-beta.2
struct BaseTemplate: HTMLComponent {

    @TemplateValue(BaseTemplateContent.self)
    let context

    var body: HTML { ... }
}

Document also adds <html></html> tags and not only the doctype tag

Added a few higher-level tags like Stylesheet, Author, Description. These and Title also add Twitter and OpenGraph metadata.

2.0.0-beta.1 - 2019-12-27 22:33:40

Moved the Vapor providers into two different repositories for Vapor 3 and Vapor 4. As well as performance and stability improvements. There is also added more protocols in order to limit the possibility of adding an attribute that is meaningless attribute to a node.

Added a Unwrap feature:

Unwrap(context.user) { user in
    P { "Welcome, " }
    Bold { user.name }
}

Made TemplateValue a property wrapper:

struct UserTemplate: HTMLTemplate {

    @TemplateValue(User.self) 
    var context

    var body: HTML { ... }
}

New Localisation system, and Date formatting - 2019-05-06 07:56:44

I is now possible to use a new protocol LocalizedTemplate in order to simplify localisation. This removes the locale: \.localePath argument in 1.2.0, by using a static var localePath = \.locale. It will also provide a enum LocalizationKeys where you can store the different keys (like "footer.about.us"), and therefore making it easier when referencing this key. E.g:

localize(.missingValues)
localize(key: "missing.values") // optional

// With a context
localize(.helloWorld, with: \.helloWorldContext)
localize(.helloWorld, with: ["name" : variable(\.name)]) // Can send in a html template with bold etc.
localizeWithContext(.helloWorld) // Using the `Context` struct a long as it conforms to `Encodable`

There is also a new feature to format dates. You can formate the date with DateFormatter.Style for a more general purpose, with dateFormat: String, or send your own formatter, if this is needed. The locale will automatically be sett if used inside a LocalizedTemplate. And therefore format the date in the most natural form.

The runtimeIf(...) is now renderIf(...).

Localisation example:

struct LocalizedView: LocalizedTemplate {

    static let localePath: KeyPath<LocalizedView.Context, String>? = \.locale

    enum LocalizationKeys: String {
        case helloWorld = "hello.world"
        case unreadMessages = "unread.messages"
    }

    /// The content needed to render LocalizationKeys.unreadMessages
    struct UnreadMessageContext: Codable {
        let number: Int
    }

    struct Context: Codable {
        let locale: String
        let description: UnreadMessageContext
        let number: Int
        let date: Date = Date()
    }

    func build() -> CompiledTemplate {
        return div.child(
            h1.child(
                localize(.helloWorld),
                small.child(
                    date(\.date, dateStyle: .medium, timeStyle: .medium)
                )
            ),
            p.child(
                localize(.unreadMessages, with: \.description)
            ),
            p.child(
                localizeWithContext(.unreadMessages)
            )
        )
    }
}

This could lead to

<!-- locale = "en", number = 1  -->
<div>
    <h1>Hello World!
        <small>May 6, 2019 at 10:24:50 AM</small>
    </h1>
    <p>You have an unread message</p>
    <p>You have an unread message</p>
</div>

<!-- locale "nb", number = 2 -->
<div>
    <h1>Hei Verden!
        <small>6. mai 2019, 10:24:50</small>
    </h1>
    <p>Du har 2 uleste meldinger.</p>
    <p>Du har 2 uleste meldinger.</p>
</div>

Markdown, Localization and concatenation - 2019-04-16 10:58:31

  • Markdown is finally supported by using the markdown("# Title") function. (Using SwiftMarkdown)
  • Localization is also supported by using the localize("hello.world", in: \.local) function The Lingo framework is used to handle localization logic.
  • It is now possible to use + to concatenate different elements Eg:
div.child(
    "Hello, " + variable(\.name) + p.child("View More")
)

Better if-statements on html nodes - 2019-03-27 19:24:14

Added:

  • .if(isNil: ..., add: ...)
  • .if(isNotNil: ..., add: ...)

e.g: div.if(isNil: \.someValue, add: .class("bg-danger"))

And a little more documentation

- 2019-03-11 14:02:27

Made the Request extension public, as this was left as internal by a mistake

- 2019-03-11 13:44:15

Added

  • A Vapor HTMLKitProvider and an extension on Request for simpler usage.
  • All the nodes are dynamic by default, and therefore removed dynamic(...).
  • Added the possibility to reference context variables outside the specified context with unsafeVariable(...). Example to retrieve a value in a superview.
  • Changed forEachInContext(render: ...) to forEach(render: ...).
  • More flexible conditions when using && and ||.
  • Made TemplateBuilder internal as the developer should use StaticView or ContextualTemplate.
  • render(...) now returns a HTTPResponse and the old render function is therefore called renderRaw(...).