3 |
4 | {{block "details" .}}{{end}}
5 |
6 | {{end}}
7 |
--------------------------------------------------------------------------------
/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/reddec/view"
9 | )
10 |
11 | //go:embed all:views
12 | var views embed.FS
13 |
14 | func main() {
15 | index := view.Must(view.New[string](views, "views/index.gohtml"))
16 | about := view.Must(view.New[string](views, "views/info/about.gohtml"))
17 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
18 | index.Render(w, "the index page")
19 | })
20 | http.HandleFunc("/info/about", func(w http.ResponseWriter, r *http.Request) {
21 | about.Render(w, "made by RedDec")
22 | })
23 | fmt.Println("ready on :8080")
24 | panic(http.ListenAndServe(":8080", nil))
25 | }
26 |
--------------------------------------------------------------------------------
/example/views/_layout.gohtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{block "title" .}}{{end}}
9 |
10 |
11 |
12 |
13 |
22 |
23 | {{block "main" .}}
24 | Default content
25 | {{end}}
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Aleksandr Baryshnikov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # View [](https://godoc.org/github.com/reddec/view)
2 |
3 | The `view` package provides a type-safe and hierarchical (layouts) way to load and render Go HTML templates using the standard library's [`html/template`](https://pkg.go.dev/html/template) package. It supports loading templates from any sources exposed as [`fs.FS`](https://pkg.go.dev/io/fs#FS). The package comes with no external dependencies and is designed for use in web applications.
4 |
5 | This is extremly light library based on gist https://gist.github.com/reddec/312367d75cc03f1ee49bae74c52a6b31 and has zero external dependecies.
6 |
7 | Key points:
8 |
9 | - **Hierarchical**: The templates are loaded in a hierarchical way, allowing you to have a base layout and extend it with partials or views at different levels. Layouts defined in each directory as `_layout.gohtml` file and can be extended.
10 | - **Type-safe**: The package provides a type-safe wrapper around the standard `html/template` library using a custom `View` struct.
11 |
12 |
13 | ## [Example](example/)
14 |
15 | Layout
16 |
17 | ```
18 | ├── main.go
19 | └── views
20 | ├── _layout.gohtml
21 | ├── index.gohtml
22 | └── info
23 | ├── _layout.gohtml
24 | └── about.gohtml
25 | ```
26 |
27 |
28 | And the code
29 |
30 | ```go
31 | package main
32 |
33 | import (
34 | "embed"
35 | "fmt"
36 | "net/http"
37 |
38 | "github.com/reddec/view"
39 | )
40 |
41 | //go:embed all:views
42 | var views embed.FS
43 |
44 | func main() {
45 | index := view.Must(view.New[string](views, "views/index.gohtml"))
46 | about := view.Must(view.New[string](views, "views/info/about.gohtml"))
47 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
48 | index.Render(w, "the index page")
49 | })
50 | http.HandleFunc("/info/about", func(w http.ResponseWriter, r *http.Request) {
51 | about.Render(w, "made by RedDec")
52 | })
53 | fmt.Println("ready on :8080")
54 | panic(http.ListenAndServe(":8080", nil))
55 | }
56 | ```
57 |
58 | - note: `all:view` - the `all:` prefix is required in order to include files with underscore in name prefix
59 |
60 | ## Installation
61 |
62 | ```bash
63 | go get github.com/reddec/view
64 | ```
--------------------------------------------------------------------------------
/view.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "html/template"
8 | "io"
9 | "io/fs"
10 | "net/http"
11 | "os"
12 | "path"
13 | "strings"
14 | )
15 |
16 | // Layout base file name.
17 | const layoutName = "_layout.gohtml"
18 |
19 | // Load is the same as LoadTemplate but using new empty template as root.
20 | func Load(store fs.FS, view string) (*template.Template, error) {
21 | return LoadTemplate(template.New(""), store, view)
22 | }
23 |
24 | // Load a single template (view) and all associated layouts (_layout.gohtml), starting from the top directory up to the current one.
25 | //
26 | // When using embedded FS (//go:embed), remember to use all suffixes (all:) to ensure that files with underscores
27 | // get embedded by the Go compiler. See https://github.com/golang/go/commit/36dbf7f7e63f3738795bb04593c3c011e987d1f3
28 | func LoadTemplate(root *template.Template, store fs.FS, view string) (*template.Template, error) {
29 | dirs := strings.Split(strings.Trim(view, "/"), "/")
30 | dirs = dirs[:len(dirs)-1] // last segment is view itself
31 | // parse layouts from all dirs starting from the top and until the current dir
32 | for i := range dirs {
33 | fpath := path.Join(path.Join(dirs[:i+1]...), layoutName)
34 | content, err := fs.ReadFile(store, fpath)
35 | if errors.Is(err, os.ErrNotExist) || errors.Is(err, fs.ErrNotExist) {
36 | continue // layout does not exists - skipping
37 | }
38 | if err != nil {
39 | return nil, fmt.Errorf("read layouad %q: %w", fpath, err)
40 | }
41 |
42 | child, err := root.Parse(string(content))
43 | if err != nil {
44 | return nil, fmt.Errorf("parse %q: %w", fpath, err)
45 | }
46 | root = child
47 | }
48 | // parse view it self
49 | content, err := fs.ReadFile(store, view)
50 | if err != nil {
51 | return nil, fmt.Errorf("parse view %q: %w", view, err)
52 | }
53 | return root.Parse(string(content))
54 | }
55 |
56 | // NewTemplate creates new [View] with provided root template and type-safe parameter.
57 | func NewTemplate[T any](root *template.Template, store fs.FS, view string) (*View[T], error) {
58 | t, err := LoadTemplate(root, store, view)
59 | if err != nil {
60 | return nil, err
61 | }
62 | return &View[T]{
63 | parsed: t,
64 | }, nil
65 | }
66 |
67 | // New creates new [View] with empty root template and type-safe parameter.
68 | func New[T any](store fs.FS, view string) (*View[T], error) {
69 | return NewTemplate[T](template.New(""), store, view)
70 | }
71 |
72 | // Must is convinient helper for wrapping around New* constructors.
73 | // The function will panic if error parameter is not nil.
74 | func Must[T any](v *View[T], e error) *View[T] {
75 | if e != nil {
76 | panic(e)
77 | }
78 | return v
79 | }
80 |
81 | // View is type-safe tiny wrapper around standard template.
82 | type View[T any] struct {
83 | parsed *template.Template
84 | }
85 |
86 | // Render template as web page and content-type to text/html.
87 | // It doesn't change response code. In case of error, some part of template
88 | // could be sent to the client.
89 | func (v *View[T]) Render(writer http.ResponseWriter, value T) error {
90 | writer.Header().Set("Content-Type", "text/html")
91 | return v.Execute(writer, value)
92 | }
93 |
94 | // Execute template and render content to the writer. It's just a type-safe wrapper around template.Execute.
95 | func (v *View[T]) Execute(writer io.Writer, value T) error {
96 | return v.parsed.Execute(writer, value)
97 | }
98 |
99 | // Bytes result of template execution.
100 | func (v *View[T]) Bytes(value T) ([]byte, error) {
101 | var buf bytes.Buffer
102 | err := v.Execute(&buf, value)
103 | return buf.Bytes(), err
104 | }
105 |
--------------------------------------------------------------------------------