Skip to content

Commit

Permalink
Added a new basic dynamic htmx template and added a way to customise …
Browse files Browse the repository at this point in the history
…its default template name and a slot name
  • Loading branch information
RussBaz committed Jan 8, 2024
1 parent 582d68d commit 91ceea5
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 29 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/vapor.git",
"state" : {
"revision" : "67fe736c37b0ad958b9d248f010cff6c1baa5c3a",
"version" : "4.89.3"
"revision" : "0680f9f6bfab7100cd585b3186740ee7860c983e",
"version" : "4.91.1"
}
},
{
Expand Down
66 changes: 53 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,13 @@ Basic configuration (`configure.swift`):
import Vapor
import VHX

// Assumes 'index-base.leaf' template exists and that it contains '#import("body")' tag
// Generates a dynamic template that wraps the content of the specified template
// This is the template that will be returned when a standard html GET request is made to an htmx endpoint.
// The default implementation simply returns '#extend("\(name)")'
func pageTemplate(_ template: String) -> String {
"""
#extend("index-base"): #export("body"): #extend("\(template)") #endexport #endextend
"""
}
// Basic config assumes 'index-base.leaf' template exists and that it contains '#import("body")' tag
// It will generate a dynamic template that wraps the content of the specified template for NON-htmx calls to 'htmx.render'
// It will simply plug the provided template into the 'body' slot of the base template

public func configure(_ app: Application) async throws {
try configureHtmx(app, pageTemplate: pageTemplate)
let config = HtmxConfiguration.basic()
try configureHtmx(app, configuration: config)
}
```

Expand Down Expand Up @@ -123,7 +118,7 @@ SPM installation:
- Add the package to your package dependencies

```swift
.package(url: "https://github.com/RussBaz/VaporHX.git", from: "0.0.18"),
.package(url: "https://github.com/RussBaz/VaporHX.git", from: "0.0.19"),
```

- Then add it to your target dependencies
Expand All @@ -134,7 +129,29 @@ SPM installation:

### Configuration

Assuming the standard use of `configure.swift`:
Assuming the standard use of `configure.swift' in all the following examples.

The simplest config (without localisation helpers):

```swift
import Vapor
import VHX

// Basic config assumes 'index-base.leaf' template exists and that it contains '#import("body")' tag
// It will generate a dynamic template that wraps the content of the specified template for NON-htmx calls to 'htmx.render'
// It will simply plug the provided template into the 'body' slot of the base template

public func configure(_ app: Application) async throws {
// other configuration
let config = HtmxConfiguration.basic()
try configureHtmx(app, configuration: config)
// more configuration
}
```

Please note that the default wrapper allows dynamically changing the base template name and the slot name. Please refer to the `render` function.

Otherwise, if you want to specify your own htmx page wrapper (plugs the provided template name into a dynamically generated page on NON-HTMX requests):

```swift
// The most straightforward configuration
Expand Down Expand Up @@ -179,6 +196,9 @@ struct HtmxConfiguration {
init(pagePrefix prefix: String)
init(pagePrefix prefix: String = "--page", pageTemplate template: @escaping (_ name: String) -> String)
init(pageSource: HXLeafSource, errorAttemptCountHeaderName: String? = nil)

// Default basic configuration
static func basic(pagePrefix prefix: String = "--page", baseTemplate: String = "index-base", slotName: String = "body") -> Self
}
```

Expand All @@ -204,10 +224,11 @@ struct HXBasicLeafSource: HXLeafSource {
}
```

In order to manually initialise this struct, please use the following function:
In order to manually initialise this struct, please use the following functions:

```swift
func hxPageLeafSource(prefix: String = "--page", template: ((_ name: String) -> String)?) -> HXLeafSource
func hxBasicPageLeafSource(prefix: String = "--page", baseTemplate: String = "index-base", slotName: String = "body") -> HXLeafSource
```

In our case the default `pagePrefix` value is `--page`. Therefore, everytime you ask `leaf` for a template prefixed with `--page/` (please do not miss `/` after the prefix, it is always required), the default `HXBasicLeafSource` will return a template generated by the `pageTemplate` closure. Everything after the prefix with `/` will be passed into the page template generator and the result of this function should be a valid `leaf` template as a string.
Expand Down Expand Up @@ -248,6 +269,23 @@ func render(_ name: String, _ context: some Encodable, page: Bool? = nil, header
func render(_ name: String, page: Bool? = nil, headers: HXResponseHeaders? = nil) async throws -> Response
```

Furthermore, if you are using the default template generator, you can manually override the base template name and the slot name. Here is an example how it can be done:

```swift
// Use square brackets at the beginning of the name to override default base template and slot names

routes.get("template") { req in
// Just square brackets without colons to override base template name only
try await req.htmx.render("[index-custom]name")
}

routes.get("slot") { req in
// Use the following format to override a slot name: [template:slot]
// Using multiple colons will result in an error
try await req.htmx.render("[index-custom:extra]name")
}
```

To learn more about the `HXResponseHeaders`, please refere to the Response Headers section.

How to redirect quickly with proper HTMX headers?
Expand Down Expand Up @@ -380,4 +418,6 @@ app.get("redirect") { req in

#### Location

`HXLocationHeader` is type safe constructor for a `HX-Location` response header. It is the most complicated response header in this library but it is thankfully a rarely used one.

To be continued...
132 changes: 132 additions & 0 deletions Sources/VHX/Htmx/HXBasicLeafTemplate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
enum HXBasicLeafTemplate {}

extension HXBasicLeafTemplate {
enum ParsedTemplate {
case basic(String)
case custom(template: String, slot: String?, value: String)
}

static func parseBasicTemplate(value: String) -> ParsedTemplate {
enum State {
case template
case slot
case value
case error
}

if value.starts(with: "[") {
var pos = value.index(after: value.startIndex)

var templateEndPos: String.Index?
var slotEndPos: String.Index?

func more(state: State) -> State {
switch state {
case .template:
guard pos < value.endIndex else {
return .error
}

let c = value[pos]

if c == "]" {
templateEndPos = pos
return .value
} else if c == ":" {
templateEndPos = pos
return .slot
} else {
return .template
}
case .slot:
guard pos < value.endIndex else {
return .error
}

let c = value[pos]

if c == "]" {
slotEndPos = pos
return .value
} else if c == ":" {
return .error
} else {
return .slot
}
case .value:
return .value
case .error:
return .error
}
}

func parse(state: State) -> State {
let next = more(state: state)
pos = value.index(after: pos)

switch next {
case .template:
return parse(state: next)
case .slot:
return parse(state: next)
case .value:
return .value
case .error:
return .error
}
}

func scheduleParse() -> Bool {
let result = parse(state: .template)

guard result == .value else { return false }

return true
}

guard scheduleParse() else { return .basic(value) }

guard let templateEndPos else { return .basic(value) }

let templateName = String(value[value.index(after: value.startIndex) ..< templateEndPos])

if let slotEndPos {
let slotName = String(value[value.index(after: templateEndPos) ..< slotEndPos])
let remainder = String(value[value.index(after: slotEndPos) ..< value.endIndex])

guard !templateName.isEmpty, !remainder.isEmpty, !slotName.isEmpty else { return .basic(value) }

return .custom(template: templateName, slot: slotName, value: remainder)
} else {
let remainder = String(value[value.index(after: templateEndPos) ..< value.endIndex])

guard !templateName.isEmpty, !remainder.isEmpty else { return .basic(value) }

return .custom(template: templateName, slot: nil, value: remainder)
}
}

return .basic(value)
}

static func prepareBasicTemplateBuilder(defaultBaseTemplate: String, defaultSlotName: String) -> PageTemplateBuilder {
func templateBuilder(_ name: String) -> String {
let result = parseBasicTemplate(value: name)

switch result {
case let .basic(value):
return
"""
#extend("\(defaultBaseTemplate)"): #export("\(defaultSlotName)"): #extend("\(value)") #endexport #endextend
"""
case let .custom(template, slot, value):
return
"""
#extend("\(template)"): #export("\(slot ?? defaultSlotName)"): #extend("\(value)") #endexport #endextend
"""
}
}

return templateBuilder
}
}
9 changes: 8 additions & 1 deletion Sources/VHX/Htmx/HXConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ public extension HtmxConfiguration {
errorAttemptCountHeaderName = nil
}

init(pagePrefix prefix: String = "--page", pageTemplate template: @escaping (_ name: String) -> String) {
init(pagePrefix prefix: String = "--page", pageTemplate template: @escaping PageTemplateBuilder) {
pageSource = hxPageLeafSource(prefix: prefix, template: template)
errorAttemptCountHeaderName = nil
}

static func basic(pagePrefix prefix: String = "--page", baseTemplate: String = "index-base", slotName: String = "body") -> Self {
let pageSource = hxBasicPageLeafSource(prefix: prefix, baseTemplate: baseTemplate, slotName: slotName)
let errorAttemptCountHeaderName: String? = nil

return .init(pageSource: pageSource, errorAttemptCountHeaderName: errorAttemptCountHeaderName)
}
}
12 changes: 10 additions & 2 deletions Sources/VHX/Htmx/HXLeafSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ public protocol HXLeafSource: LeafSource {
var pagePrefix: String { get }
}

public typealias PageTemplateBuilder = (_ name: String) -> String

public struct HXBasicLeafSource: HXLeafSource {
public let pagePrefix: String
public let pageTemplate: (_ name: String) -> String
public let pageTemplate: PageTemplateBuilder

public enum HtmxPageLeafSourceError: Error {
case illegalFormat
Expand All @@ -32,7 +34,7 @@ public struct HXBasicLeafSource: HXLeafSource {
}
}

public func hxPageLeafSource(prefix: String = "--page", template: ((_ name: String) -> String)?) -> HXLeafSource {
public func hxPageLeafSource(prefix: String = "--page", template: PageTemplateBuilder?) -> HXLeafSource {
if let template {
return HXBasicLeafSource(pagePrefix: prefix, pageTemplate: template)
} else {
Expand All @@ -44,3 +46,9 @@ public func hxPageLeafSource(prefix: String = "--page", template: ((_ name: Stri
return HXBasicLeafSource(pagePrefix: prefix, pageTemplate: template)
}
}

public func hxBasicPageLeafSource(prefix: String = "--page", baseTemplate: String = "index-base", slotName: String = "body") -> HXLeafSource {
let template = HXBasicLeafTemplate.prepareBasicTemplateBuilder(defaultBaseTemplate: baseTemplate, defaultSlotName: slotName)

return HXBasicLeafSource(pagePrefix: prefix, pageTemplate: template)
}
23 changes: 17 additions & 6 deletions Sources/VHX/Htmx/Htmx.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ public extension Htmx {
func render(_ name: String, _ context: some Encodable, page: Bool? = nil, headers: HXResponseHeaderAddable? = nil) async throws -> Response {
let page = page ?? (req.method == .GET && !req.headers.htmx.request)

let view = if page {
try await req.view.render("\(req.application.htmx.pageSource.pagePrefix)/\(name)", context).get()
let templateName: String = if page {
"\(req.application.htmx.pageSource.pagePrefix)/\(name)"
} else {
try await req.view.render(name, context).get()
if name.starts(with: "["), let i = name.firstIndex(of: "]") {
String(name[name.index(after: i) ..< name.endIndex])
} else {
name
}
}

let view = try await req.view.render(templateName, context).get()
let response = try await view.encodeResponse(for: req)

if let headers {
Expand All @@ -81,12 +86,18 @@ public extension Htmx {
func render(_ name: String, page: Bool? = nil, headers: HXResponseHeaderAddable? = nil) async throws -> Response {
let page = page ?? (req.method == .GET && !req.headers.htmx.request)

let view = if page {
try await req.view.render("\(req.application.htmx.pageSource.pagePrefix)/\(name)")
let templateName: String = if page {
"\(req.application.htmx.pageSource.pagePrefix)/\(name)"
} else {
try await req.view.render(name)
if name.starts(with: "["), let i = name.firstIndex(of: "]") {
String(name[name.index(after: i) ..< name.endIndex])
} else {
name
}
}

let view = try await req.view.render(templateName)

let response = try await view.encodeResponse(for: req)

if let headers {
Expand Down
15 changes: 13 additions & 2 deletions Sources/VHX/Tags/HXTextTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ public enum TextTagError: Error {

public struct HXTextTag: LeafTag {
public func render(_ ctx: LeafContext) throws -> LeafData {
guard ctx.parameters.count == 1 else {
guard ctx.parameters.count == 1 || ctx.parameters.count == 2 else {
throw TextTagError.wrongNumberOfParameters
}
guard let text = ctx.parameters[0].string else {
throw TextTagError.invalidFormatParameter
}

let code: String?

if ctx.parameters.count == 2 {
guard let c = ctx.parameters[1].string else {
throw TextTagError.invalidFormatParameter
}
code = c
} else {
code = nil
}

let localised = if let req = ctx.request {
req.language.localise(text: text)
req.language.localise(text: text, for: code)
} else {
text
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/VHX/VHX.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Vapor

public func configureHtmx(_ app: Application, pageTemplate template: ((_ name: String) -> String)? = nil) throws {
public func configureHtmx(_ app: Application, pageTemplate template: PageTemplateBuilder? = nil) throws {
let config = if let template {
HtmxConfiguration(pageTemplate: template)
} else {
Expand Down
Loading

0 comments on commit 91ceea5

Please sign in to comment.