leaf-kit icon indicating copy to clipboard operation
leaf-kit copied to clipboard

Custom tags with body

Open koenpunt opened this issue 4 years ago • 2 comments

I tried to define a custom tag that accepts a body, but the body is provided to the tag as [Syntax], which is not something that converts to the required return type of LeafData.

How can I capture the body, and return if necessary?

For context, I'm trying to create requireRole(role) tag, that returns the body if the role matches the role of the current user;

#requireRole("ADMIN"):
<a href="/projects/#(project.id)/delete">Delete</a>
#endrequireRole

In the tag class I retrieve the body using ctx.requireBody(), which, as mentioned, returns an array of Syntax elements.

final class RequireRoleTag<A>: LeafTag where A: RoleAuthenticatable {
    init(userType: A.Type) {}

    func render(_ ctx: LeafContext) throws -> LeafData {
        try ctx.requireParameterCount(1)
        let body = try ctx.requireBody()

        guard let requiredRole = ctx.parameters[0].string else {
            throw "role is not a string"
        }

        guard let req = ctx.request,
              let role = getRole(req: req)
        else {
            return .trueNil
        }

        if role == requiredRole {
            // This doesn't work
            return body
        }

        return .trueNil
    }

    private func getRole(req: Request) -> String? {
        let a = req.auth.get(A.self)
        return a?.role.description
    }
}

koenpunt avatar Jan 05 '21 16:01 koenpunt

+1, this feels like it should be straightforward as #requireRole is essentially syntactic sugar over #if.

A slightly more complex case that I was hoping to solve with a custom tag is injecting common variables without needing to set them in the view model. For example...

#withUser():
    <img src="#(user.avatarURL)" title="#(user.name) />
#endwithUser

When building re-usable templates such as navigation, there are many use-cases for wanting common data available without needing to add all that common context to the view model/controller. I guess this could all be bundled up into some common view model type, but that's still extra code that's required.

danpalmer avatar Jul 16 '23 15:07 danpalmer

How to return LeafData from [Syntax] (only works correctly if there are no nested Leaf tags, so doesn't solve your issue):

let body = try ctx.requireBody()

guard case let .raw(byteBuffer) = body.first else {
    throw "Body not found"
}
            
return .data(Data(buffer: byteBuffer))

Make sure your custom tag conforms to UnsafeUnescapedLeafTag, because the body may contain HTML tags.

andreasley avatar Sep 05 '23 10:09 andreasley