Swiftpack.co - Package - vapor-community/HTMLKit

HTMLKit logo

HTMLKit

Render lightning fast HTML templates in a typesafe 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-alpha.1"),

And register the provider and the different templates with in configure.swift

try services.register(HTMLKitProvider())

// Or you can do it manually with
let renderer = HTMLRenderer()
try renderer.add(view: MyTemplate())
services.register(renderer)

Usage

To create a HTML component, conform to the HTMLComponent protocol. Look at the example above for an Alert component.

public struct Alert: HTMLComponent {

    let message: HTML
    let isDisimissable: Conditionable

    public init(isDisimissable: Conditionable = true, @HTMLBuilder message: () -> HTML) {
        self.isDisimissable = isDisimissable
        self.message = message()
    }

    public var body: HTML {
        Div {
            message
            IF(isDisimissable) {
                Button {
                    Span { "×" }
                        .aria(for: "hidden", value: true)
                }
                .type(.button)
                .class("close")
                .data(for: "dismiss", value: "alert")
                .aria(for: "label", value: "Close")
            }
        }
        .class("alert alert-danger bg-danger" + IF(isDisimissable) { " fade show" })
        .role("alert")
    }
}

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

struct SomePage: StaticView {

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

If you need a page that changes based on runtime given values. Then a TemplateView could be more appropriate. Here you will need to pass the context when rendering.


struct DummyPage: TemplateView {

    struct Context {
        let string: String
        let int: Int?
    }

    var body: HTML {
        Div {
            P { context.string }
            IF(context.int.isDefined) {
                Small { context.int }
            }
        }
    }
}
...
let context = DummyPage.Context(string: "Some string", int: nil)
try DummyPage().render(with: context, for: req)

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: TemplateView {

    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

Github

link
Stars: 65
Help us keep the lights on

Releases

1.3.0 - May 6, 2019

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>

1.2.0 - Apr 16, 2019

  • 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")
)

1.1.3 - Mar 27, 2019

Added:

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

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

And a little more documentation

1.1.1 - Mar 11, 2019

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

1.1.0 - Mar 11, 2019

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(...).