Path and Title
Path
Currently, our page only serves the root path.
Add a CityID parameter to the path variant declaration to enable city selection via the path.
This is part of the Path Model.
main.go
type Path struct {
Home bool `path:"/"`
Dashboard bool `path:"/:CityID"` // new path variant
CityID int
}
You can add as many path variants as you like. The matched one will have the
truevalue.
Subscription
Derive a separate piece with the city id and provide it to the page. The page content will depend on it:
The current path is reactive too, so this uses the same State ideas on top of the Path Model.
main.go
func main() {
r := doors.NewRouter()
doors.UseModel(r, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
// derive beam with the city id
city := doors.NewBeam(s, func(p Path) int {
// city is not selected
if p.Home {
return -1
}
return p.CityID
})
return doors.ResponseComp(App{path: s, city: city})
})
if err := http.ListenAndServe(":8080", r); err != nil {
panic(err)
}
}
Write a stub dashboard component that accepts city id:
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
}
elem (d dashboard) Main() {
~{
city, _ := driver.Locations.CitiesGet(d.city)
}
~(if !city.IsValid() {
~(doors.Status(404))
<h1>City Not Found</h1>
} else {
<h1>Weather in ~(city.Name, ", ", city.Country.Name)</h1>
})
~// reset selection
<a
class="secondary"
(doors.ALink{
Model: Path{Home: true},
})
role="button">
Change
</a>
}
doors.Statusis a special utility to set the page HTTP status code. It affects only the initial render.
And subscribe the app body to it:
app.gox
elem (a App) Main() {
~/* ... */
<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,
}
}
})
}))
</main>
~/* ... */
}
Title
The title tag, and also meta, is processed differently from other tags. Regardless of where you put it, it is moved to <head> and synchronized with the frontend.
For
<title>,<meta>, anddoors.Status(...), see Head and Status.
dashboard.gox
elem (d dashboard) Main() {
~/* ... */
~(if !city.IsValid() {
~(doors.Status(404))
<title>Not Found</title>
<h1>City Not Found</h1>
} else {
<title>~(city.Name)</title>
<h1>Weather in ~(city.Name, ", ", city.Country.Name)</h1>
})
~/* ... */
}
location_selector.gox
elem (l locationSelector) Main() {
~/* ... */
<title>Select Location</title>
~/* ... */
}
This feels unusual at first, but it is actually very convenient, because you can update the title where you query the data.

Next: Scopes
Code
main.go
package main
import (
"net/http"
"github.com/doors-dev/doors"
)
type Path struct {
Home bool `path:"/"`
Dashboard bool `path:"/:CityID"`
CityID int
}
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
})
return doors.ResponseComp(App{path: s, city: city})
})
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]
}
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,
}
})
}))
</main>
</body>
</html>
}
./location_selector.gox
package main
import (
"context"
"time"
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
type location struct {
country driver.Place
city driver.Place
}
func LocationSelector(update func(ctx context.Context, city int)) gox.Comp {
loc := doors.NewSource(location{})
city := doors.NewBeam(loc, func(l location) driver.Place {
return l.city
})
country := doors.NewBeam(loc, func(l location) driver.Place {
return l.country
})
return locationSelector{
location: loc,
city: city,
country: country,
update: update,
}
}
type locationSelector struct {
update func(ctx context.Context, city int)
location doors.Source[location]
country doors.Beam[driver.Place]
city doors.Beam[driver.Place]
}
elem (l locationSelector) Main() {
<article>
<title>Select Location</title>
<section>
~placeSelector{
title: "Country",
options: new(doors.Door),
search: driver.Locations.CountriesSearch,
selected: l.country,
update: func(ctx context.Context, place driver.Place) {
l.location.Update(ctx, location{
country: place,
})
},
}
</section>
~(doors.Sub(l.country, l.selectCity))
~(doors.Sub(l.city, l.submit))
</article>
}
elem (l locationSelector) selectCity(country driver.Place) {
~(if country.IsValid() {
<section>
~placeSelector{
title: "City",
options: new(doors.Door),
search: func(input string) ([]driver.Place, error) {
return driver.Locations.CitiesSearch(country.Id, input)
},
selected: l.city,
update: func(ctx context.Context, place driver.Place) {
l.location.Mutate(ctx, func(l location) location {
l.city = place
return l
})
},
}
</section>
})
}
elem (l locationSelector) submit(city driver.Place) {
~(if city.IsValid() {
<hr/>
<a
(doors.AClick{
Indicator: doors.IndicatorOnlyAttr("aria-busy", "true"),
PreventDefault: true,
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.update(ctx, city.Id)
return false
},
})
id="submit"
href="#"
role="button">
Confirm
</a>
~(focus("submit"))
})
}
type placeSelector struct {
title string
options *doors.Door
search func(input string) ([]driver.Place, error)
selected doors.Beam[driver.Place]
update func(ctx context.Context, place driver.Place)
}
func (l placeSelector) Main() gox.Elem {
return doors.Sub(l.selected, func(p driver.Place) gox.Elem {
if p.IsValid() {
return l.place(p)
}
return l.input()
}).Main()
}
elem (l placeSelector) place(p driver.Place) {
<h3>~(l.title): <b>~(p.Name)</b></h3>
<button
(doors.AClick{
Indicator: doors.IndicatorOnlyAttr("aria-busy", "true"),
Scope: doors.ScopeOnlyBlocking(),
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.update(ctx, driver.Place{})
return true
},
})
class="secondary">
Change
</button>
}
elem (l placeSelector) input() {
~{
loaderID := "loader-" + doors.IDString(l.title)
inputID := "input-" + doors.IDString(l.title)
}
<h3>Select ~(l.title)<span id=(loaderID)></span></h3>
<input
id=(inputID)
(doors.AInput{
Scope: doors.ScopeOnlyDebounce(300 * time.Millisecond, 600 * time.Millisecond),
Indicator: doors.IndicatorOnlyAttrQuery("#" + loaderID, "aria-busy", "true"),
On: func(ctx context.Context, r doors.RequestInput) bool {
l.options.Update(ctx, l.results(r.Event().Value))
return false
},
})
type="search"
placeholder=(l.title)
autocomplete="off"/>
~(focus(inputID))
~{
l.options.Clear(ctx)
}
~(l.options)
}
func (l placeSelector) results(input string) gox.Elem {
if len(input) == 0 {
return nil
}
if len(input) < 2 {
return <p>
<mark>Type at least two letters to search</mark>
</p>
}
results, _ := l.search(input)
if len(results) == 0 {
return <p>
<i>nothing found</i>
</p>
}
scope := doors.ScopeOnlyBlocking()
return <ul>
~(for _, place := range results {
<li>
<a
(doors.AClick{
PreventDefault: true,
Scope: scope,
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.update(ctx, place)
return true
},
}, doors.AKeyDown{
Scope: scope,
Filter: []string{"Enter"},
On: func(ctx context.Context, _ doors.RequestKeyboard) bool {
l.update(ctx, place)
return true
},
})
href="#">
~(place.Name)
</a>
</li>
})
</ul>
}
elem focus(id string) {
<script data:id=(id)>
const id = $data("id")
const el = document.getElementById(id)
el.focus()
</script>
}
./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
}
elem (d dashboard) Main() {
~{
city, _ := driver.Locations.CitiesGet(d.city)
}
~(if !city.IsValid() {
~(doors.Status(404))
<title>Not Found</title>
<h1>City Not Found</h1>
} else {
<title>~(city.Name)</title>
<h1>Weather in ~(city.Name, ", ", city.Country.Name)</h1>
})
<a
class="secondary"
(doors.ALink{
Model: Path{Home: true},
})
role="button">
Change
</a>
}