Query Params

Model

In the weather API, besides the city, we also have two variables: units (metric / imperial) and forecast days.

Add them to the path model as query params, and prepare a settings struct:

Query values are part of the Path Model.

main.go

type Path struct {
	Home      bool `path:"/"`
	Dashboard bool `path:"/:CityID"`
	CityID    int
	Units     *driver.Units `query:"units"`
	Days      *int          `query:"days"`
}

// function to derive settings from path
func NewSettigns(p Path) Settings {
	// default values
	s := Settings{
		Days:  7,
		Units: driver.Metric,
	}
	if p.Days != nil {
		s.Days = max(*p.Days, 1)
	}
	if p.Units != nil && *p.Units == driver.Imperial {
		s.Units = driver.Imperial
	}
	return s
}

type Settings struct {
	Units driver.Units
	Days  int
}

func (s Settings) DaysQuery() *int {
	// omit default
	if s.Days == 7 {
		return nil
	}
	return &s.Days
}

func (s Settings) UnitsQuery() *driver.Units {
	// omit default
	if s.Units == driver.Metric {
		return nil
	}
	return &s.Units
}

Notice that query params use reference types. Otherwise, they get a zero value and always appear in the URI.

State

Next, inject this state into the app.

This uses the same derived-state pattern from State.

app.gox

type App struct {
	/* ... */
	settings doors.Beam[Settings]
}

main.go

func main() {
	/* ... */

	doors.UseModel(r, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
		/* ... */
		// new state piece
		settings := doors.NewBeam(s, NewSettigns)
		return doors.ResponseComp(App{path: s, city: city, settings: settings})
	})

	/* ... */
}

Next, we need it in the dashboard:

dashboard.gox

type dashboard struct {
	city int
	settings doors.Beam[Settings]
}

app.gox

elem (a App) Main() {
	~/* ... */

					~dashboard{
						city: city,
						// new field
						settings: a.settings,
					}
	~/* ... */
}

Code

main.go

package main

import (
	"net/http"

	"github.com/doors-dev/doors"
	"github.com/doors-dev/tutorial/driver"
)

type Path struct {
	Home      bool `path:"/"`
	Dashboard bool `path:"/:CityID"`
	CityID    int
	Units     *driver.Units `query:"units"`
	Days      *int          `query:"days"`
}

func NewSettigns(p Path) Settings {
	s := Settings{
		Days:  7,
		Units: driver.Metric,
	}
	if p.Days != nil {
		s.Days = max(*p.Days, 1)
	}
	if p.Units != nil && *p.Units == driver.Imperial {
		s.Units = driver.Imperial
	}
	return s
}

type Settings struct {
	Units driver.Units
	Days  int
}

func (s Settings) DaysQuery() *int {
	if s.Days == 7 {
		return nil
	}
	return &s.Days
}

func (s Settings) UnitsQuery() *driver.Units {
	if s.Units == driver.Metric {
		return nil
	}
	return &s.Units
}

func main() {
	r := doors.NewRouter()

	doors.UseModel(r, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
		city := doors.NewBeam(s, func(p Path) int {
			if p.Home {
				return -1
			}
			return p.CityID
		})
		settings := doors.NewBeam(s, NewSettigns)
		return doors.ResponseComp(App{path: s, city: city, settings: settings})
	})

	if err := http.ListenAndServe(":8080", r); err != nil {
		panic(err)
	}
}

app.gox

package main

import (
	"context"

	"github.com/doors-dev/doors"
	"github.com/doors-dev/gox"
)

type App struct {
	path doors.Source[Path]
	city doors.Beam[int]
	settings doors.Beam[Settings]
}

elem (a App) Main() {
	<!doctype html>
	<html lang="en" data-theme="dark">
		<head>
			<meta charset="utf-8">
			<meta name="viewport" content="width=device-width, initial-scale=1">
			<link
				(doors.ResourceExternal("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"))
				rel="stylesheet">
		</head>
		<body>
			<main class="container">
				~(doors.Sub(a.city, elem(city int) {
					~(if city == -1 {
						~(LocationSelector(func(ctx context.Context, city int) {
							a.path.Mutate(ctx, func(p Path) Path {
								p.Home = false
								p.Dashboard = true
								p.CityID = city
								return p
							})
						}))
					} else {
						~dashboard{
							city: city,
							settings: a.settings,
						}
					})
				}))
			</main>
		</body>
	</html>
}

dashboard.gox

package main

import (
	"github.com/doors-dev/doors"
	"github.com/doors-dev/gox"
	"github.com/doors-dev/tutorial/driver"
)

type dashboard struct {
	city int
	settings doors.Beam[Settings]
}

elem (d dashboard) Main() {
	~{
		city, _ := driver.Locations.CitiesGet(d.city)
	}
	~(d.header(city))
	~(doors.Sub(d.settings, elem(s Settings) {
		~(d.menu(s))
	}))
}

elem (d dashboard) header(city driver.City) {
	~(if !city.IsValid() {
		~(doors.Status(404))
		<title>Not Found</title>
		<h1>City Not Found</h1>
	} else {
		<title>~(city.Name) Weather</title>
		<h1>Weather in ~(city.Name, ", ", city.Country.Name)</h1>
	})
}

elem (d dashboard) menu(s Settings) {
	<a
		class="secondary"
		(doors.ALink{
			Model: Path{
				Home: true,
				Days: s.DaysQuery(),
				Units: s.UnitsQuery(),
			},
		})
		role="button">
		Change
	</a>
	<nav>
		~(d.navDays(s))
		~(d.navUnits(s))
	</nav>
}

elem (d dashboard) navDays(s Settings) {
	<ul>
		~(for i := range 7 {
			~{
				s.Days = i + 1
			}
			<li>
				<a
					class="secondary"
					(doors.ALink{
						Model: Path{
							Dashboard: true,
							CityID: d.city,
							Days: s.DaysQuery(),
							Units: s.UnitsQuery(),
						},
					})>
					~(s.Days)
					~(if s.Days == 1 {
						 day
					} else {
						 days
					})
				</a>
			</li>
		})
	</ul>
}

elem (d dashboard) navUnits(s Settings) {
	<ul>
		~(for _, unit := range []driver.Units{driver.Metric, driver.Imperial} {
			<li>
				~{
					s.Units = unit
				}
				<a
					class="secondary"
					(doors.ALink{
						Model: Path{
							Dashboard: true,
							CityID: d.city,
							Days: s.DaysQuery(),
							Units: s.UnitsQuery(),
						},
					})>
					~(unit.String())
				</a>
			</li>
		})
	</ul>
}