v0.5.7 beta
Back-end UI Framework

for feature-rich, secure, and fast web apps in Go

Tutorial

Catalog Page

Mini app with three pages.

  • Show a list of categories.
  • Show a list of items within a category.
  • Show item card.

I assume you have live reloading enabled. If you prefer not to use it, don’t forget to run templ generate and restart.

1. Path Model

Let’s add a new page path model.

./common/path.go

package common

type CatalogPath struct {
	IsMain bool `path:"/catalog"`                // show categories
	IsCat  bool `path:"/catalog/:CatId"`         // show items of category
	IsItem bool `path:"/catalog/:CatId/:ItemId"` // show item
	CatId  string
	ItemId int
	Page   *int `query:"page"` // query param for pagination (used pointer to avoid showing 0 default value)
}

// prev one, keep it
type HomePath struct {
	Main bool `path:"/"`
}

2. Page & Handler

In the new catalog package

./catalog/page.templ

package catalog

import "github.com/doors-dev/doors"
import "github.com/derstruct/doors-tutorial/common"

type Path = common.CatalogPath

type catalogPage struct {
}

templ (c *catalogPage) Head() {
	<title>catalog</title>
}

templ (c *catalogPage) Body() {
	<h1>Catalog</h1>
}

/*
Instead of doing this:

templ (c *catalogPage) Render(b doors.SourceBeam[Path]) {
	@common.Template(c)
}

Because there is only one component `common.Template(c)`, we can return it directly:
*/

func (c *catalogPage) Render(b doors.SourceBeam[Path]) templ.Component {
    return common.Template(c)
}

// page request handler
func Handler(p doors.PageRouter[Path], r doors.RPage[Path]) doors.PageRoute {
	return p.Page(&catalogPage{})
}

3. Router

./main.go

package main

import (
	"net/http"

	"github.com/derstruct/doors-tutorial/catalog"
	"github.com/derstruct/doors-tutorial/home"
	"github.com/doors-dev/doors"
)

func main() {
	r := doors.NewRouter()
	r.Use(
    doors.UsePage(home.Handler),
    // our new catalog page
    doors.UsePage(catalog.Handler),
  )
	err := http.ListenAndServeTLS(":8443", "localhost+2.pem", "localhost+2-key.pem", r)
	if err != nil {
		panic(err)
	}
}

4. Check

Visit https://localhost:8443/catalog

5. Styling

To save some time, let’s use PicoCSS’s default styles. Include CDN styles in our template.

./common/page.templ

package common

import "github.com/doors-dev/doors"

type Page interface {
	Head() templ.Component
	Body() templ.Component
}

templ Template(p Page) {
	<!DOCTYPE html>
	<html lang="en">
		<head>
			<meta charset="utf-8"/>
			<meta name="viewport" content="width=device-width, initial-scale=1"/>
			<meta name="color-scheme" content="light dark"/>
			@doors.Include()

			// same as <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"/>
			// but adds CSP headers (useful if you enable CSP later)
			@doors.ImportStyleExternal{
				Href: "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css",
			}
			@p.Head()
		</head>
		<body>
			@p.Body()
		</body>
	</html>
}

Please refer to Imports for details

5. Menu

We now have two pages; let’s add some navigation.

./common/components.templ

package common

templ menu() {
	<nav>
		<ul>
			<li><strong>doors tutorial</strong></li>
		</ul>
		<ul>
			<li>
				<a href="/">home</a>
			</li>
			<li>
				<a href="/catalog">services</a>
			</li>
		</ul>
	</nav>
}

Include it in our template.

./common/page.templ

templ Template(p Page) {
		/* ... */
    <body>
        // PicoCSS container
        <main class="container">
            // menu component
            @menu()
            @p.Body()
        </main>
    </body>
		/* ... */
}

Confirm it works in the browser

5. Idiomatic Menu

doors allows you to create href attributes in a type-safe manner and includes extensive tools for highlighting active links.

/common/components.templ

package common

import "github.com/doors-dev/doors"

templ menu() {
	<nav>
		<ul>
			<li><strong>doors tutorial</strong></li>
		</ul>
		<ul>
			<li>
			  // Magic attribute syntax, attribute attached to the next element
				@doors.AHref{
					// href Path Model
					Model: HomePath{},
					// active link highlighting settings
					Active: doors.Active{
						// indicate active link with attribute aria-current="page"
						Indicator: doors.IndicatorOnlyAttr("aria-current", "page"),
					},
				}
				<a>home</a>
			</li>
			<li>
			  // Magic attribute syntax, attribute attached to the next element
				@doors.AHref{
					// href Path Model
					Model: CatalogPath{
						// marker of variant
						IsMain: true,
					},
					// active link highlighting settings
					Active: doors.Active{
						// indicate active link with attribute aria-current="page"
						Indicator: doors.IndicatorOnlyAttr("aria-current", "page"),
						// page path must start with href value to enable highlighting
						PathMatcher: doors.PathMatcherStarts(),
						// ignore query params 
						QueryMatcher: doors.QueryMatcherIgnore(),
					},
				}
				<a>services</a>
			</li>
		</ul>
	</nav>
}

Navigation between the home page and the catalog triggers a new page load because you’re switching between different path models. We learn about dynamic pages in the next article.

Please refer to Href and Src for full details on href. Alternatively, instead of using Magic Attribute syntax, you can use attribute spread; see Attributes for details.

6. Conclusion

Visit https://localhost:8443/ to verify that page switching works and active link indication is applied.


Next: Dynamic Content