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 some helpers:
Query values are part of Routing.
app.gox
type Path struct {
Route Route `path:"/ | /:CityID"`
CityID int
Units *driver.Units `query:"units"`
Days *int `query:"days"`
}
// Get the days value from the query.
func (p Path) days() int {
// Default value.
if p.Days == nil {
return 7
}
return min(max(*p.Days, 1), 7)
}
// Helper to convert days back to a query value.
func daysQuery(days int) *int {
if days == 7 {
return nil
}
return &days
}
// Get the units value from the query.
func (p Path) units() driver.Units {
// Default value.
if p.Units == nil || *p.Units != driver.Imperial {
return driver.Metric
}
return driver.Imperial
}
// Helper to convert units back to a query value.
func unitsQuery(units driver.Units) *driver.Units {
if units == driver.Metric {
return nil
}
return &units
}
Query params use reference types in our case. Otherwise, they get a zero value and always appear in the URI. Decoding and encoding of query params are provided by go-playground/form v4. Refer to its documentation for all supported features.
Effect
You may notice that query params are lost on location change. To mitigate that, we have two options: convert Change to a button that mutates path state, or subscribe the Change link to both units and days. The first option is genuinely better because it has a smaller re-render surface. But the second is a nice opportunity to learn the Effect feature.
Effect causes the closest dynamic parent to reload automatically on state change. It is a "React-style" API.
elem (d dashboard) Main() {
~/* ... */
~// Refactor the change link into a separate component.
~(d.change())
~(d.menu())
}
elem (d dashboard) change() {
~// Dynamic container proxy.
~>(new(doors.Door)) <>
~{
// Read state values with "effect".
days, _ := d.days.Effect(ctx)
units, ok := d.units.Effect(ctx)
}
~(if ok {
<a
class="secondary"
(doors.ALink{
Model: Path{
Route: Selector,
Days: daysQuery(days),
Units: unitsQuery(units),
},
})
role="button">
Change
</a>
})
</>
}
First, notice the new syntax ~>. This is called proxy syntax in GoX. A proxy wraps the following element and can apply any transformation to it. Essentially, it is similar to passing an element to a function, but with semantic meaning: this element will be transformed somehow and will not be rendered as-is. In the case of a doors.Door proxy, this element turns into a dynamic container.
Second, notice that we ignored ok from the first effect call. Effect fails, returning false, only if the context is canceled because the dynamic parent re-rendered or unmounted, so it makes sense to check only the last call.
We also could have used a grouped Settings struct with days and units fields instead of managing those params separately. That is acceptable, but in general it is almost always better to derive smaller and more specific state values so you update precisely what needs to be updated. Doors is not built around a virtual DOM with diffs, and it compensates with rich state derivation mechanics.
Next: Active Link
Code
dashboard.gox
package main
import (
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
func WeatherDashboard(path doors.Source[Path]) gox.Comp {
city := doors.DeriveBeam(path, func(p Path) int {
return p.CityID
})
days := doors.DeriveBeam(path, func(p Path) int {
return p.days()
})
units := doors.DeriveBeam(path, func(p Path) driver.Units {
return p.units()
})
return city.Bind(elem(cityID int) {
~dashboard{
cityID: cityID,
days: days,
units: units,
}
})
}
type dashboard struct {
cityID int
days doors.Beam[int]
units doors.Beam[driver.Units]
}
elem (d dashboard) Main() {
~{
city, _ := driver.Locations.CitiesGet(d.cityID)
}
~(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>
})
~(d.change())
~(d.menu())
}
elem (d dashboard) change() {
~>(new(doors.Door)) <>
~{
days, _ := d.days.Effect(ctx)
units, ok := d.units.Effect(ctx)
}
~(if ok {
<a
class="secondary"
(doors.ALink{
Model: Path{
Route: Selector,
Days: daysQuery(days),
Units: unitsQuery(units),
},
})
role="button">
Change
</a>
})
</>
}
elem (d dashboard) menu() {
<nav>
~(d.units.Bind(d.daysNav))
~(d.days.Bind(d.unitNav))
</nav>
}
elem (d dashboard) daysNav(units driver.Units) {
<ul>
~(for i := range 7 {
~{
days := i + 1
}
<li>
<a
class="secondary"
(doors.ALink{
Model: Path{
Route: Dashboard,
CityID: d.cityID,
Days: daysQuery(days),
Units: unitsQuery(units),
},
})>
~(days)
~(if days == 1 {
day
} else {
days
})
</a>
</li>
})
</ul>
}
elem (d dashboard) unitNav(days int) {
<ul>
~(for _, units := range []driver.Units{driver.Metric, driver.Imperial} {
<li>
<a
class="secondary"
(doors.ALink{
Model: Path{
Route: Dashboard,
CityID: d.cityID,
Days: daysQuery(days),
Units: unitsQuery(units),
},
})>
~(units.String())
</a>
</li>
})
</ul>
}
app.gox
package main
import (
"context"
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
type Path struct {
Route Route `path:"/ | /:CityID"`
CityID int
Units *driver.Units `query:"units"`
Days *int `query:"days"`
}
func (p Path) days() int {
if p.Days == nil {
return 7
}
return min(max(*p.Days, 1), 7)
}
func daysQuery(days int) *int {
if days == 7 {
return nil
}
return &days
}
func (p Path) units() driver.Units {
if p.Units == nil || *p.Units != driver.Imperial {
return driver.Metric
}
return driver.Imperial
}
func unitsQuery(units driver.Units) *driver.Units {
if units == driver.Metric {
return nil
}
return &units
}
type Route int
const (
Selector Route = iota
Dashboard
)
type App struct{}
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.Route(
doors.RouteModel(a.content),
doors.RouteLocationDefaultComp(<>
<title>Not Found</title>
~(doors.Status(404))
<h1>Location Not Found</h1>
</>),
))
</main>
</body>
</html>
}
elem (a App) content(path doors.Source[Path]) {
~(path.Route(
doors.RouteMatch(func(p Path) bool {
return p.Route == Selector
}).Comp(<>
<title>Select Location</title>
~(LocationSelector(func(ctx context.Context, city int) {
path.Update(ctx, Path{
Route: Dashboard,
CityID: city,
})
}))
</>),
doors.RouteDefault(WeatherDashboard),
))
}
