martini-contrib / render

Martini middleware/handler for easily rendering serialized JSON, XML, and HTML template responses.
MIT License
245 stars 57 forks source link

FunctionMaps within a Controller function #32

Closed mickelsonm closed 10 years ago

mickelsonm commented 10 years ago

Is there a way to add a function map within a controller? I want to keep my logic seperated so that I am not exposing models in main.go and I want to be able to evaluate things on the template itself. It currently panics by saying the function map I want to introduce isn't defined, but how do I declare it without putting it on main.go and keeping it specific to the controller? Consider the code snippets below:

main.go:

import(
    "../controllers/blog"
)

func main(){
    m := martini.Classic()
    ...
    m.Get("/blog, blogCtlr.Index")
    ...
    m.Run()
}

m.Get("/blog", blogCtlr.Index)

FuncMap trying to introduce:

FuncMap["showPostsLink"] = func (c blog.Category) bool{
    return len(c.Posts) > 0 && c.Active
}

blogCtlr.go:

import(
    "github.com/martini-contrib/render"
    "github.com/martini-contrib/web"
    "net/http"
    "../models/blog"
)

func Index(ctx *web.Context, r.Render){
    categories, err := blog.GetAllCategories()
    if err != nil{
        ctx.Abort(http.StatusInternalServerError, err.Error())
        return
    }
    data := struct{
        Categories : categories,
    }
    r.HTML(200, "blog", data)
    return
}

blog.tmpl

<div class="categories">
{{range $c := .Categories}}
    <div class="category">
        <div class="categoryname">$c.Name</div>
        {{if showPostsLink $c}}
            <a class="btn btn-default" href="/blog/showposts/$c.Id">Show Posts</a>
        {{end}}
    </div>
{{end}}
</div>

Thanks for any and all help anyone is able to provide!

mickelsonm commented 10 years ago

I tried setting up a global variable called FuncMaps on my controller and then putting it in the render options FuncMap like:

m.Use(render.Renderer(render.Options{
    Directory:       "templates",
    Layout:          "layout",
    Extensions:      []string{".tmpl", ".html"},
    Delims:          render.Delims{"{{", "}}"},
    Charset:         "UTF-8",
    IndentJSON:      true,
    HTMLContentType: "text/html",
    Funcs: []template.FuncMap{
        {
            blogCtlr.FuncMaps
        }
    }
}))

This works for some things, but there are times when you need to compare it against a specific instance, from a list, etc.

var (
    FuncMaps = template.FuncMap{
        "formatDate": func(dt time.Time) string {
            tlayout := "01/02/2006 3:04 PM"
            Local, _ := time.LoadLocation("US/Central")
            return dt.In(Local).Format(tlayout)
        },
        "showCommentsLink": func(c blog.Comments) bool {
            return len(c.Approved) > 0 || len(c.Unapproved) > 0
        },
    }
)

func EditPost(ctx *web.Context, r render.Render) {
    str_id := ctx.FormValue("postid")
    postID, err := strconv.Atoi(str_id)
    if err != nil {
        ctx.Redirect(http.StatusFound, "/blog")
        return
    }

    var post blog.Post

    if postID > 0 {
        post = blog.Post{ID: postID}
        if err := post.Get(); err != nil {
            ctx.Redirect(http.StatusFound, "/blog")
            return
        }
    } else {
        post = blog.Post{ID: 0}
    }

    ...
    FuncMap["isUser"] = func(uid int) bool {
        return uid == post.UserID
    }

    FuncMap["hasCategory"] = func(cid int) bool {
        for _, cat := range post.Categories {
            if cid == cat.ID {
                return true
            }
        }
        return false
    }
    ...
    r.HTML(http.StatusFound, "blog/edit.html", post)
}

...
<div class="form-group">
    <label for="userid" class="col-lg-2 control-label">Author</label>
    <div class="col-lg-10">
        <select name="userid" id="userid">
            <option value="0">Choose an Author</option>
            {{range .AllUsers}}
            <option value="{{.ID}}" {{if isUser .ID }}selected="selected"{{end}}>{{.Fname}} {{.Lname}}</option>
            {{end}}
        </select>
    </div>
</div>
...

So obviously this approach doesn't seem to work either.

mickelsonm commented 10 years ago

@codegangsta : Any ideas or suggestions? Thanks man.

codegangsta commented 10 years ago

This is unfortunately a pain point in the Go templating system. The template loader wants to know all of the functions in the funcmap before the templates are compiled. This is an issue as we precompile our templates for performance reasons.

mickelsonm commented 10 years ago

Thanks for the reply @codegangsta . I agree that it is a pain point, but at the same time it's a very useful thing, especially when doing things with dynamic or semi-dynamic views. I do understand the reasoning for and why things are precompiled, but it doesn't necessarily work in situations where you need to check things prior to rendering/displaying a view. The reasoning for dynamic function map behavior is that it allows me to write components to behave differently from one view to another. For example, I have a component that paginates my data and within my controller I get the data, calculate the number of pages, etc.

The project I am working on really needs this functionality and I am really struggling to figure out a way to make this achievable. One idea I had was to figure out a way to skip the regular compile phase and to somehow compile/render it at run-time. Another would be to publicly expose all rendering methods.

mickelsonm commented 10 years ago

@unrolled : Do you have any ideas or suggestions? I would try to add in the functionality I need myself, but I am really struggling with it so that's why I am asking for some help.

unrolled commented 10 years ago

Hmmm, let me ponder this for a day or two. I understand what you're attempting to do... but doing it in a properly and elegant fashion may be challenging.

unrolled commented 10 years ago

@mickelsonm Well unfortunately I don't have a solid solution for this. My only suggestion would be precompiling what you expect. In your example above you have FuncMaps for isUser, and hasCategory... I would just link the results to a map and pass the result to your template:

func EditPost(ctx *web.Context, r render.Render) {
    str_id := ctx.FormValue("postid")
    postID, err := strconv.Atoi(str_id)
    if err != nil {
        ctx.Redirect(http.StatusFound, "/blog")
        return
    }

    var post blog.Post

    if postID > 0 {
        post = blog.Post{ID: postID}
        if err := post.Get(); err != nil {
            ctx.Redirect(http.StatusFound, "/blog")
            return
        }
    } else {
        post = blog.Post{ID: 0}
    }

    data := make(map[string]interface{})
    data["post"] = post

    ...
    data["isUser"] = (uid == post.UserID)

    data["hasCategory"] = func(cid int) bool {
        for _, cat := range post.Categories {
            if cid == cat.ID {
                return true
            }
        }
        return false
    }(cid)
    ...
    r.HTML(http.StatusFound, "blog/edit.html", data)
}
...
<div class="form-group">
    <label for="userid" class="col-lg-2 control-label">Author</label>
    <div class="col-lg-10">
        <select name="userid" id="userid">
            <option value="0">Choose an Author</option>
            {{range .post.AllUsers}}
            <option value="{{.post.ID}}" {{if .isUser }}selected="selected"{{end}}>{{.post.Fname}} {{.post.Lname}}</option>
            {{end}}
        </select>
    </div>
</div>
...
mickelsonm commented 10 years ago

@unrolled : Thanks for taking the time to review this. I'll try your approach out soon.

mickelsonm commented 10 years ago

So I had a chance to dive into this approach some and have realized that this approach leads to a scoping problem. Note how I am using 'formatDate' and 'showCommentsLink':

Here's my controller/handler:

func Index(rw http.ResponseWriter, r *http.Request, ren render.Render) {
    data := make(map[string]interface{})

    data["PageTitle"] = "Blog"
    data["Categories"], _ = blog.BlogCategory{}.GetAll()
    data["Posts"], _ = blog.Post{}.GetAll()
    data["Comments"], _ = blog.Comment{}.GetAll()

    data["formatDate"] = func(dt time.Time) string {
        tlayout := "01/02/2006 3:04 PM"
        Local, _ := time.LoadLocation("US/Central")
        return dt.In(Local).Format(tlayout)
    }

    data["showCommentsLink"] = func(c blog.Comments) bool {
        return len(c.Approved) > 0 || len(c.Unapproved) > 0
    }

    ren.HTML(http.StatusOK, "blog/index", data)
}

Here's my template:

<tbody>
    {{range $post := .Posts}}
    <tr>
        <td>{{$post.Title}}</td>
        <td>{{.formatDate $post.Created}}</td>
        <td>{{if $post.Published.IsZero }}Not Published {{else}} {{.formatDate $post.Published}}{{end}}</td>
        <td>{{len .Comments.Approved}}</td>
        <td>{{len .Comments.Unapproved}}</td>
        <td>
            <div class="btn-group">
                <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
                Action
                <span class="caret"></span>
                </button>
                <ul class="dropdown-menu" role="menu">
                    <li><a href="/blog/posts/{{$post.ID}}">Edit</a></li>
                    <li><a href="#" class="delete" data-id="{{$post.ID}}">Delete</a></li>
                    {{if .showCommentsLink $post.Comments}}
                    <li><a href="/blog/postcomments/{{$post.ID}}">View Comments</a></li>
                    {{end}}
                </ul>
            </div>
        </td>
    </tr>
    {{end}}
</tbody>

So how do I say to use my function map and not have it think that formatDate and showCommentsLink are fields that are part of my Post object?

unrolled commented 10 years ago

Ahh I see what you're doing there. My implementation was assuming you were dealing with a single post. Not iterating over multiple... With a single post I was insinuating you run any functions against the post before passing it to the template. This would have avoided adding the functions to the FuncMap.

Sorry for the confusion.

mickelsonm commented 10 years ago

I'll go ahead and close this, seeing the pre-rendering of templates makes this impossible to do with the current code base. The lesson I have learned here is that you need to be extremely careful when using function maps in your go templates. Thanks for the help guys!