Commit 92fe3ff4 authored by Dustin L. Howett's avatar Dustin L. Howett
Browse files

Expose .Obj, .With and .Value.

* {{.Obj}}
  The object passed into a bound template's Exec.
* {{.With Name Value}}
  Returns a new view context with the specified key-value pair.
* {{.Value Name}}
  Returns the value prevously bound to Name in this context stack.
parent 00597432
package views
import "net/http"
type contextShared struct {
request *http.Request
page string
// unguarded vis-a-vis concurrency: a single viewContext is not used in
// a multithreaded context.
varCache map[string]interface{}
}
// viewContext represents the template context passed to each render.
type viewContext struct {
parent *viewContext
shared *contextShared
key interface{}
value interface{}
}
// Exposed to templates.
func (v *viewContext) Value(key interface{}) interface{} {
if v.key == key {
return v.value
}
if v.parent == nil {
return nil
}
return v.parent.Value(key)
}
// Exposed to templates.
func (v *viewContext) Obj() interface{} {
return v.Value("object")
}
// Exposed to templates.
func (v *viewContext) Request() *http.Request {
return v.shared.request
}
// Exposed to templates.
func (v *viewContext) With(key string, value interface{}) *viewContext {
return &viewContext{
parent: v,
shared: v.shared,
key: key,
value: value,
}
}
......@@ -22,15 +22,15 @@ func varFromDataProvider(dp DataProvider) func(vctx viewContext, name string) in
// otherwise, capture dp and use it for view value generation.
return func(vctx viewContext, name string) interface{} {
if vctx.varCache != nil {
if val, ok := vctx.varCache[name]; ok {
if vctx.shared.varCache != nil {
if val, ok := vctx.shared.varCache[name]; ok {
return val
}
} else {
vctx.varCache = make(map[string]interface{})
vctx.shared.varCache = make(map[string]interface{})
}
val := dp.ViewValue(vctx.Request, name)
vctx.varCache[name] = val
val := dp.ViewValue(vctx.shared.request, name)
vctx.shared.varCache[name] = val
return val
}
}
......@@ -2,7 +2,7 @@
Package views provides Ghostbin's view model. Views are templates loaded
from a set of globbed files.
The view model provides four predefined template functions, and allows the
The view model provides five predefined template functions, and allows the
binding of additional template functions by way of the
GlobalFunctionProviderOption.
......@@ -13,8 +13,11 @@ GlobalFunctionProviderOption.
Fetches a variable from the local view data provider.
now
Returns the current time.
subexec . <name>
Renders a template and returns its HTML.
subtemplate . <name>
Renders a sibling view and returns its HTML.
Renders a sibling view via subexec. When rendering a page
"x", subtemplate . "y" will render template "x_y".
A view model alone is not useful; it can contain any number of
templates but provides no way to use them. This is where binding comes
......@@ -32,5 +35,55 @@ render the special `tmpl_page` template, passing the bound name along in
{{.page}}. {{.page}} is crucial to the operation of subtemplate; for a
given page "A", {{subtemplate "X"}} will render the template named
"A_X".
At execution time, bound views provide on the {{.}} template argument a
context object with the following methods
.Obj
Returns the object or objects (as a slice) that were provided
to View.Exec().
.Request
Returns the *net/http.Request that triggered the rendering of
this view.
.With <key> <value>
Returns a new context (for the same view, object, and request),
with the provided key mapped to the provided value.
.With is useful only in conjunction with .Value.
.Value <key>
Returns the value mapped to the provided key by way of .With.
USING .With AND .Value
----------------------
Since .With returns a new view context, it can be thought of as akin to
package context's .WithValue. .With attaches a key-value pair to a view
context without mutating the parent context's state. Therefore, only
descendent contexts can access that value.
Its primary use is to pass values across nested template invocations:
{{template "enclose"}}
{{- $t := .Value "subtemplate" -}}
<div id="enclosed_{{$t}}">
{{subexec . $t}}
</div>
{{end}}
{{template "a" -}}
Hello World
{{- end}}
{{template "b"}}
{{with .With "subtemplate" "a" -}}
{{template "enclose" .}}
{{- end}}
{{end}}
The template set above seen through `b' will render
<div id="enclosed_a">
Hello World
</div>
*/
package views
......@@ -33,8 +33,7 @@ type Model struct {
// Bind combines a view model, a view ID, and a data provider into a
// single, durable reference to a template. The supplied data provider
// will be used for all `local` variable lookups for the durartion of the
// View's life.
// will be used for all `local` variable lookups for the View's lifetime.
func (m *Model) Bind(id interface{}, dp DataProvider) (*View, error) {
m.mu.Lock()
defer m.mu.Unlock()
......@@ -106,6 +105,10 @@ func New(glob string, options ...ModelOption) (*Model, error) {
"now": time.Now,
// rebind in subviews.
"subexec": func(args ...interface{}) interface{} {
// subexec is rebound for all bound views.
panic(errors.New("unbound use of subexec"))
},
"subtemplate": func(args ...interface{}) interface{} {
// subtemplate is rebound for all bound views.
panic(errors.New("unbound use of subtemplate"))
......
......@@ -2,6 +2,7 @@ package views
import (
"bytes"
"fmt"
"html/template"
"net/http"
"sync"
......@@ -9,16 +10,6 @@ import (
"github.com/Sirupsen/logrus"
)
// viewContext represents the template context passed to each render.
type viewContext struct {
page string
r *http.Request
// unguarded vis-a-vis concurrency: a single viewContext is not used in
// a multithreaded context.
varCache map[string]interface{}
}
// viewID is the internal interface that allows us to differentiate page-based
// IDs from string-based IDs
type viewID interface {
......@@ -44,7 +35,15 @@ func (p PageID) template() string {
}
func (p PageID) baseContext() *viewContext {
return &viewContext{page: string(p)}
return &viewContext{
shared: &contextShared{
page: string(p),
},
}
}
func (p PageID) String() string {
return "page " + string(p)
}
type stringViewID string
......@@ -54,7 +53,13 @@ func (s stringViewID) template() string {
}
func (s stringViewID) baseContext() *viewContext {
return &viewContext{}
return &viewContext{
shared: &contextShared{},
}
}
func (s stringViewID) String() string {
return string(s)
}
// View represents an ID bound to a data provider and Model. Its behavior
......@@ -71,9 +76,14 @@ type View struct {
tmpl *template.Template
}
func (v *View) subtemplate(vctx *viewContext, name string) template.HTML {
// Exposed to templates.
func (v *View) subexec(vctx *viewContext, name string) template.HTML {
v.mu.RLock()
t := v.tmpl
v.mu.RUnlock()
buf := &bytes.Buffer{}
st := v.tmpl.Lookup(vctx.page + "_" + name)
st := t.Lookup(name)
if st == nil {
// We return an empty snippet here, as a subtemplate failing to exist is non-fatal.
return template.HTML("")
......@@ -83,7 +93,7 @@ func (v *View) subtemplate(vctx *viewContext, name string) template.HTML {
if err != nil {
if v.m.logger != nil {
v.m.logger.WithFields(logrus.Fields{
"page": vctx.page,
"id": v.id,
"subtemplate": name,
"error": err,
}).Error("failed to service subtemplate request")
......@@ -92,6 +102,12 @@ func (v *View) subtemplate(vctx *viewContext, name string) template.HTML {
return template.HTML(buf.String())
}
// Exposed to templates.
func (v *View) subtemplate(vctx *viewContext, name string) template.HTML {
parent := vctx.shared.page
return v.subexec(vctx, fmt.Sprintf("%s_%s", parent, name))
}
func (v *View) rebind(root *template.Template) error {
v.mu.Lock()
defer v.mu.Unlock()
......@@ -103,6 +119,7 @@ func (v *View) rebind(root *template.Template) error {
tmpl.Funcs(template.FuncMap{
"local": varFromDataProvider(v.dp),
"subexec": v.subexec,
"subtemplate": v.subtemplate,
})
v.tmpl = tmpl
......@@ -112,13 +129,18 @@ func (v *View) rebind(root *template.Template) error {
// Exec executes a view given a ResponseWriter and a Request. The Request
// is used as the primary key for every variable lookup during the
// template's execution.
func (v *View) Exec(w http.ResponseWriter, r *http.Request) error {
func (v *View) Exec(w http.ResponseWriter, r *http.Request, params ...interface{}) error {
v.mu.RLock()
t := v.tmpl
v.mu.RUnlock()
vctx := v.id.baseContext()
vctx.r = r
vctx.shared.request = r
if len(params) == 1 {
vctx = vctx.With("object", params[0])
} else if len(params) > 1 {
vctx = vctx.With("object", params)
}
return t.ExecuteTemplate(w, v.id.template(), vctx)
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment