2

Context:

Recently, I've decided to take up Swift server side development because I think the Vapor framework is extremely cool. I've gotten a bit stuck while experimenting and would like some advice on templating with leaf and vapor.

I've reviewed the documentation several times when it comes to rendering views. Rendering a templated view with variables requires the name of the leaf template and a Response Representable node object containing the variables.

Trying to work out a scenario with templating and the framework itself (because that's how I learn best), I tried to mock a blog format. This is my class/get request:

// MARK: Blog Post Object

final class BlogPost: NodeRepresentable {

    var postId: Int
    var postTitle: String
    var postContent: String
    var postPreview: String

    func makeNode(context: Context) throws -> Node {
        return try Node(node: [
            "postId":self.postId,
            "postTitle":self.postTitle,
            "postContent":self.postContent,
            "postPreview":self.postPreview
        ])
    }

    init(_ postId: Int, _ postTitle: String, _ postContent: String) {

        self.postId = postId
        self.postTitle = postTitle
        self.postContent = postContent
        self.postPreview = postContent.trunc(100)
    }
}


// MARK: Blog view request; iterate over blog objects

drop.get("blog") { request in
    let result = try drop.database?.driver.raw("SELECT * FROM Posts;")

    guard let posts = result?.nodeArray else {
        throw Abort.serverError
    }

    var postCollection = [BlogPost]()


    for post in posts {
        guard let postId = post["postId"]?.int,
            let postTitle = post["postTitle"]?.string,
            let postContent = post["postPreview"]?.string else {
                throw Abort.serverError
        }

        let post = BlogPost(postId, postTitle, postContent)
        postCollection.append(post)
    }

    // Pass posts to be tokenized

    /* THIS CODE DOESN'T WORK BECAUSE "CANNOT CONVERT VALUE OF TYPE 
     * '[BLOGPOST]' TO EXPECTED DICTIONARY VALUE OF TYPE "NODE"
     * LOOKING FOR THE BEST METHOD TO PASS THIS LIST OF OBJECTS
     */

    drop.view.make("blog", [
        "posts":postCollection 
    ])

}

and this is my blog.leaf file:

#extend("base")

#export("head") {
    <title>Blog</title>
}

#export("body") {

    <h1 class="page-header">Blog Posts</h1>

    <div class="page-content-container">

    #loop(posts, "posts") {
        <div class="post-container">
            <h3 style="post-title">#(posts["postTitle"])</h3>
            <p style="post-preview">#(posts["postPreview"])</h3>
        </div>
    }

    </div>

}

Problem:

As you can see, I'm a bit stuck on finding the best method for iterating over objects and templating their properties into the leaf file. Anyone have any suggestions? Sorry for the bad programming conventions, by the way. I'm fairly new in Object/Protocol Oriented Programming.

Caleb Kleveter
  • 11,170
  • 8
  • 62
  • 92
Charles Kenney
  • 360
  • 3
  • 12

3 Answers3

2

What I ended up doing is, making the Post model conform to the Model protocol.

import Foundation
import HTTP
import Vapor


// MARK: Post Class

final class Post: Model {
    
    var id: Node?
    var title: String
    var content: String
    var date: Date
    var isVisible: Bool
    
    // TODO: Implement truncate extension for String and set preview
    // to content truncated to 100 characters
    
    var preview = "placeholder"
    
    var exists: Bool = false
    
    init(title: String, content: String, isVisible: Bool = true) {
        
        self.title = title
        self.content = content
        self.date = Date()
        self.isVisible = isVisible
    }
    
    init(node: Node, in context: Context) throws {
        
        let dateInt: Int = try node.extract("date")
        let isVisibleInt: Int = try node.extract("isVisible")
        
        id = try node.extract("id")
        title = try node.extract("title")
        content = try node.extract("content")
        date = Date(timeIntervalSinceNow: TimeInterval(dateInt))
        isVisible = Bool(isVisibleInt as NSNumber)
        exists = false
    }
    
    func makeNode(context: Context) throws -> Node {
        
        return try Node(node: [
            
            "id": id,
            "title": title,
            "content": content,
            "date": Int(date.timeIntervalSince1970),
            "isVisible": Int(isVisible as NSNumber)
            ])
    }
    
    static func prepare(_ database: Database) throws {
        
        try database.create("Posts") { posts in
            
            posts.id()
            posts.string("title", optional: false)
            posts.string("content", optional: false)
            posts.int("date", optional: false)
            posts.int("isVisible", optional: false)
        }
    }
    
    static func revert(_ database: Database) throws {
        
        try database.delete("posts")
    }
}

Then to return/create instances of the Post object:

import Vapor
import Foundation
import HTTP

final class BlogController {
    
    func addRoutes(_ drop: Droplet) {
        
        let blogRouter = drop.grouped("blog")
        let blogAPIRouter = drop.grouped("api","blog")
        
        blogRouter.get("posts", handler: getPostsView)
        
        blogAPIRouter.get("posts", handler: getPosts)
        blogAPIRouter.post("newPost", handler: newPost)
    }
    
    // MARK: Get Posts
    func getPosts(_ request: Request) throws -> ResponseRepresentable {
        
        let posts = try Post.all().makeNode()
        return try JSON(node: [
            "Posts":posts
            ])
    }
    
    // Mark: New Post
    func newPost(_ request: Request) throws -> ResponseRepresentable {
        guard let title = request.data["title"]?.string,
            let content = request.data["content"]?.string else {
                
                throw Abort.badRequest
        }
        
        var post = Post(title: title, content: content)
        try post.save()
        
        return "success"
    }
    
    // Mark: Get Posts Rendered
    func getPostsView(_ request: Request) throws -> ResponseRepresentable {
        return try getPosts(request)
    }
    
    
}
Charles Kenney
  • 360
  • 3
  • 12
0

I'm not an expert on Vapor yet, but I think you need to use .makeNode() so your postCollection object get converted to something you can later use on the template.

Something like this:

drop.view.make("blog", ["posts":postCollection.makeNode()])

Victor Alejandria
  • 120
  • 1
  • 3
  • 10
  • What I ended up doing is, I made the post model conform to the Model protocol. Then I did this to return post objects: ` // MARK: Get Posts func getPosts(_ request: Request) throws -> ResponseRepresentable { let posts = try Post.all().makeNode() return try JSON(node: [ "Posts":posts ]) }` This is node representable, so it can be used in the rendering of a view. – Charles Kenney Apr 14 '17 at 03:07
0
func list(_ req: Request) throws -> ResponseRepresentable {
let list = try User.all()
let node = try list.makeNode(in: nil)
let json =  try JSON(node: [ "list":node ])
return json 
}
Deepak Singh
  • 241
  • 3
  • 11