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:
- 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 [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:
- `Context.SetErr(error)` and `Context.GetErr() error` helpers
- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`
- `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.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.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).

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 get request information from the client's request by a 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
// which implements the `context#Unmarshaler` interface for reading request's body
// 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")`.
// Defaults to "iris.api.version".
VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"`
// GetViewLayoutContextKey is the key of the context's user values' key
// which is being used to set the template
// layout from a middleware or the main handler.
// Overrides the parent's or the configuration's.
// ViewEngineContextKey is the context's values key
// responsible to store and retrieve(view.Engine) the current view engine.
// A middleware or a Party can modify its associated value to change
// 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"`
// GetViewDataContextKey is the key of the context's user values' key
// which is being used to set the template
// binding data from a middleware or the main handler.
// ViewDataContextKey is the context's values key
// responsible to store and retrieve(interface{}) the current view binding data.
// 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"`
// RemoteAddrHeaders are the allowed request headers names
// that can be valid to parse the client's IP based on.
@ -945,6 +957,11 @@ func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey
}
// GetViewEngineContextKey returns the ViewEngineContextKey field.
func (c Configuration) GetViewEngineContextKey() string {
return c.ViewEngineContextKey
}
// GetViewLayoutContextKey returns the ViewLayoutContextKey field.
func (c Configuration) GetViewLayoutContextKey() string {
return c.ViewLayoutContextKey
@ -1094,10 +1111,12 @@ func WithConfiguration(c Configuration) Configurator {
main.VersionContextKey = v
}
if v := c.ViewEngineContextKey; v != "" {
main.ViewEngineContextKey = v
}
if v := c.ViewLayoutContextKey; v != "" {
main.ViewLayoutContextKey = v
}
if v := c.ViewDataContextKey; v != "" {
main.ViewDataContextKey = v
}
@ -1169,8 +1188,9 @@ func DefaultConfiguration() Configuration {
LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language",
VersionContextKey: "iris.api.version",
ViewLayoutContextKey: "iris.viewLayout",
ViewDataContextKey: "iris.viewData",
ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data",
RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{

View File

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

View File

@ -2392,16 +2392,22 @@ func (ctx *Context) CompressReader(enable bool) error {
// | Rich Body Content Writers/Renderers |
// +------------------------------------------------------------+
const (
// NoLayout to disable layout for a particular template file
NoLayout = "iris.nolayout"
)
// ViewEngine registers a view engine for the current chain of handlers.
// It overrides any previously registered engines, including the application's root ones.
// 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
// 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.
//
// 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,
// 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
// 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.
//
// 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
// 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())`.
func (ctx *Context) GetViewData() map[string]interface{} {
viewDataContextKey := ctx.app.ConfigurationReadOnly().GetViewDataContextKey()
@ -2514,7 +2520,21 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro
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 {
ctx.StopWithStatus(http.StatusInternalServerError)
}
@ -2522,6 +2542,16 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro
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 (
// ContentBinaryHeaderValue header value for binary data.
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
}
// 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.
// It returns the current Party.
//

View File

@ -241,6 +241,11 @@ type Party interface {
// Returns the GET *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.
// 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
// 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.
//
// 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)
s.middleware = func(name string, text []byte) (contents string, err error) {
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
}
for k, v := range emptyFuncs {
for k, v := range s.funcs {
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"
"path/filepath"
"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
@ -73,3 +85,19 @@ func (v *View) Load() error {
}
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
}