Closed mickelsonm closed 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.
@codegangsta : Any ideas or suggestions? Thanks man.
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.
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.
@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.
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.
@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>
...
@unrolled : Thanks for taking the time to review this. I'll try your approach out soon.
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?
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.
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!
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:
FuncMap trying to introduce:
blogCtlr.go:
blog.tmpl
Thanks for any and all help anyone is able to provide!