hummingbird-project / swift-mustache

Mustache templating engine
Apache License 2.0
21 stars 2 forks source link

Using Fluent models in templates #20

Closed JulioBarros closed 1 year ago

JulioBarros commented 1 year ago

Hi,

I'm trying out hummingbird and am having an issue with using fluent models in templates. I basically have something like this.

let html = request.mustache.render(["topics": ts], withTemplate: "topics")!

and

        {{#topics}}
            <div><strong>{{title}}</strong></div>
        {{/topics}}

When ts is an array or struct or regular classes the template renders fine. However if ts is straight from a call from fluent I don't get anything to render. So,

let ts =  try await Topic.query(on: request.db).all()

Does not work but

        let topics =  try await Topic.query(on: request.db).all()
        let ts = topics.map{ Foo(title: $0.title, description: $0.description) }

(where foo is either a struct or class) does.

What am I missing? Thanks in advance.

adam-fowler commented 1 year ago

The properties in a Fluent model are wrapped in propertyWrappers that is why they don't work. You need another level of indirection. It looks like FieldProperty stores is output values in a field outputValue. You could try below. I haven't tested this but you might be able to use the following template

{{#topics}}
    <div><strong>{{title.outputValue}}</strong></div>
{{/topics}}

Given this is using an internal detail of the @Field property wrapper you are probably better copying into a separate struct

adam-fowler commented 1 year ago

Another solution might be to extend FieldProperty from FluentKit as follows. This would be a lot cleaner

extension FieldProperty: HBMustacheCustomRenderable {
    /// default version returning the standard rendering
    public var renderText: String { String(describing: self.value) }
    /// default version returning false
    public var isNull: Bool { self.value == nil }
}
JulioBarros commented 1 year ago

Thanks for the suggestions. Unfortunately, I could not get either one to work (perhaps due to my limited swift knowledge) but I'll try something else. Thanks again.

adam-fowler commented 1 year ago

Ok I'll have a closer look.

adam-fowler commented 1 year ago

Ok the following should work. You need to change the extension to FieldProperty slightly

extension FieldProperty: HBMustacheCustomRenderable {
    /// default version returning the standard rendering
    public var renderText: String {
        self.value.map { String(describing: $0) } ?? ""
    }

    /// default version returning false
    public var isNull: Bool {
        self.value == nil
    }
}

And then you add an '_' prefix to the variable references in your moustache

{{#topics}}
    <div><strong>{{_title}}</strong></div>
{{/topics}}
JulioBarros commented 1 year ago

Yes that works. Thanks!

adam-fowler commented 1 year ago

Another solution which will give better results is the following. Conform FieldProperty to HBMustacheTransformable and add a transform function wrappedValue which returns the property wrapper wrappedValue.

extension FieldProperty: HBMustacheTransformable {
    public func transform(_ name: String) -> Any? {
        switch name {
        case "wrappedValue":
            return wrappedValue
        default:
            return nil
        }
    }
}

Then in your mustache you add

{{#topics}}
    <div><strong>{{wrappedValue(_title)}}</strong></div>
{{/topics}}

The reason this is better than the above solution is the wrappedValue function returns the actual wrappedValue and not a string describing it. If you are only printing strings it doesn't make any difference but if you have sections using the property wrapper variables you want this solution so it can respond to booleans, optionals, and collections correctly.