From bf72b863097d5e096b5806a7f4647a01660d17ec Mon Sep 17 00:00:00 2001 From: Ruaidhri Date: Mon, 3 Feb 2020 00:27:32 -0800 Subject: [PATCH 1/7] Improve some documentation of the API --- redirect.go | 11 ++++-- views.go | 96 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/redirect.go b/redirect.go index 94267e8..b86a5e5 100644 --- a/redirect.go +++ b/redirect.go @@ -57,10 +57,15 @@ func SeeOtherPage(w http.ResponseWriter, req *http.Request, location string) boo return true } -// Redirect is a helper that will do a treetop redirect if the request is for a partial -// or fall back to a standard HTTP redirect otherwise using the status code supplied. +// Redirect is a helper that will instruct the Treetop client library to direct the web browser +// to a new URL. If the request is not from a Treetop client, the 3xx redirect method is used. +// +// This is necessary because 3xx HTTP redirects are opaque to XHR, when a full browser redirect +// is needed a 'X-Treetop-See-Other' header is used. +// +// Example: +// treetop.Redirect(w, req, "/some/other/path", http.StatusSeeOther) // -// treetop.Redirect(w, req, "/some/other/path", http.StatusSeeOther) func Redirect(w http.ResponseWriter, req *http.Request, location string, status int) { if ok := SeeOtherPage(w, req, location); !ok { http.Redirect(w, req, location, status) diff --git a/views.go b/views.go index e3cf447..3670635 100644 --- a/views.go +++ b/views.go @@ -1,17 +1,44 @@ package treetop -// Treetop includes this view definition utility which is designed -// for constructing hierarchies of template files paired with handler functions. +// View is a utility for generating request handlers given a definition +// of a template hierarchies. +// +// A view is a pair: a template string (usually file path) and a handler function. +// A view can contain zero or more 'blocks', these are named sub-sections which allow +// other views to be swapped in as needed. +// +// Since multi-page web applications require an endpoint for every action. +// The template inheritance feature supported by the Go standard library is useful +// for reusing HTML template fragments. +// +// Example of a basic template hierarchy +// +// baseHandler(...) +// / base.html ==========\ +// | | +// | / "content" ===\ | +// | | | | +// | | ^ | | +// | \______^_______/ | +// \_________^___________/ +// ^ +// / \ +// contentAHandler(...) __/ \__ contentBHandler(...) +// contentA.html contentB.html +// +// +// Request/response example +// +// GET /path/to/a +// > HTTP/1.1 200 OK +// > ... { base.html + contentA.html } // -// Creating an endpoint for every dynamic part of a page can result in -// a lot more handlers than are typically needed for a serverside web app. +// GET /path/to/b +// > HTTP/1.1 200 OK +// > ... { base.html + contentB.html } // -// The template inheritance feature supported by the Go standard library is an -// ideal way to reuse HTML template fragments using hierarchies. This page view utility -// pairs each template file with a corresponding handler func, so that loading view data can -// be similarly modularized. // -// Example: +// Example of using Treetop View type to generate handers for these endpoints // // base := page.NewView( // treetop.DefaultTemplateExec, @@ -19,20 +46,37 @@ package treetop // baseHandler, // ) // -// // register a block within the base template -// content := base.NewSubView( +// contentA := base.NewSubView( // "content", -// "content.html", -// contentHandler, +// "contentA.html", +// contentAHandler, // ) // -// // Register a http.Handler that is capable of rendering a full document -// // or just the content section -// mymux.Handle("/", treetop.ViewHandler(content)) +// contentB := base.NewSubView( +// "content", +// "contentB.html", +// contentBHandler, +// ) +// +// mymux.Handle("/path/to/a", treetop.ViewHandler(contentA)) +// mymux.Handle("/path/to/b", treetop.ViewHandler(contentB)) +// // +// Notice that each template is paired with it's own handler function. As a result +// request handling can be modularized along with the markup. In the example above, +// the HTTP handlers created by the Treetop library are capable of rendering either +// a full HTML document or the relevant "content" section alone. +// +type View struct { + Template string + Extends *Block + HandlerFunc HandlerFunc + Blocks []*Block + Renderer TemplateExec +} -// NewView create a top level view definition with configuration -// derived from the page instance. +// NewView create a top level view definition which is designed +// for constructing hierarchies of template files paired with handler functions. func NewView(execute TemplateExec, template string, handlerFunc HandlerFunc) *View { return &View{ Template: template, @@ -41,16 +85,6 @@ func NewView(execute TemplateExec, template string, handlerFunc HandlerFunc) *Vi } } -// View is a paring of a template string with a treetop.HandlerFunc -// each view can contain a tree of named subviews -type View struct { - Template string - Extends *Block - HandlerFunc HandlerFunc - Blocks []*Block - Renderer TemplateExec -} - // Block represent a slot that other sub-views can inhabit // within an enclosing treetop.View definition type Block struct { @@ -84,9 +118,9 @@ func (v *View) SubView(blockName, template string, handler HandlerFunc) *View { } // DefaultSubView defines a new view (sub-view) that references it's parent via a -// named block, equivalent to SubView method. The difference is that the parent will also -// have a return reference this the new view, and will use it for the specified block -// when no other 'overriding' view is involved. +// named block. It is equivalent to the SubView method except that the parent will also +// have a return reference. The new view will become the 'default' template +// for the specified block in the parent. func (v *View) DefaultSubView(blockName, template string, handler HandlerFunc) *View { sub := v.SubView(blockName, template, handler) sub.Extends.DefaultPartial = sub From f69b0a414673cbe9384874a03c5245d600f8c767 Mon Sep 17 00:00:00 2001 From: Ruaidhri Date: Fri, 7 Feb 2020 09:41:06 -1000 Subject: [PATCH 2/7] Improve docs for treetop View struct --- views.go | 62 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/views.go b/views.go index 3670635..95761d9 100644 --- a/views.go +++ b/views.go @@ -1,46 +1,47 @@ package treetop -// View is a utility for generating request handlers given a definition -// of a template hierarchies. +// View is a utility for defining hierarchies of nested templates +// from which HTTP request handlers can be generated. // -// A view is a pair: a template string (usually file path) and a handler function. -// A view can contain zero or more 'blocks', these are named sub-sections which allow -// other views to be swapped in as needed. +// Multi-page web applications require a lot of endpoints. Template inheritance +// is commonly used to reduce HTML boilerplate and improve reuse. Treetop views incorporate +// request handlers into the hierarchy to gain the same benefit. // -// Since multi-page web applications require an endpoint for every action. -// The template inheritance feature supported by the Go standard library is useful -// for reusing HTML template fragments. +// A 'View' is a template string (usually file path) paired with a handler function. +// In Go, templates can contain named nested blocks. Defining a 'SubView' associates +// a request handler and a fragment template with a parent. Thus HTTP handlers can +// be easily generated for various page configurations. Within a generated handler, +// data is passed between parent and child in a mechanical way. // // Example of a basic template hierarchy // -// baseHandler(...) -// / base.html ==========\ -// | | -// | / "content" ===\ | -// | | | | -// | | ^ | | -// | \______^_______/ | -// \_________^___________/ -// ^ -// / \ -// contentAHandler(...) __/ \__ contentBHandler(...) -// contentA.html contentB.html -// -// -// Request/response example +// baseHandler(...) +// | base.html ========================| +// | | +// | {{ template "content" .Content }} | +// |_________________^_________________| +// | +// ______/ \______ +// contentAHandler(...) contentBHandler(...) +// | contentA.html ========== | | contentB.html ========== | +// | | | | +// | {{ define "content" }}.. | | {{ define "content" }}.. | +// |__________________________| |__________________________| +// +// Pseudo request and response: // // GET /path/to/a // > HTTP/1.1 200 OK -// > ... { base.html + contentA.html } +// > ... base.html { Content: contentA.html } // // GET /path/to/b // > HTTP/1.1 200 OK -// > ... { base.html + contentB.html } +// > ... base.html { Content: contentB.html } // // -// Example of using Treetop View type to generate handers for these endpoints +// Example of using the library to bind generated handlers to a HTTP router. // -// base := page.NewView( +// base := treetop.NewView( // treetop.DefaultTemplateExec, // "base.html", // baseHandler, @@ -62,10 +63,9 @@ package treetop // mymux.Handle("/path/to/b", treetop.ViewHandler(contentB)) // // -// Notice that each template is paired with it's own handler function. As a result -// request handling can be modularized along with the markup. In the example above, -// the HTTP handlers created by the Treetop library are capable of rendering either -// a full HTML document or the relevant "content" section alone. +// This is useful for creating Treetop enabled endpoints because we wish to be able +// to either load a full page or just a part of a page depending upon the request. +// The generated 'ViewHandler' supports Treetop partials by default. // type View struct { Template string From e39c7d9c04b9f72aabff9a099878e4fb77e13fb3 Mon Sep 17 00:00:00 2001 From: Ruaidhri Date: Sat, 15 Feb 2020 16:19:46 -0800 Subject: [PATCH 3/7] Further package reorg and breaking API changes --- CHANGELOG.md | 3 + README.markdown | 4 +- doc.go | 83 ++++++++++++++++++++++++++ examples/greeter.go | 72 ----------------------- examples/main.go | 25 ++++++++ examples/shared/templates.go | 28 +++++++++ examples/view/greeter.go | 76 ++++++++++++++++++++++++ examples/writer/greeter.go | 84 ++++++++++++++++++++++++++ handler.go | 2 +- handler_test.go | 12 ++-- interfaces.go | 29 --------- response.go | 91 ++++++++++++++++++++--------- response_test.go | 2 +- templateExecutors.go => template.go | 4 ++ helpers.go => viewHelpers.go | 8 +-- views.go | 36 +++++++----- 16 files changed, 400 insertions(+), 159 deletions(-) create mode 100644 doc.go delete mode 100644 examples/greeter.go create mode 100644 examples/main.go create mode 100644 examples/shared/templates.go create mode 100644 examples/view/greeter.go create mode 100644 examples/writer/greeter.go delete mode 100644 interfaces.go rename templateExecutors.go => template.go (93%) rename helpers.go => viewHelpers.go (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a0fd9..ab6088b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ execute on a wishlist of API changes. - Remove `treetop.Test`, testing recipes and resources belong elsewhere - Split `treetop.Writer` function into `treetop.NewPartialWriter` and `treetop.NewFragmentWriter` and remove the confusing `isPartial` flag - Change `treetop.View` from an interface to a struct and expose internals to make debugging easier +- Rename `HandlePartial` method to `HandleSubView` in `treetop.Response` interface to be consistent with the view builder +- Rename `Done` method to `Finished` in `treetop.Response` interface +- Rename `HandlerFunc` signature type to `ViewHandlerFunc` to make a clearer association with the view builder #### Defining a page with views diff --git a/README.markdown b/README.markdown index ab5710d..234f7f0 100644 --- a/README.markdown +++ b/README.markdown @@ -130,7 +130,7 @@ Each template filepath is paired with a data handler which is responsible for yi Content interface{} }{ Session: Session{"example.user"}, - Content: rsp.HandlePartial("content", req), + Content: rsp.HandleSubView("content", req), } } @@ -141,7 +141,7 @@ Each template filepath is paired with a data handler which is responsible for yi Form interface{} }{ Title: "My Contact Form", - Form: rsp.HandlePartial("form", req), + Form: rsp.HandleSubView("form", req), } } diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..bf6544d --- /dev/null +++ b/doc.go @@ -0,0 +1,83 @@ +/* +Package treetop includes tools for incorporating HTML fragment requests into Go +web applications. + +Fragments requests are specified by a protocol with the goal removing some of the common +causes of JavaScript boilerplate in modern web applications. Reliance on data APIs can also be greatly reduced. + +For documentation and examples see https://github.com/rur/treetop and https://github.com/rur/treetop-recipes + +Introduction + +In the spirit of opt-in integration, this package supports two use cases. +Writers are handler scoped helpers that are useful for incorporating +ad-hoc fragments with an existing application. The view builder abstraction +is designed for constructing an integrated UI with many cooperating endpoints. + +Example of ad-hoc partial writer + + import ( + "fmt" + "github.com/rur/treetop" + "net/http" + ) + ... + func MyHandlerFunc(w http.ResponseWriter, req *http.Request) { + if pw, ok := treetop.NewPartialWriter(w, req); ok { + fmt.Fprint(pw, `

Hello Treetop!

`) + return + } + // otherwise render a full page containing an element with id="greeting" + ... + } + +For this example the full page document will include the standard Treetop client library, an element +with the "greeting" ID attribute value and an anchor element that has a 'treetop' attribute. + +HTML document for the example, + + + + + My Page + + + +

Message

+

Hello Page!

+ +
Fetch greeting
+ + + + + + +When the 'Fetch greeting' anchor is clicked, the client library will issue an XHR fetch +with the appropriate headers. Elements on the DOM will be replaced with fragments from +the response body based upon their ID attribute. + + +Views and Templates + +More realistic web applications will take advantage of the HTML template support +in the Go standard library. Treetop includes a View builder to makes it more convenient +to define handlers that support partials and fragments. + +See the documentation of the View type for details. + +Browser History - Partials vs Fragments + +This can cause confussion, but it is a very useful distinction. A partial is a +_part_ of an HTML document, a fragment is a general purpose HTML +snippet. Both have a URL, but only the former should be considered 'navigation' +by the user agent. This allows browser history to be handled correctly so that the back, +forward and refresh behavior work as expected. + +You should note that the client relies upon the HTML 5 history API to support +Treetop partials. + +*/ +package treetop diff --git a/examples/greeter.go b/examples/greeter.go deleted file mode 100644 index 77e3a02..0000000 --- a/examples/greeter.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - - "github.com/rur/treetop" -) - -var ( - base = ` - - - - - Treetop Greeter - - -

Treetop Greeter

-
-
- Greet, -
-
-{{ block "message" .Message}}{{ end }} - - - - - ` - landing = ` -{{ block "message" .}} -

Give me someone to say hello to!

-{{ end }} - ` - greeting = ` -{{ block "message" .}} -
-

Hello, {{ . }}!

-

Clear

-
-{{ end }} - ` -) - -func main() { - page := treetop.NewView( - treetop.StringTemplateExec, - base, - baseHandler, - ) - greetForm := page.SubView("message", landing, treetop.Noop) - greetMessage := page.SubView("message", greeting, greetingHandler) - - http.Handle("/", treetop.ViewHandler(greetForm)) - http.Handle("/greet", treetop.ViewHandler(greetMessage)) - fmt.Println("serving on http://0.0.0.0:3000/") - log.Fatal(http.ListenAndServe(":3000", nil)) -} - -func baseHandler(rsp treetop.Response, req *http.Request) interface{} { - return struct { - Message interface{} - }{ - Message: rsp.HandlePartial("message", req), - } -} - -func greetingHandler(_ treetop.Response, req *http.Request) interface{} { - return req.URL.Query().Get("name") -} diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..60344ad --- /dev/null +++ b/examples/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/rur/treetop" + "github.com/rur/treetop/examples/shared" + + "github.com/rur/treetop/examples/view" + "github.com/rur/treetop/examples/writer" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/favicon.ico", func(_ http.ResponseWriter, _ *http.Request) { /* noop */ }) + mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { + treetop.StringTemplateExec(w, []string{shared.BaseTemplate}, nil) + }) + view.SetupGreeter(mux) + writer.SetupGreeter(mux) + fmt.Println("serving on http://0.0.0.0:3000/") + log.Fatal(http.ListenAndServe(":3000", mux)) +} diff --git a/examples/shared/templates.go b/examples/shared/templates.go new file mode 100644 index 0000000..d92b5de --- /dev/null +++ b/examples/shared/templates.go @@ -0,0 +1,28 @@ +package shared + +// BaseTemplate is the document markup shared by all of the example applications +var BaseTemplate = ` + + + + + Treetop Examples + + + + + + {{ block "content" . }} +

↑ Choose a demo ↑

+ {{ end }} + + + + + + ` diff --git a/examples/view/greeter.go b/examples/view/greeter.go new file mode 100644 index 0000000..0fd0065 --- /dev/null +++ b/examples/view/greeter.go @@ -0,0 +1,76 @@ +package view + +import ( + "net/http" + + "github.com/rur/treetop" + "github.com/rur/treetop/examples/shared" +) + +var ( + content = ` +{{ block "content" . }} +
+

Treetop View Greeter

+
+
+ Greet, +
+
+ {{ template "message" .Message}} +
+{{ end }} + ` + landing = ` +{{ block "message" . }} +

Give me someone to say hello to!

+{{ end }} + ` + greeting = ` +{{ block "message" . }} +
+

Hello, {{ . }}!

+

Clear

+
+{{ end }} + ` +) + +// SetupGreeter add routes for /view example endpoint +func SetupGreeter(mux *http.ServeMux) { + page := treetop.NewView( + treetop.StringTemplateExec, + shared.BaseTemplate, + treetop.Delegate("content"), // adopt "content" handler as own template data + ) + content := page.SubView("content", content, contentHandler) + + greetForm := content.SubView("message", landing, treetop.Noop) + greetMessage := content.SubView("message", greeting, greetingHandler) + + mux.Handle("/view", treetop.ViewHandler(greetForm)) + mux.Handle("/view/greet", treetop.ViewHandler(greetMessage)) +} + +// contentHandler loads data for the content template +func contentHandler(rsp treetop.Response, req *http.Request) interface{} { + return struct { + // In the content template the .Message field is passed explicitly to the + // "message" template block. + Message interface{} + }{ + // HandleSubView triggers the sub handler call and returns the sub template data. + // `who` in the case of the greetingHandler, or nil in the case of Noop + Message: rsp.HandleSubView("message", req), + } +} + +// greetingHandler obtains the name for the greeting from the request query +func greetingHandler(_ treetop.Response, req *http.Request) interface{} { + who := req.URL.Query().Get("name") + if who == "" { + return "World" + } + // NOTE: relying on Go package html/template for input escaping + return who +} diff --git a/examples/writer/greeter.go b/examples/writer/greeter.go new file mode 100644 index 0000000..1ebbd9d --- /dev/null +++ b/examples/writer/greeter.go @@ -0,0 +1,84 @@ +package writer + +import ( + "net/http" + + "github.com/rur/treetop" + "github.com/rur/treetop/examples/shared" +) + +var ( + content = ` +{{ block "content" . }} +
+

Treetop Writer Greeter

+
+
+ Greet, +
+
+ {{ template "message" .Message}} +
+{{ end }} + ` + landing = ` +{{ block "message" . }} +

Give me someone to say hello to!

+{{ end }} + ` + greeting = ` +{{ block "message" . }} +
+

Hello, {{ . }}!

+

Clear

+
+{{ end }} + ` +) + +// SetupGreeter registers writer greeter endpoints +func SetupGreeter(mux *http.ServeMux) { + mux.HandleFunc("/writer/greet", greetingHandler) + mux.HandleFunc("/writer", landingHandler) +} + +func landingHandler(w http.ResponseWriter, req *http.Request) { + var err error + defer func() { + if err != nil { + panic(err) + } + }() + if pw, ok := treetop.NewPartialWriter(w, req); ok { + err = treetop.StringTemplateExec(pw, []string{landing}, nil) + return + } + err = treetop.StringTemplateExec(w, []string{ + shared.BaseTemplate, + content, + landing, + }, nil) +} + +func greetingHandler(w http.ResponseWriter, req *http.Request) { + var err error + defer func() { + if err != nil { + panic(err) + } + }() + who := req.URL.Query().Get("name") + if who == "" { + who = "World" + } + if pw, ok := treetop.NewPartialWriter(w, req); ok { + err = treetop.StringTemplateExec(pw, []string{greeting}, who) + return + } + // return full page instead + err = treetop.StringTemplateExec(w, []string{ + shared.BaseTemplate, + content, + greeting, + }, struct{ Message string }{who}) +} diff --git a/handler.go b/handler.go index 7bfb0f0..ffb80a4 100644 --- a/handler.go +++ b/handler.go @@ -59,7 +59,7 @@ func ViewHandler(view *View, includes ...*View) *Handler { type Partial struct { Extends string Template string - HandlerFunc HandlerFunc + HandlerFunc ViewHandlerFunc Blocks []Partial } diff --git a/handler_test.go b/handler_test.go index c96c234..717bd96 100644 --- a/handler_test.go +++ b/handler_test.go @@ -31,7 +31,7 @@ func TestHandler_ServeHTTP(t *testing.T) { Extends: "root", Template: "test.html.tmpl", HandlerFunc: func(rsp Response, req *http.Request) interface{} { - d := rsp.HandlePartial("testblock", req) + d := rsp.HandleSubView("testblock", req) return fmt.Sprintf("Loaded sub data: %s", d) }, Blocks: []Partial{ @@ -87,7 +87,7 @@ func TestHandler_ServeHTTP(t *testing.T) { Fragment: &Partial{ Template: "test.html.tmpl", HandlerFunc: func(rsp Response, req *http.Request) interface{} { - d := rsp.HandlePartial("testblock", req) + d := rsp.HandleSubView("testblock", req) return fmt.Sprintf("Loaded sub data: %s", d) }, Blocks: []Partial{ @@ -240,14 +240,14 @@ func captureOutput(f func()) string { return buf.String() } -func blockDebug(blocknames []string) HandlerFunc { +func blockDebug(blocknames []string) ViewHandlerFunc { return func(rsp Response, req *http.Request) interface{} { var d []struct { Block string Data interface{} } for _, n := range blocknames { - data := rsp.HandlePartial(n, req) + data := rsp.HandleSubView(n, req) if data != nil { d = append( d, @@ -280,7 +280,7 @@ func TestPartial_TemplateList(t *testing.T) { type fields struct { Extends string Template string - HandlerFunc HandlerFunc + HandlerFunc ViewHandlerFunc Root *Partial Blocks []Partial } @@ -390,7 +390,7 @@ func Test_insertPartial(t *testing.T) { type fields struct { Extends string Template string - HandlerFunc HandlerFunc + HandlerFunc ViewHandlerFunc Blocks []Partial } type args struct { diff --git a/interfaces.go b/interfaces.go deleted file mode 100644 index 6ad3557..0000000 --- a/interfaces.go +++ /dev/null @@ -1,29 +0,0 @@ -package treetop - -import ( - "context" - "io" - "net/http" -) - -// Response is the HandlerFunc facade for the treetop request response process. -// It can be used to delegate handling to block and to track the resolution of a -// single request across multiple handlers. -type Response interface { - http.ResponseWriter - Status(int) int - DesignatePageURL(string) - ReplacePageURL(string) - Done() bool - HandlePartial(string, *http.Request) interface{} - ResponseID() uint32 - Context() context.Context -} - -// TemplateExec interface is a signature of a function which can be configured to -// execute the templates with supplied data. -type TemplateExec func(io.Writer, []string, interface{}) error - -// HandlerFunc is the interface for treetop handler functions that support hierarchical -// partial data loading. -type HandlerFunc func(Response, *http.Request) interface{} diff --git a/response.go b/response.go index f4f8963..99fc3e1 100644 --- a/response.go +++ b/response.go @@ -1,7 +1,3 @@ -// Hierarchical view handlers control the treetop response lifecycle through -// the treetop.ResponseWriter interface, which is a superset of the html.ResponseWriter -// interface. - package treetop import ( @@ -10,6 +6,64 @@ import ( "net/http" ) +// Response extends the http.ResponseWriter interface to give ViewHandelersFunc's limited +// ability to control the hierarchical request handing. +// +// Note that writing directly to the underlying ResponseWriter in the handler will cancel the +// treetop handling process. Taking control of response writing in this way is a very common and +// useful practice especially for error messages or redirects. +type Response interface { + http.ResponseWriter + + // Status allows a handler to indicate (not determine) what the HTTP status + // should be for the response. + // + // When different handlers indicate a different status, + // the code with the greater numeric value is chosen. + // + // For example, given: Bad Request, Unauthorized and Internal Server Error. + // Status values are differentiated as follows, 400 < 401 < 500, + // 'Internal Server Error' is chosen for the response header. + // + // The resulting response status is returned. Getting the current status + // without affecting the response can be done as follows + // + // status := rsp.Status(0) + // + Status(int) int + + // DesignatePageURL forces the response to be handled as a navigation event with a specified URL. + // The browser will have a new history entry created for the supplied URL. + DesignatePageURL(string) + + // ReplacePageURL forces the location bar in the web browser to be updated with the supplied + // URL. This should be done by *replacing* the existing history entry. (not adding a new one) + ReplacePageURL(string) + + // Finished will return true if a handler has taken direct responsibility for writing the + // response. + Finished() bool + + // HandleSubView loads data from a named child subview handler. If no handler is availabe for the name, + // nil will be returned. + // + // NOTE: Since a sub handler may have returned nil, there is no way for the parent handler to determine + // whether the name resolved to a concrete view. + HandleSubView(string, *http.Request) interface{} + + // ResponseID returns the ID treetop has associated with this request. + // Since multiple handlers may be involved, the ID is useful for logging and caching. + // + // Response IDs avoid potential pitfalls around Request instance comparison that can affect middleware. + // + // NOTE: This is *not* a UUID, response IDs are incremented from zero when the server is started + ResponseID() uint32 + + // Context returns the context associated with the treetop process. + // This is a child of the http Request context. + Context() context.Context +} + // responseImpl is the API that treetop request handlers interact with // through the treetop.ResponseWriter interface. type responseImpl struct { @@ -24,30 +78,18 @@ type responseImpl struct { replaceURL bool } -// Implement http.ResponseWriter interface by delegating to embedded instance -func (rsp *responseImpl) Header() http.Header { - return rsp.ResponseWriter.Header() -} - -// Implement http.ResponseWriter interface by delegating to embedded instance +// Write delegates to the underlying ResponseWriter while setting finished flag to true func (rsp *responseImpl) Write(b []byte) (int, error) { rsp.finished = true return rsp.ResponseWriter.Write(b) } -// Implement http.ResponseWriter interface by delegating to embedded instance +// WriteHeader delegates to the underlying ResponseWriter while setting finished flag to true func (rsp *responseImpl) WriteHeader(statusCode int) { rsp.finished = true rsp.ResponseWriter.WriteHeader(statusCode) } -// Indicate what the HTTP status should be for the response. -// -// Note that when different handlers indicate a different status -// the code with the greater numeric value is chosen. -// -// For example, given: Bad Request, Unauthorized and Internal Server Error. -// Status values are differentiated as follows, 400 < 401 < 500, so 'Internal Server Error' wins! func (rsp *responseImpl) Status(status int) int { if status > rsp.status { rsp.status = status @@ -55,32 +97,23 @@ func (rsp *responseImpl) Status(status int) int { return rsp.status } -// ReplacePageURL forces the location bar in the web browser to be updated with the supplied -// URL. This should be done by *replacing* the existing history entry. (not adding a new one) func (rsp *responseImpl) ReplacePageURL(url string) { rsp.pageURL = url rsp.replaceURL = true rsp.pageURLSpecified = true } -// DesignatePageURL forces the response to be handled as a full or part of a full page. -// the web browser will have a new history entry for the supplied URL. func (rsp *responseImpl) DesignatePageURL(url string) { rsp.pageURL = url rsp.replaceURL = false rsp.pageURLSpecified = true } -// Done allows a handler to check if the request has already been satisfied. -func (rsp *responseImpl) Done() bool { +func (rsp *responseImpl) Finished() bool { return rsp.finished } -// Load data from a named child block handler. -// -// The second return value indicates whether the delegated handler called Data(...) -// or not. This is necessary to discern the meaning of a `nil` data value. -func (rsp *responseImpl) HandlePartial(name string, req *http.Request) interface{} { +func (rsp *responseImpl) HandleSubView(name string, req *http.Request) interface{} { // don't do anything if a response has already been written if rsp.finished { return nil diff --git a/response_test.go b/response_test.go index 8b42e73..871e9de 100644 --- a/response_test.go +++ b/response_test.go @@ -140,7 +140,7 @@ func Test_responseImpl_PartialHandler(t *testing.T) { status: tt.fields.status, partial: &tt.fields.partial, } - got := rsp.HandlePartial(tt.args.name, tt.args.req) + got := rsp.HandleSubView(tt.args.name, tt.args.req) if !reflect.DeepEqual(got, tt.data) { t.Errorf("responseImpl.PartialHandler() got = %v, want %v", got, tt.data) } diff --git a/templateExecutors.go b/template.go similarity index 93% rename from templateExecutors.go rename to template.go index b095286..6e1c4ec 100644 --- a/templateExecutors.go +++ b/template.go @@ -9,6 +9,10 @@ import ( "path/filepath" ) +// TemplateExec interface is a signature of a function which can be configured to +// execute the templates with supplied data. +type TemplateExec func(io.Writer, []string, interface{}) error + // DefaultTemplateExec implements TemplateExec as a thin wrapper around the // ParseFiles method in html/template package func DefaultTemplateExec(w io.Writer, templates []string, data interface{}) error { diff --git a/helpers.go b/viewHelpers.go similarity index 87% rename from helpers.go rename to viewHelpers.go index 84fdb06..d6beb7a 100644 --- a/helpers.go +++ b/viewHelpers.go @@ -11,7 +11,7 @@ func Noop(_ Response, _ *http.Request) interface{} { return nil } // Constant treetop handler helper is used to generate a treetop.HandlerFunc that always // returns the same value. -func Constant(data interface{}) HandlerFunc { +func Constant(data interface{}) ViewHandlerFunc { return func(rsp Response, _ *http.Request) interface{} { return data } @@ -20,15 +20,15 @@ func Constant(data interface{}) HandlerFunc { // Delegate handler helper will delegate partial handling to a named block of that // partial. The designated block data will be adopted as the partial template data // and no other block handler will be executed. -func Delegate(blockname string) HandlerFunc { +func Delegate(blockname string) ViewHandlerFunc { return func(rsp Response, req *http.Request) interface{} { - return rsp.HandlePartial(blockname, req) + return rsp.HandleSubView(blockname, req) } } // RequestHandler handler helper is used where only the http.Request instance is needed // to resolve the template data so the treetop.Response isnt part of the actual handler function. -func RequestHandler(f func(*http.Request) interface{}) HandlerFunc { +func RequestHandler(f func(*http.Request) interface{}) ViewHandlerFunc { return func(_ Response, req *http.Request) interface{} { return f(req) } diff --git a/views.go b/views.go index 95761d9..77a0fee 100644 --- a/views.go +++ b/views.go @@ -1,31 +1,34 @@ package treetop -// View is a utility for defining hierarchies of nested templates +import "net/http" + +// View is a utility for building hierarchies of nested templates // from which HTTP request handlers can be generated. // // Multi-page web applications require a lot of endpoints. Template inheritance // is commonly used to reduce HTML boilerplate and improve reuse. Treetop views incorporate -// request handlers into the hierarchy to gain the same benefit. +// nested request handlers into the hierarchy to gain the same advantage. // // A 'View' is a template string (usually file path) paired with a handler function. // In Go, templates can contain named nested blocks. Defining a 'SubView' associates -// a request handler and a fragment template with a parent. Thus HTTP handlers can -// be easily generated for various page configurations. Within a generated handler, -// data is passed between parent and child in a mechanical way. +// a handler function and a fragment template with a parent. Thus HTTP handlers can +// be generated for various page configurations. Within the generated handler, +// parent and child views are combined in a mechanical way. // // Example of a basic template hierarchy // // baseHandler(...) // | base.html ========================| -// | | +// | … | // | {{ template "content" .Content }} | -// |_________________^_________________| +// | … ^ | +// |_________________|_________________| // | // ______/ \______ // contentAHandler(...) contentBHandler(...) // | contentA.html ========== | | contentB.html ========== | // | | | | -// | {{ define "content" }}.. | | {{ define "content" }}.. | +// | {{ block "content" . }}… | | {{ block "content" . }}… | // |__________________________| |__________________________| // // Pseudo request and response: @@ -63,21 +66,24 @@ package treetop // mymux.Handle("/path/to/b", treetop.ViewHandler(contentB)) // // -// This is useful for creating Treetop enabled endpoints because we wish to be able -// to either load a full page or just a part of a page depending upon the request. -// The generated 'ViewHandler' supports Treetop partials by default. +// This is useful for creating Treetop enabled endpoints because the generated handler +// is capable of loading either a full page or just a part of a page depending upon the request. // type View struct { Template string Extends *Block - HandlerFunc HandlerFunc + HandlerFunc ViewHandlerFunc Blocks []*Block Renderer TemplateExec } +// ViewHandlerFunc is the interface for treetop handler functions that support hierarchical +// partial data loading. +type ViewHandlerFunc func(Response, *http.Request) interface{} + // NewView create a top level view definition which is designed // for constructing hierarchies of template files paired with handler functions. -func NewView(execute TemplateExec, template string, handlerFunc HandlerFunc) *View { +func NewView(execute TemplateExec, template string, handlerFunc ViewHandlerFunc) *View { return &View{ Template: template, HandlerFunc: handlerFunc, @@ -95,7 +101,7 @@ type Block struct { // SubView defines a new view (sub-view) that references to its parent via a // named block. -func (v *View) SubView(blockName, template string, handler HandlerFunc) *View { +func (v *View) SubView(blockName, template string, handler ViewHandlerFunc) *View { var block *Block for i := 0; i < len(v.Blocks); i++ { if v.Blocks[i].Name == blockName { @@ -121,7 +127,7 @@ func (v *View) SubView(blockName, template string, handler HandlerFunc) *View { // named block. It is equivalent to the SubView method except that the parent will also // have a return reference. The new view will become the 'default' template // for the specified block in the parent. -func (v *View) DefaultSubView(blockName, template string, handler HandlerFunc) *View { +func (v *View) DefaultSubView(blockName, template string, handler ViewHandlerFunc) *View { sub := v.SubView(blockName, template, handler) sub.Extends.DefaultPartial = sub return sub From 403a5273bc3b853d73bc85bfd6c3af5278579af3 Mon Sep 17 00:00:00 2001 From: Ruaidhri Date: Sat, 15 Feb 2020 16:20:53 -0800 Subject: [PATCH 4/7] Bugfix: Writer should set full response URL header --- writer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/writer.go b/writer.go index df03982..e20f2d1 100644 --- a/writer.go +++ b/writer.go @@ -56,7 +56,7 @@ func (tw *writer) Write(p []byte) (n int, err error) { return n, err } if !tw.written { - tw.responseWriter.Header().Set("X-Response-Url", respURI.EscapedPath()) + tw.responseWriter.Header().Set("X-Response-Url", respURI.String()) if tw.replaceURLState { tw.responseWriter.Header().Set("X-Response-History", "replace") } From 90992c26a0225e5b455e20ccf3031fac8d3b43c1 Mon Sep 17 00:00:00 2001 From: Ruaidhri Date: Sun, 16 Feb 2020 17:49:12 -0800 Subject: [PATCH 5/7] Update changelog before tagging v0.2.0 --- CHANGELOG.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6088b..fdf2d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ -## [0.2.0] - 2020-01-26 +## [0.2.0] - 2020-02-16 -### Breaking API Changes +Add clarifications to the prototype library API and improve code docs. + +### Bugfix -Taking Treetop from POC to Alpha gives me an opportunity to -execute on a wishlist of API changes. +- Writer implementation was not including query in response URL header + +### Breaking API Changes - Remove `treetop.Renderer` type, unnecessary since it only wraps `TemplateExec` - Add TemplateExec parameter to `treetop.NewView..` API method for creating base views From 52d413f3da61fa9848f5bfafb9f925534aec7ccd Mon Sep 17 00:00:00 2001 From: Ruaidhri Date: Sun, 16 Feb 2020 19:22:21 -0800 Subject: [PATCH 6/7] More improvements to code docs --- doc.go | 55 ++++++++++++++++++++++++++++++++++++------------------- views.go | 20 ++++++++++---------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/doc.go b/doc.go index bf6544d..53eacec 100644 --- a/doc.go +++ b/doc.go @@ -1,18 +1,19 @@ /* -Package treetop includes tools for incorporating HTML fragment requests into Go +Package treetop is a library for incorporating HTML fragment requests into Go web applications. -Fragments requests are specified by a protocol with the goal removing some of the common +Fragments requests are specified by a protocol with the goal of removing some of the common causes of JavaScript boilerplate in modern web applications. Reliance on data APIs can also be greatly reduced. For documentation and examples see https://github.com/rur/treetop and https://github.com/rur/treetop-recipes Introduction -In the spirit of opt-in integration, this package supports two use cases. -Writers are handler scoped helpers that are useful for incorporating -ad-hoc fragments with an existing application. The view builder abstraction -is designed for constructing an integrated UI with many cooperating endpoints. +In the spirit of opt-in integration this package supports two use cases. +The first is the handler scoped 'Writer' which is useful for supporting +ad-hoc fragments in an existing application. The view builder abstraction is +the second, it is designed for constructing a UI with many +cooperating endpoints. Example of ad-hoc partial writer @@ -27,14 +28,14 @@ Example of ad-hoc partial writer fmt.Fprint(pw, `

Hello Treetop!

`) return } - // otherwise render a full page containing an element with id="greeting" + // otherwise render a full page ... } -For this example the full page document will include the standard Treetop client library, an element -with the "greeting" ID attribute value and an anchor element that has a 'treetop' attribute. +For the example, the 'full page' document will include: the Treetop client library, an element +with an ID of "greeting" and an anchor element with a 'treetop' attribute. -HTML document for the example, +Example HTML document @@ -44,27 +45,44 @@ HTML document for the example,

Message

+

Hello Page!

When the 'Fetch greeting' anchor is clicked, the client library will issue an XHR fetch -with the appropriate headers. Elements on the DOM will be replaced with fragments from +with the appropriate headers. Elements on the DOM will then be replaced with fragments from the response body based upon their ID attribute. - Views and Templates -More realistic web applications will take advantage of the HTML template support -in the Go standard library. Treetop includes a View builder to makes it more convenient -to define handlers that support partials and fragments. +Many Go web applications take advantage of the HTML template support +in the Go standard library. Package treetop includes a view builder +that works with the template system to support pages, partials and fragments. + +Example + + base := treetop.NewView( + treetop.DefaultTemplateExec, + "base.html.tmpl", + baseHandler, + ) + + greeting := base.NewSubView( + "greeting", + "greeting.html.tmpl", + greetingHandler, + ) + + mymux.Handle("/", treetop.ViewHandler(greeting)) See the documentation of the View type for details. @@ -73,11 +91,10 @@ Browser History - Partials vs Fragments This can cause confussion, but it is a very useful distinction. A partial is a _part_ of an HTML document, a fragment is a general purpose HTML snippet. Both have a URL, but only the former should be considered 'navigation' -by the user agent. This allows browser history to be handled correctly so that the back, +by the user agent. This allows browser history to be handled correctly so that back, forward and refresh behavior work as expected. -You should note that the client relies upon the HTML 5 history API to support -Treetop partials. +Note: The client relies upon the HTML 5 history API to support Treetop partials. */ package treetop diff --git a/views.go b/views.go index 77a0fee..49f48ca 100644 --- a/views.go +++ b/views.go @@ -3,17 +3,16 @@ package treetop import "net/http" // View is a utility for building hierarchies of nested templates -// from which HTTP request handlers can be generated. +// from which HTTP request handlers can be constructed. // -// Multi-page web applications require a lot of endpoints. Template inheritance +// Multi-page web apps require a lot of endpoints. Template inheritance // is commonly used to reduce HTML boilerplate and improve reuse. Treetop views incorporate -// nested request handlers into the hierarchy to gain the same advantage. +// request handlers into the hierarchy to gain the same advantage. // // A 'View' is a template string (usually file path) paired with a handler function. -// In Go, templates can contain named nested blocks. Defining a 'SubView' associates -// a handler function and a fragment template with a parent. Thus HTTP handlers can -// be generated for various page configurations. Within the generated handler, -// parent and child views are combined in a mechanical way. +// Go templates can contain named nested blocks. Defining a 'SubView' associates +// a handler and a template with a block embedded within a parent template. +// HTTP handlers can then be constructed for various page configurations. // // Example of a basic template hierarchy // @@ -42,7 +41,7 @@ import "net/http" // > ... base.html { Content: contentB.html } // // -// Example of using the library to bind generated handlers to a HTTP router. +// Example of using the library to bind constructed handlers to a HTTP router. // // base := treetop.NewView( // treetop.DefaultTemplateExec, @@ -66,8 +65,9 @@ import "net/http" // mymux.Handle("/path/to/b", treetop.ViewHandler(contentB)) // // -// This is useful for creating Treetop enabled endpoints because the generated handler -// is capable of loading either a full page or just a part of a page depending upon the request. +// This is useful for creating Treetop enabled endpoints because the constructed handler +// is capable of loading either a full page or just the "content" part of the page depending +// upon the request. // type View struct { Template string From 56e53a038fe03d128d5992a58c0173e31379f43b Mon Sep 17 00:00:00 2001 From: Ruaidhri Devery Date: Sun, 23 Feb 2020 16:00:51 -0800 Subject: [PATCH 7/7] Improve the package introduction docs --- doc.go | 15 +++++++++++---- response.go | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/doc.go b/doc.go index 53eacec..e087f3e 100644 --- a/doc.go +++ b/doc.go @@ -1,9 +1,16 @@ /* -Package treetop is a library for incorporating HTML fragment requests into Go -web applications. +Package treetop provides tools for handlers to support a HTML template request protocol . -Fragments requests are specified by a protocol with the goal of removing some of the common -causes of JavaScript boilerplate in modern web applications. Reliance on data APIs can also be greatly reduced. +So your webpage does IO; and you need to show updates without clobbering the interface. +The common approach is to expose a data API and dispatch JavaScript to micromanage the client. +That will work, but it is pretty heavy duty for what seems like a simple problem. + +Conventional HTTP works very well for navigation and web forms alike, no micromanagement required. +Perhaps it could be extended to solve our dynamic update problem. That is the starting point for Treetop, +to see how far we can get with a simple protocol. + +Treetop is unique because it puts the server-side hander in complete control of how the page will be updated +following a request. For documentation and examples see https://github.com/rur/treetop and https://github.com/rur/treetop-recipes diff --git a/response.go b/response.go index 99fc3e1..204d8da 100644 --- a/response.go +++ b/response.go @@ -7,7 +7,7 @@ import ( ) // Response extends the http.ResponseWriter interface to give ViewHandelersFunc's limited -// ability to control the hierarchical request handing. +// ability to control the hierarchical request handling. // // Note that writing directly to the underlying ResponseWriter in the handler will cancel the // treetop handling process. Taking control of response writing in this way is a very common and