Ability to register a view engine per group of routes or for the current a chain of handlers

Example at: https://github.com/kataras/iris/tree/master/_examples/view/context-view-engine
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-08-05 19:34:55 +03:00
parent b363492cca
commit 5d480dc801
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
22 changed files with 282 additions and 66 deletions

View File

@ -359,6 +359,8 @@ Response:
Other Improvements: Other Improvements:
- Ability to register a view engine per group of routes or for the current chain of handlers through `Party.RegisterView` and `Context.ViewEngine` respectfully.
- Add [Blocks](_examples/view/template_blocks_0) template engine. <!-- Reminder for @kataras: follow https://github.com/flosch/pongo2/pull/236#issuecomment-668950566 discussion so we can get back on using the original pongo2 repository as they fixed the issue about an incompatible 3rd party package (although they need more fixes, that's why I commented there) --> - Add [Blocks](_examples/view/template_blocks_0) template engine. <!-- Reminder for @kataras: follow https://github.com/flosch/pongo2/pull/236#issuecomment-668950566 discussion so we can get back on using the original pongo2 repository as they fixed the issue about an incompatible 3rd party package (although they need more fixes, that's why I commented there) -->
- Add [Ace](_examples/view/template_ace_0) template parser to the view engine and other minor improvements. - Add [Ace](_examples/view/template_ace_0) template parser to the view engine and other minor improvements.
@ -492,8 +494,9 @@ New Package-level Variables:
New Context Methods: New Context Methods:
- `Context.SetErr(error)` and `Context.GetErr() error` helpers - `Context.ViewEngine(ViewEngine)` to set a view engine on-fly for the current chain of handlers, responsible to render templates through `ctx.View`. [Example](_examples/view/context-view-engine).
- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error` - `Context.SetErr(error)` and `Context.GetErr() error` helpers.
- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`.
- `Context.Clone() Context` returns a copy of the Context. - `Context.Clone() Context` returns a copy of the Context.
- `Context.IsCanceled() bool` reports whether the request has been canceled by the client. - `Context.IsCanceled() bool` reports whether the request has been canceled by the client.
- `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too). - `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too).

View File

@ -0,0 +1,65 @@
package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
// Register a root view engine, as usual,
// will be used to render files through Context.View method
// when no Party or Handler-specific view engine is available.
app.RegisterView(iris.Blocks("./views/public", ".html"))
// http://localhost:8080
app.Get("/", index)
// Register a view engine per group of routes.
adminGroup := app.Party("/admin")
adminGroup.RegisterView(iris.Blocks("./views/admin", ".html"))
// http://localhost:8080/admin
adminGroup.Get("/", admin)
// Register a view engine on-fly for the current chain of handlers.
views := iris.Blocks("./views/on-fly", ".html")
if err := views.Load(); err != nil {
app.Logger().Fatal(err)
}
// http://localhost:8080/on-fly
app.Get("/on-fly", setViews(views), onFly)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Public Index Title",
}
ctx.ViewLayout("main")
ctx.View("index", data)
}
func admin(ctx iris.Context) {
data := iris.Map{
"Title": "Admin Panel",
}
ctx.ViewLayout("main")
ctx.View("index", data)
}
func setViews(views iris.ViewEngine) iris.Handler {
return func(ctx iris.Context) {
ctx.ViewEngine(views)
ctx.Next()
}
}
func onFly(ctx iris.Context) {
data := iris.Map{
"Message": "View engine changed through 'setViews' custom middleware.",
}
ctx.View("index", data)
}

View File

@ -0,0 +1,3 @@
{{ define "content" }}
<h1>Hello, Admin!</h1>
{{ end }}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>
</head>
<body>
{{ template "content" .}}
<h4>Copyright &copy; 2020 Admin</h4>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h1>On-fly</h1>
<h3>{{.Message}}</h3>

View File

@ -0,0 +1,12 @@
<!-- You can define more than one block.
The default one is "content" which should be the main template's body.
So, even if it's missing (see index.html), it's added automatically by the view engine.
When you need to define more than one block, you have to be more specific:
-->
{{ define "content" }}
<h1>Internal Server Error</h1>
{{ end }}
{{ define "message" }}
<p style="color:red;">{{.Message}}</p>
{{ end }}

View File

@ -0,0 +1 @@
<h1>Index Body</h1>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Code}}</title>
</head>
<body>
{{ template "content" .}}
{{block "message" .}}{{end}}
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ if .Title }}{{ .Title }}{{ else }}Default Main Title{{ end }}</title>
</head>
<body>
{{ template "content" . }}
<footer>{{ partial "partials/footer" .}}</footer>
</body>
</html>

View File

@ -0,0 +1 @@
<h3>Footer Partial</h3>

View File

@ -23,6 +23,9 @@ type (
// Developers send responses to the client's request through a Context. // Developers send responses to the client's request through a Context.
// Developers get request information from the client's request by a Context. // Developers get request information from the client's request by a Context.
Context = *context.Context Context = *context.Context
// ViewEngine is an alias of `context.ViewEngine`.
// See HTML, Blocks, Django, Jet, Pug, Ace, Handlebars, Amber and e.t.c.
ViewEngine = context.ViewEngine
// UnmarshalerFunc a shortcut, an alias for the `context#UnmarshalerFunc` type // UnmarshalerFunc a shortcut, an alias for the `context#UnmarshalerFunc` type
// which implements the `context#Unmarshaler` interface for reading request's body // which implements the `context#Unmarshaler` interface for reading request's body
// via custom decoders, most of them already implement the `context#UnmarshalerFunc` // via custom decoders, most of them already implement the `context#UnmarshalerFunc`

View File

@ -756,18 +756,30 @@ type Configuration struct {
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`. // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`.
// Defaults to "iris.api.version". // Defaults to "iris.api.version".
VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"`
// GetViewLayoutContextKey is the key of the context's user values' key // ViewEngineContextKey is the context's values key
// which is being used to set the template // responsible to store and retrieve(view.Engine) the current view engine.
// layout from a middleware or the main handler. // A middleware or a Party can modify its associated value to change
// Overrides the parent's or the configuration's. // a view engine that `ctx.View` will render through.
// If not an engine is registered by the end-developer
// then its associated value is always nil,
// meaning that the default value is nil.
// See `Party.RegisterView` and `Context.ViewEngine` methods as well.
// //
// Defaults to "iris.ViewLayout" // Defaults to "iris.view.engine".
ViewEngineContextKey string `json:"viewEngineContextKey,omitempty" yaml:"ViewEngineContextKey" toml:"ViewEngineContextKey"`
// ViewLayoutContextKey is the context's values key
// responsible to store and retrieve(string) the current view layout.
// A middleware can modify its associated value to change
// the layout that `ctx.View` will use to render a template.
//
// Defaults to "iris.view.layout".
ViewLayoutContextKey string `json:"viewLayoutContextKey,omitempty" yaml:"ViewLayoutContextKey" toml:"ViewLayoutContextKey"` ViewLayoutContextKey string `json:"viewLayoutContextKey,omitempty" yaml:"ViewLayoutContextKey" toml:"ViewLayoutContextKey"`
// GetViewDataContextKey is the key of the context's user values' key // ViewDataContextKey is the context's values key
// which is being used to set the template // responsible to store and retrieve(interface{}) the current view binding data.
// binding data from a middleware or the main handler. // A middleware can modify its associated value to change
// the template's data on-fly.
// //
// Defaults to "iris.viewData" // Defaults to "iris.view.data".
ViewDataContextKey string `json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"` ViewDataContextKey string `json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"`
// RemoteAddrHeaders are the allowed request headers names // RemoteAddrHeaders are the allowed request headers names
// that can be valid to parse the client's IP based on. // that can be valid to parse the client's IP based on.
@ -945,6 +957,11 @@ func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey return c.VersionContextKey
} }
// GetViewEngineContextKey returns the ViewEngineContextKey field.
func (c Configuration) GetViewEngineContextKey() string {
return c.ViewEngineContextKey
}
// GetViewLayoutContextKey returns the ViewLayoutContextKey field. // GetViewLayoutContextKey returns the ViewLayoutContextKey field.
func (c Configuration) GetViewLayoutContextKey() string { func (c Configuration) GetViewLayoutContextKey() string {
return c.ViewLayoutContextKey return c.ViewLayoutContextKey
@ -1094,10 +1111,12 @@ func WithConfiguration(c Configuration) Configurator {
main.VersionContextKey = v main.VersionContextKey = v
} }
if v := c.ViewEngineContextKey; v != "" {
main.ViewEngineContextKey = v
}
if v := c.ViewLayoutContextKey; v != "" { if v := c.ViewLayoutContextKey; v != "" {
main.ViewLayoutContextKey = v main.ViewLayoutContextKey = v
} }
if v := c.ViewDataContextKey; v != "" { if v := c.ViewDataContextKey; v != "" {
main.ViewDataContextKey = v main.ViewDataContextKey = v
} }
@ -1169,8 +1188,9 @@ func DefaultConfiguration() Configuration {
LocaleContextKey: "iris.locale", LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language", LanguageContextKey: "iris.locale.language",
VersionContextKey: "iris.api.version", VersionContextKey: "iris.api.version",
ViewLayoutContextKey: "iris.viewLayout", ViewEngineContextKey: "iris.view.engine",
ViewDataContextKey: "iris.viewData", ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data",
RemoteAddrHeaders: nil, RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false, RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{ RemoteAddrPrivateSubnets: []netutil.IPRange{

View File

@ -51,6 +51,9 @@ type ConfigurationReadOnly interface {
GetLanguageContextKey() string GetLanguageContextKey() string
// GetVersionContextKey returns the VersionContextKey field. // GetVersionContextKey returns the VersionContextKey field.
GetVersionContextKey() string GetVersionContextKey() string
// GetViewEngineContextKey returns the ViewEngineContextKey field.
GetViewEngineContextKey() string
// GetViewLayoutContextKey returns the ViewLayoutContextKey field. // GetViewLayoutContextKey returns the ViewLayoutContextKey field.
GetViewLayoutContextKey() string GetViewLayoutContextKey() string
// GetViewDataContextKey returns the ViewDataContextKey field. // GetViewDataContextKey returns the ViewDataContextKey field.

View File

@ -2392,16 +2392,22 @@ func (ctx *Context) CompressReader(enable bool) error {
// | Rich Body Content Writers/Renderers | // | Rich Body Content Writers/Renderers |
// +------------------------------------------------------------+ // +------------------------------------------------------------+
const ( // ViewEngine registers a view engine for the current chain of handlers.
// NoLayout to disable layout for a particular template file // It overrides any previously registered engines, including the application's root ones.
NoLayout = "iris.nolayout" // Note that, because performance is everything,
) // the "engine" MUST be already ready-to-use,
// meaning that its `Load` method should be called once before this method call.
//
// To register a view engine per-group of groups too see `Party.RegisterView` instead.
func (ctx *Context) ViewEngine(engine ViewEngine) {
ctx.values.Set(ctx.app.ConfigurationReadOnly().GetViewEngineContextKey(), engine)
}
// ViewLayout sets the "layout" option if and when .View // ViewLayout sets the "layout" option if and when .View
// is being called afterwards, in the same request. // is being called afterwards, in the same request.
// Useful when need to set or/and change a layout based on the previous handlers in the chain. // Useful when need to set or/and change a layout based on the previous handlers in the chain.
// //
// Note that the 'layoutTmplFile' argument can be set to iris.NoLayout || view.NoLayout || context.NoLayout // Note that the 'layoutTmplFile' argument can be set to iris.NoLayout
// to disable the layout for a specific view render action, // to disable the layout for a specific view render action,
// it disables the engine's configuration's layout property. // it disables the engine's configuration's layout property.
// //
@ -2418,7 +2424,7 @@ func (ctx *Context) ViewLayout(layoutTmplFile string) {
// //
// If .View's "binding" argument is not nil and it's not a type of map // If .View's "binding" argument is not nil and it's not a type of map
// then these data are being ignored, binding has the priority, so the main route's handler can still decide. // then these data are being ignored, binding has the priority, so the main route's handler can still decide.
// If binding is a map or context.Map then these data are being added to the view data // If binding is a map or iris.Map then these data are being added to the view data
// and passed to the template. // and passed to the template.
// //
// After .View, the data are not destroyed, in order to be re-used if needed (again, in the same request as everything else), // After .View, the data are not destroyed, in order to be re-used if needed (again, in the same request as everything else),
@ -2457,7 +2463,7 @@ func (ctx *Context) ViewData(key string, value interface{}) {
// A check for nil is always a good practise if different // A check for nil is always a good practise if different
// kind of values or no data are registered via `ViewData`. // kind of values or no data are registered via `ViewData`.
// //
// Similarly to `viewData := ctx.Values().Get("iris.viewData")` or // Similarly to `viewData := ctx.Values().Get("iris.view.data")` or
// `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`. // `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`.
func (ctx *Context) GetViewData() map[string]interface{} { func (ctx *Context) GetViewData() map[string]interface{} {
viewDataContextKey := ctx.app.ConfigurationReadOnly().GetViewDataContextKey() viewDataContextKey := ctx.app.ConfigurationReadOnly().GetViewDataContextKey()
@ -2514,7 +2520,21 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro
bindingData = ctx.values.Get(cfg.GetViewDataContextKey()) bindingData = ctx.values.Get(cfg.GetViewDataContextKey())
} }
err := ctx.app.View(ctx, filename, layout, bindingData) if key := cfg.GetViewEngineContextKey(); key != "" {
if engineV := ctx.values.Get(key); engineV != nil {
if engine, ok := engineV.(ViewEngine); ok {
err := engine.ExecuteWriter(ctx, filename, layout, bindingData)
if err != nil {
ctx.app.Logger().Errorf("View [%v] [%T]: %v", ctx.getLogIdentifier(), engine, err)
return err
}
return nil
}
}
}
err := ctx.app.View(ctx, filename, layout, bindingData) // if failed it logs the error.
if err != nil { if err != nil {
ctx.StopWithStatus(http.StatusInternalServerError) ctx.StopWithStatus(http.StatusInternalServerError)
} }
@ -2522,6 +2542,16 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro
return err return err
} }
// getLogIdentifier returns the ID, or the client remote IP address,
// useful for internal logging of context's method failure.
func (ctx *Context) getLogIdentifier() interface{} {
if id := ctx.GetID(); id != nil {
return id
}
return ctx.RemoteAddr()
}
const ( const (
// ContentBinaryHeaderValue header value for binary data. // ContentBinaryHeaderValue header value for binary data.
ContentBinaryHeaderValue = "application/octet-stream" ContentBinaryHeaderValue = "application/octet-stream"

22
context/view.go Normal file
View File

@ -0,0 +1,22 @@
package context
import "io"
// ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris.
type ViewEngine interface {
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
Load() error
// ExecuteWriter should execute a template by its filename with an optional layout and bindingData.
ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error
// Ext should return the final file extension which this view engine is responsible to render.
Ext() string
}
// ViewEngineFuncer is an addition of a view engine,
// if a view engine implements that interface
// then iris can add some closed-relative iris functions
// like {{ url }}, {{ urlpath }} and {{ tr }}.
type ViewEngineFuncer interface {
// AddFunc should adds a function to the template's function map.
AddFunc(funcName string, funcBody interface{})
}

View File

@ -1022,6 +1022,24 @@ func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) (routes []*Ro
return return
} }
// RegisterView registers and loads a view engine middleware for that group of routes.
// It overrides any of the application's root registered view engines.
// To register a view engine per handler chain see the `Context.ViewEngine` instead.
// Read `Configuration.ViewEngineContextKey` documentation for more.
func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) {
if err := viewEngine.Load(); err != nil {
api.errors.Add(err)
return
}
api.Use(func(ctx *context.Context) {
ctx.ViewEngine(viewEngine)
ctx.Next()
})
// Note (@kataras): It does not return the Party in order
// to keep the iris.Application a compatible Party.
}
// Layout overrides the parent template layout with a more specific layout for this Party. // Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party. // It returns the current Party.
// //

View File

@ -241,6 +241,11 @@ type Party interface {
// Returns the GET *Route. // Returns the GET *Route.
Favicon(favPath string, requestPath ...string) *Route Favicon(favPath string, requestPath ...string) *Route
// RegisterView registers and loads a view engine middleware for that group of routes.
// It overrides any of the application's root registered view engines.
// To register a view engine per handler chain see the `Context.ViewEngine` instead.
// Read `Configuration.ViewEngineContextKey` documentation for more.
RegisterView(viewEngine context.ViewEngine)
// Layout overrides the parent template layout with a more specific layout for this Party. // Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party. // It returns the current Party.
// //

View File

@ -280,7 +280,7 @@ func (app *Application) View(writer io.Writer, filename string, layout string, b
} }
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions // ConfigureHost accepts one or more `host#Configuration`, these configurators functions
// can access the host created by `app.Run`, // can access the host created by `app.Run` or `app.Listen`,
// they're being executed when application is ready to being served to the public. // they're being executed when application is ready to being served to the public.
// //
// It's an alternative way to interact with a host that is automatically created by // It's an alternative way to interact with a host that is automatically created by

View File

@ -21,10 +21,11 @@ func Ace(directory, extension string) *HTMLEngine {
once := new(sync.Once) once := new(sync.Once)
s.middleware = func(name string, text []byte) (contents string, err error) { s.middleware = func(name string, text []byte) (contents string, err error) {
once.Do(func() { // on first template parse, all funcs are given. once.Do(func() { // on first template parse, all funcs are given.
for k, v := range s.funcs { for k, v := range emptyFuncs {
funcs[k] = v funcs[k] = v
} }
for k, v := range emptyFuncs {
for k, v := range s.funcs {
funcs[k] = v funcs[k] = v
} }
}) })

View File

@ -1,31 +0,0 @@
package view
import (
"io"
)
// NoLayout disables the configuration's layout for a specific execution.
const NoLayout = "iris.nolayout"
// returns empty if it's no layout or empty layout and empty configuration's layout.
func getLayout(layout string, globalLayout string) string {
if layout == NoLayout {
return ""
}
if layout == "" && globalLayout != "" {
return globalLayout
}
return layout
}
// Engine is the interface which all view engines should be implemented in order to be registered inside iris.
type Engine interface {
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
Load() error
// ExecuteWriter should execute a template by its filename with an optional layout and bindingData.
ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error
// Ext should return the final file extension which this view engine is responsible to render.
Ext() string
}

View File

@ -1,10 +0,0 @@
package view
// EngineFuncer is an addition of a view engine,
// if a view engine implements that interface
// then iris can add some closed-relative iris functions
// like {{ urlpath }} and {{ urlpath }}.
type EngineFuncer interface {
// AddFunc should adds a function to the template's function map.
AddFunc(funcName string, funcBody interface{})
}

View File

@ -5,6 +5,18 @@ import (
"io" "io"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/kataras/iris/v12/context"
)
type (
// Engine is the interface for a compatible Iris view engine.
// It's an alias of context.ViewEngine.
Engine = context.ViewEngine
// EngineFuncer is the interface for a compatible Iris view engine
// which accepts builtin framework functions such as url, urlpath and tr.
// It's an alias of context.ViewEngineFuncer.
EngineFuncer = context.ViewEngineFuncer
) )
// View is responsible to // View is responsible to
@ -73,3 +85,19 @@ func (v *View) Load() error {
} }
return nil return nil
} }
// NoLayout disables the configuration's layout for a specific execution.
const NoLayout = "iris.nolayout"
// returns empty if it's no layout or empty layout and empty configuration's layout.
func getLayout(layout string, globalLayout string) string {
if layout == NoLayout {
return ""
}
if layout == "" && globalLayout != "" {
return globalLayout
}
return layout
}