Optimization
Right now, changing either query parameter updates the whole dashboard, including the menu and all charts:

That is more work than we need. Some parts of the UI do not depend on every setting, and some charts do not depend on units at all.
Derive
Compared to common front-end stacks, Doors is not built around a virtual DOM with diffs. It swaps dynamic container content directly.
To reduce the update surface, derive smaller beams from shared state and subscribe only where that specific value is needed.
This is the same
SourceandBeamcomposition model described in State.
Derive separate days and units beams:
dashboard.gox
// dashboard constructor
func Dashboard(c int, settings doors.Beam[Settings]) gox.Comp {
// separate settings beams
days := doors.NewBeam(settings, func (s Settings) int {
return s.Days
})
units := doors.NewBeam(settings, func (s Settings) driver.Units {
return s.Units
})
return dashboard{
city: c,
settings: settings,
days: days,
units: units,
}
}
type dashboard struct {
city int
settings doors.Beam[Settings]
days doors.Beam[int]
units doors.Beam[driver.Units]
}
Also update the dashboard render site:
app.gox
elem (a App) Main() {
~/* ... */
<main class="container">
~(doors.Sub(a.city, elem(city int) {
~(if city == -1 {
~/* ... */
} else {
~(Dashboard(city, a.settings))
})
}))
</main>
~/* ... */
}
Subscribe
Now each chart can depend only on the settings it actually uses:
dashboard.gox
elem (d dashboard) charts(c driver.City) {
<div class="grid">
<div>
~(doors.Sub(d.settings, elem(s Settings) {
~chart{
title: "Temperature",
svg: func() []byte {
values, _ := driver.Weather.Temperature(ctx, c, s.Units, s.Days)
svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.Temperature())
return svg
},
}
}))
~(doors.Sub(d.days, elem(days int) {
~chart{
title: "Humidity",
svg: func() []byte {
values, _ := driver.Weather.Humidity(ctx, c, days)
svg, _ := driver.ChartLine(values.Values, values.Labels, "%")
return svg
},
}
}))
</div>
<div>
~(doors.Sub(d.days, elem(days int) {
~chart{
title: "Weather",
svg: func() []byte {
values, _ := driver.Weather.Code(ctx, c, days)
svg, _ := driver.ChartPie(values.Values)
return svg
},
}
}))
~(doors.Sub(d.settings, elem(s Settings) {
~chart{
title: "Wind Speed",
svg: func() []byte {
values, _ := driver.Weather.WindSpeed(ctx, c, s.Units, s.Days)
svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.WindSpeed())
return svg
},
}
}))
</div>
</div>
}
The menu can be split the same way. It does not need to rebuild every link on every settings change:
elem (d dashboard) menu() {
~(doors.Sub(d.settings, elem(s Settings) {
<a
class="secondary"
(doors.ALink{
Model: Path{
Home: true,
Days: s.DaysQuery(),
Units: s.UnitsQuery(),
},
})
role="button">
Change
</a>
}))
<nav>
~(doors.Sub(d.units, d.navDays))
~(doors.Sub(d.days, d.navUnits))
</nav>
}
elem (d dashboard) navDays(u driver.Units) {
~{
s := Settings{Units: u}
}
~/* ... */
}
elem (d dashboard) navUnits(days int) {
~{
s := Settings{Days: days}
}
~/* ... */
}
Finally, move the subscriptions into menu and charts:
elem (d dashboard) Main() {
~{
city, _ := driver.Locations.CitiesGet(d.city)
}
~(d.header(city))
~// subscriptions now live inside menu and charts
<section>
~(d.menu())
</section>
~(if city.IsValid() {
<section>
~(d.charts(city))
</section>
})
}

Now the unaffected charts stay in place, but the loader still appears everywhere. That is better, but not ideal.
Indication
The indicator can be more specific too.
See Indication for the selector helpers used here.
First, add one more field to chart:
type chart struct {
title string
svg func() []byte
units bool
}
elem (c chart) Main() {
<article>
<header>
~(c.title, " ")
<span
class=func {
if c.units {
return "chart-loader chart-loader-units"
}
return "chart-loader"
}>
</span>
</header>
/* ... */
</article>
}
funcis specialGoXliteral syntax that is executed in place.
Pass true for charts that depend on units:
elem (d dashboard) charts(c driver.City) {
<div class="grid">
<div>
~(doors.Sub(d.settings, elem(s Settings) {
~chart{
/* ... */
units: true,
}
}))
~(doors.Sub(d.days, elem(days int) {
~chart{
/* ... */
units: false,
}
}))
</div>
<div>
~(doors.Sub(d.days, elem(days int) {
~chart{
/* ... */
units: false,
}
}))
~(doors.Sub(d.settings, elem(s Settings) {
~chart{
/* ... */
units: true,
}
}))
</div>
</div>
}
Then narrow the indication rules:
elem (d dashboard) navDays(u driver.Units) {
/* ... */
<a
class="secondary"
(doors.ALink{
Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader", "aria-busy", "true"),
/* ... */
})>
~/* ... */
</a>
/* ... */
}
elem (d dashboard) navUnits(days int) {
/* ... */
<a
class="secondary"
(doors.ALink{
Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader-units", "aria-busy", "true"),
/* ... */
})>
~(unit.String())
</a>
/* ... */
}
After the indication fix, only the affected charts show loading state:

Lazy
Querying weather data takes some time, so the initial dashboard load is still a bit slower than it needs to be.

We can improve the perceived loading time by generating the SVG in parallel:
This pattern uses Door, using
Replaceto swap in the content when it is ready.
elem (c chart) Main() {
<article>
<header>
~/* ... */
</header>
<div class="img-wrapper">
~{
// create dynamic component
door := new(doors.Door)
}
~(door)
~{
// replace it with the image when it is ready
door.Replace(ctx, <img height="auto" width="100%" src=(svg()) type="image/svg+xml"/>)
}
<div class="img-loader" aria-busy="true"></div>
</div>
</article>
}
It is cleaner to extract that into a helper:
// gox.Comp is broader than gox.Elem
elem Lazy(content gox.Comp) {
~{
door := new(doors.Door)
}
~(door)
~{
door.Replace(ctx, content)
}
}
elem (c chart) Main() {
<article>
~/* ... */
<div class="img-wrapper">
~(Lazy(<img height="auto" width="100%" src=(c.svg()) type="image/svg+xml"/>))
<div class="img-loader" aria-busy="true"></div>
</div>
</article>
}
Result:

Next: Authentication
Code
dashboard.gox
package main
import (
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
"github.com/doors-dev/tutorial/driver"
)
func Dashboard(c int, settings doors.Beam[Settings]) gox.Comp {
days := doors.NewBeam(settings, func(s Settings) int {
return s.Days
})
units := doors.NewBeam(settings, func(s Settings) driver.Units {
return s.Units
})
return dashboard{
city: c,
settings: settings,
days: days,
units: units,
}
}
type dashboard struct {
city int
settings doors.Beam[Settings]
days doors.Beam[int]
units doors.Beam[driver.Units]
}
elem (d dashboard) Main() {
~{
city, _ := driver.Locations.CitiesGet(d.city)
}
~(d.header(city))
<section>
~(d.menu())
</section>
~(if city.IsValid() {
<section>
~(d.charts(city))
</section>
})
}
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() {
~(doors.Sub(d.settings, elem(s Settings) {
<a
class="secondary"
(doors.ALink{
Model: Path{
Home: true,
Days: s.DaysQuery(),
Units: s.UnitsQuery(),
},
})
role="button">
Change
</a>
}))
<nav>
~(doors.Sub(d.units, d.navDays))
~(doors.Sub(d.days, d.navUnits))
</nav>
}
elem (d dashboard) navDays(units driver.Units) {
~{
s := Settings{Units: units}
}
<ul>
~(for i := range 7 {
~{
s.Days = i + 1
}
<li>
<a
class="secondary"
(doors.ALink{
Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader", "aria-busy", "true"),
Active: doors.Active{
Indicator: doors.IndicatorOnlyAttr("aria-current", "true"),
},
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(days int) {
~{
s := Settings{Days: days}
}
<ul>
~(for _, unit := range []driver.Units{driver.Metric, driver.Imperial} {
<li>
~{
s.Units = unit
}
<a
class="secondary"
(doors.ALink{
Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader-units", "aria-busy", "true"),
Active: doors.Active{
Indicator: doors.IndicatorOnlyAttr("aria-current", "true"),
},
Model: Path{
Dashboard: true,
CityID: d.city,
Days: s.DaysQuery(),
Units: s.UnitsQuery(),
},
})>
~(unit.String())
</a>
</li>
})
</ul>
}
elem (d dashboard) charts(c driver.City) {
<div class="grid">
<div>
~(doors.Sub(d.settings, elem(s Settings) {
~chart{
title: "Temperature",
svg: func() []byte {
values, _ := driver.Weather.Temperature(ctx, c, s.Units, s.Days)
svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.Temperature())
return svg
},
units: true,
}
}))
~(doors.Sub(d.days, elem(days int) {
~chart{
title: "Humidity",
svg: func() []byte {
values, _ := driver.Weather.Humidity(ctx, c, days)
svg, _ := driver.ChartLine(values.Values, values.Labels, "%")
return svg
},
units: false,
}
}))
</div>
<div>
~(doors.Sub(d.days, elem(days int) {
~chart{
title: "Weather",
svg: func() []byte {
values, _ := driver.Weather.Code(ctx, c, days)
svg, _ := driver.ChartPie(values.Values)
return svg
},
units: false,
}
}))
~(doors.Sub(d.settings, elem(s Settings) {
~chart{
title: "Wind Speed",
svg: func() []byte {
values, _ := driver.Weather.WindSpeed(ctx, c, s.Units, s.Days)
svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.WindSpeed())
return svg
},
units: true,
}
}))
</div>
</div>
}
type chart struct {
title string
svg func() []byte
units bool
}
elem (c chart) Main() {
<article>
<header>
~(c.title, " ")
<span
class=func {
if c.units {
return "chart-loader chart-loader-units"
}
return "chart-loader"
}>
</span>
</header>
<div class="img-wrapper">
~(Lazy(<img height="auto" width="100%" src=(c.svg()) type="image/svg+xml"/>))
<div class="img-loader" aria-busy="true"></div>
</div>
</article>
}
elem Lazy(content gox.Comp) {
~{
door := new(doors.Door)
}
~(door)
~{
door.Replace(ctx, content)
}
}
app.gox
package main
import (
"context"
_"embed"
"github.com/doors-dev/doors"
"github.com/doors-dev/gox"
)
//go:embed style.css
var styles []byte
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">
<link href=(styles) rel="stylesheet" cache>
</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, a.settings))
})
}))
</main>
</body>
</html>
}