Elementary

repository·main·Indexed 18 days ago

https://github.com/elementary-swift/elementary

A modern, lightweight HTML templating library for Swift featuring a SwiftUI-inspired declarative API. It is optimized for high-performance server-side rendering with support for Swift Concurrency, streaming, and AsyncSequence via AsyncForEach. Elementary provides official integration packages for the Hummingbird and Vapor web frameworks and includes a TaskLocal-powered environment system for managing values across component trees.

Tokens
2.3K
Snippets
9
Records
10
Agent score
14%

What's inside Elementary

  1. Compose HTML with SwiftUI-inspired syntax

    main

    Elementary uses a declarative, SwiftUI-inspired composition API. You define your HTML structure by conforming to the HTML or HTMLDocument protocols and implementing a body property.

    struct FeatureList: HTML {
        var features: [String]
    
        var body: some HTML {
            ul {
                for feature in features {
                    li { feature }
                }
            }
        }
    }
  2. Set up dev mode with auto-reload on save

    main

    The demo includes a swift-dev script that enables a development workflow where open browser tabs automatically reload when Swift source files are changed. This workflow relies on watchexec for file watching and browsersync for browser synchronization.

    Prerequisites

    On macOS, install the required tools using Homebrew and npm:

    npm install -g browser-sync
    brew install watchexec

    Running in watch-mode

    Execute the following script to watch all Swift files in the demo package, build on-demand, and re-sync the browser page:

    ./swift-dev
    npm install -g browser-sync
    brew install watchexec
    ./swift-dev
  3. Install Elementary via Swift Package Manager

    main

    Add Elementary as a dependency to your Package.swift file to use it for HTML templating in pure Swift.

    .package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0")
    .product(name: "Elementary", package: "elementary")
  4. Integrate Elementary with Hummingbird or Vapor

    main

    Elementary provides official integration packages for popular Swift web frameworks to streamline HTML rendering in your server-side applications.

    Hummingbird Integration

    Add HummingbirdElementary to your Package.swift:

    .package(url: "https://github.com/hummingbird-community/hummingbird-elementary.git", from: "0.3.0")
    .product(name: "HummingbirdElementary", package: "hummingbird-elementary")

    Vapor Integration

    Add VaporElementary to your Package.swift:

    .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0")
    .product(name: "VaporElementary", package: "vapor-elementary")
    // Hummingbird
    .package(url: "https://github.com/hummingbird-community/hummingbird-elementary.git", from: "0.3.0")
    .product(name: "HummingbirdElementary", package: "hummingbird-elementary")
    
    // Vapor
    .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0")
    .product(name: "VaporElementary", package: "vapor-elementary")
  5. Manage environment values with TaskLocals

    main

    Elementary provides a lightweight environment system powered by TaskLocals. This allows you to pass data down the component tree without explicit parameter passing.

    1. Define a TaskLocal key.
    2. Use the @Environment property wrapper in your component to access it.
    3. Use the .environment(_:_: ) modifier to provide the value.
    enum MyValues {
        @TaskLocal static var userName = "Anonymous"
    }
    
    struct MyComponent: HTML {
        @Environment(MyValues.$userName) var userName
    
        var body: some HTML {
            p { "Hello, \(userName)!" }
        }
    }
    
    // Providing the value
    MyComponent().environment(MyValues.$userName, "Drax")
    enum MyValues {
        @TaskLocal static var userName = "Anonymous"
    }
    
    struct MyComponent: HTML {
        @Environment(MyValues.$userName) var userName
        var body: some HTML {
            p { "Hello, \(userName)!" }
        }
    }
    
    // Usage
    MyComponent().environment(MyValues.$userName, "Drax")
  6. Render HTML to strings or streams

    main

    Elementary supports multiple rendering modes depending on your needs: streaming for performance or string collection for simplicity.

    Streaming HTML

    For high-performance response streaming (e.g., in Hummingbird or Vapor), use render(into:). This allows the browser to start loading the page while the server is still producing content, using Swift concurrency to handle back pressure.

    Rendering to String

    To collect the entire rendered HTML into a single String, use the .render() method on any type conforming to HTML.

    Formatted Rendering

    For testing or debugging, use .renderFormatted() to produce human-readable, indented HTML.

    // Stream HTML
    try await MainPage().render(into: responseStreamWriter)
    
    // Render to String
    let html: String = div(.class("pretty")) { "Hello" }.render()
    
    // Formatted for testing
    print(div { p { "Hi" } }.renderFormatted())
    // Stream HTML, optimized for responsiveness and back pressure-aware
    try await MainPage().render(into: responseStreamWriter)
    
    // Collect in a string
    let html: String = div(.class("pretty")) { "Hello" }.render()
    
    // Formatted version for testing
    print(div { p { "Hi" } }.renderFormatted())
  7. Handle HTML attributes and conditional styling

    main

    Attributes are passed directly into the element function or applied via modifiers. Elementary uses generics to ensure attributes are type-safe for the specific tag being used.

    Inline Attributes

    Attributes are placed immediately after the tag name:

    div(.data("hello", value: "there")) { ... }

    Conditional Attributes

    Use the .attributes(_:when:) modifier to apply attributes based on a boolean condition:

    li { text }
        .attributes(.class("important"), when: isImportant)

    Attribute Fallthrough

    If you expose the HTMLTag type in your component's body, attributes applied to the component will 'fall through' to the underlying element:

    struct Button: HTML {
        var text: String
        var body: some HTML<HTMLTag.input> {
            input(.type(.button), .value(text))
        }
    }
    
    // Usage:
    Button(text: "Hello").attributes(.autofocus)

    Note on Merging: By default, class and style attributes are merged (using spaces or semicolons). All other attributes are overwritten if a duplicate is provided.

    // Inline
    div(.data("hello", value: "there")) { ... }
    
    // Conditional
    li { text }
        .attributes(.class("important"), when: isImportant)
    
    // Fallthrough
    struct Button: HTML {
        var text: String
        var body: some HTML<HTMLTag.input> {
            input(.type(.button), .value(text))
        }
    }
    
    Button(text: "Hello").attributes(.autofocus)
  8. Use Async support in HTML content

    main

    Elementary supports Swift Concurrency directly within your HTML definitions. You can await data inside a body block or use specialized elements for streaming sequences.

    Awaiting data in body

    You can perform asynchronous work directly inside the element closures:

    div {
        let text = await getMyData()
        p { "This totally works: \(text)" }
    }

    AsyncForEach for sequences

    To efficiently render an AsyncSequence without loading the entire collection into memory, use AsyncForEach. This streams each element to the HTML output as it becomes available:

    ul {
        let users = try await db.users.findAll()
        AsyncForEach(users) { user in
            li { "\(user.name)" }
        }
    }
    // Awaiting inside a block
    div {
        let text = await getMyData()
        p { "Result: \(text)" }
    }
    
    // Streaming an AsyncSequence
    AsyncForEach(users) { user in
        li { "\(user.name)" }
    }