Optimization

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

Dashboard before granular optimization

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 Source and Beam composition 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>
	})
}

Only affected charts rerender while loaders still appear everywhere

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>
}

func is special GoX literal 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: Scoped chart loaders after the indication fix

Lazy

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

Dashboard load before lazy rendering

We can improve the perceived loading time by generating the SVG in parallel:

This pattern uses Door, using Replace to 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: Lazy chart rendering with simulated throttling


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>
}