Repository navigation
Doors 0.18 Release Notes — "Grace"
Doors 0.18 brings two main things:
- Call batching. A view that changes in several places changes once. Mark a handler
Batch: trueand the browser applies its updates in one step, while rendering stays parallel and non-blocking. - Nested routing. Sections, tabs, and variants fit in one route call and one container, however deep. Every route is complete as soon as it is written.
Highlights
Call batching
Every Door renders on its own and its update goes to the browser as soon as it is ready. That keeps pages responsive, and it shows when one action changes several places or replaces content that takes time to produce: the page changes piece by piece, a list refreshes before its counter, a view empties out and fills in.
Set Batch: true on a link (ALink) or on an event attr (AClick, ASubmit, AKeyDown, AHook, and the rest, next to Race), and the browser holds the handler's updates and applies them together when the handler finishes. The handler runs and renders exactly as before; only the paint is grouped. A link to a view that takes a moment to load keeps the current view intact until the new one is complete, then replaces it at once. A list, its counter, and its toolbar change in the same step:
~>doors.AClick{
Batch: true,
On: func(ctx context.Context, _ doors.RequestPointer) bool {
p.list.Inner(ctx, OrderList{status: status})
p.count.Inner(ctx, OrderCount{status: status})
return false
},
} <button>Show ~(status)</button>
The batch covers everything the handler starts with its context: Door updates, the state propagation they trigger, and path changes. Other calls are not held up by it unless they touch a Door the batch holds or content the batch is about to place; those land right after it, in order.
To batch updates yourself, anywhere, use doors.OnBatch(ctx, on, ops...): the updates made in ops are applied at once, and on runs when the browser has applied them. In a handler with Batch set, OnBatch joins the handler's batch instead of opening a new one:
doors.OnBatch(ctx, func(ctx context.Context) {
p.status.Inner(ctx, "Saved")
}, func(ctx context.Context) {
p.form.Inner(ctx, OrderForm{order: order})
p.list.Inner(ctx, OrderList{status: status})
})
Completion channels deliver a result only on a detached context (DetachedContext, InstanceContext, Go); on other contexts, including a handler ctx used from a goroutine, they close once the call is scheduled. doors.Call with Into needs the second value. To react after the batch, use OnBatch. Title and meta updates apply on arrival and are not batched; when held calls reach SolitairePending, the oldest batch is applied early. See Core Concepts and Door.
Nested routes in one container
A route call used to choose one branch, and each further level was another route call inside the branch content, with its own container. Now the levels nest in one call:
<>
~(doors.Route(
doors.RouteModel[Path](
doors.RouteMatch(
func(p Path) bool {
return p.Section == SectionDashboard
},
doors.RouteComp[Path](Dashboard{}),
),
doors.RouteMatch(
func(p Path) bool {
return p.Section == SectionSettings
},
doors.RouteBeamDerive(
func(path doors.Beam[Path]) doors.Beam[string] {
return doors.DeriveBeam(
path,
func(p Path) string {
return p.Tab
},
)
},
doors.RouteBeamValue("profile", doors.RouteComp[string](ProfileTab{})),
doors.RouteBeamValue("team", doors.RouteComp[string](TeamTab{})),
),
),
),
doors.RouteComp[doors.Location](NotFound{}),
))
</>
Going from /dashboard to /settings/team, or between tabs, replaces the container content once.
Leaves always match and act as the fallback of their list:
RouteComp[T](comp)renders a fixed componentRouteBind(render)renders from the value and again whenever it changesRouteSource(render)hands its content the writable sourceRouteBeam(render)hands its content a read-only beam
Groups match when their condition holds and one of their children matches:
RouteMatch(pred, routes...)andRouteValue(value, routes...)RouteDerive(derive, routes...)routes its children on a derived source, usually inside aRouteMatchRouteModel[M](routes...)routes its children on a path model decoded from theLocation
Each group has a read-only twin: RouteBeamMatch, RouteBeamValue, RouteBeamDerive, RouteBeamModel. See State and Routing.
Fall-through
A group whose children match nothing falls through to its next sibling. A URL that decodes into a path model but matches none of the routes inside its RouteModel reaches the 404 that follows.
Equal derived values
While a RouteDerive value stays equal by its comparator, the routes below it are not checked again, the same rule beams use to suppress equal updates. A URL change that decodes into an equal model doesn't reach the routes inside a RouteModel.
App-wide context values
doors.WithValue(key, value) makes ctx.Value(key) return the value in every context Doors creates: the page factory, renders, handlers, detached, instance and session contexts, and requests, including the *http.Request a hook or resource handler receives. A database pool or a service client reaches every component and handler without props. A value set for the same key inside Doors, with context.WithValue or doors.Ctx, still wins. doors.WithFunc(f) runs f once per app in NewApp and applies the option it returns, so one option passed to several apps creates a separate value for each.
type dbKey struct{}
func DB(ctx context.Context) *sql.DB {
return ctx.Value(dbKey{}).(*sql.DB)
}
app := doors.NewApp(page, doors.WithValue(dbKey{}, db))See Configuration.
Cached resource URLs in code
doors.NewResource(ctx, resource, contentType) returns the cached URL of a static resource, the one the cache attribute produces, so scripts and islands can load stylesheets, images, or fonts by URL. See Resources.
Portable resource URLs
Private resource URLs need the browser session cookie, so they fail when something other than the page fetches them: an Android WebView hands downloads to the system download manager, which sends no cookies, and apps or in-app browsers open links outside the page's cookie jar. The portable attribute gives a resource served through a hook (handler, hook, and proxy sources, private or nocache static resources, scripts and stylesheets) a URL that carries its own access:
<a href=(doors.ResourceHook(serveReport)) portable name="report.pdf" download>
Download report
</a>
The URL is a bearer link: anyone holding it can load the resource while the content that rendered it lives; it stops working when that content is cleared, the page instance or session ends, or a one-shot hook returns true. Requests through it create no session, and the handler sees the page's session like any hook. portable works with private and nocache; with cache it is an error. Handler-backed sources also accept private, which changes nothing for them. See Resources.
Behavior Changes
- One container per call.
Source.Route,Beam.RouteBeam, anddoors.Routerender a single Door however deep the routes nest, andRouteBindleaves no longer add a Door of their own. CSS that relied on the extra wrappers needs updating. - Branch selection across levels. A chosen branch stays mounted while every condition on its path holds, even if an earlier route would now match too. When one condition fails, the whole tree selects again from the top, as on the first render. A showing leaf gives way to an earlier route that starts matching; a leaf inside a
RouteDerivecounts as the derive's fallback. RouteValueuses the routed source's comparator instead of==and accepts anyT. A source with a nil comparator never matches aRouteValue; route it withRouteMatch.- Derive functions run once per render of the route container, for every
RouteDeriveandRouteBeamDerivein the tree, shown or not. - Hook completion waits for the hook's own page updates. The indicator release, the scope release, and the
$hookpromise now wait for updates the hook made if they are still queued in the browser. OnSettleand hook completion track the handler's own instance only. Updates the handler triggers in other instances, such as other tabs through shared state, are no longer awaited.- No goroutine per child context.
context.WithCancelandcontext.AfterFuncon a Doors context (handlers, detached and instance contexts, subscriptions) no longer start a goroutine each. - Hook handlers see the path after the hook URL. For a
NewHookURL or a handler resource,r.URL.Pathis the part after the hook URL, starting with/, instead of the full/~/...path;r.RequestURIkeeps the original and theDoors-Hook-Prefixheader holds the hook URL. Anhttp.ServeMuxcan route sub-paths of a hook, see Custom Endpoints. Handlers that parsed the full path need updating. - Hook calls carry their order in the
D0-Trackheader instead of thetquery parameter, so atparameter of your own reaches the handler. $hook(name)without an argument sendsnull.OnSettleandOnBatchaccept a nilon.
Breaking Changes
| Old | New |
|---|---|
RouteDefaultComp[T](comp) |
RouteComp[T](comp) |
RouteDefault(render) |
RouteSource(render) |
RouteDefaultBeam(render) |
RouteBeam(render) |
RouteDefaultBind(render) |
RouteBind(render) |
RouteMatch(pred).Comp/.Source/.Beam/.Bind(...) |
RouteMatch(pred, leaf) |
RouteValue(v).Comp/.Source/.Beam/.Bind(...) |
RouteValue(v, leaf) |
RouteDerive(f).Source(set, render) |
RouteMatch(pred, RouteDerive(derive, RouteSource(render))), with the lens built by DeriveSource |
RouteDerive(f).Beam(render), .Bind(render) |
RouteBeamMatch(pred, RouteBeamDerive(derive, leaf)), with the beam built by DeriveBeam |
RouteDeriveEqual(f, equal) |
DeriveSourceEqual or DeriveBeamEqual inside the derive function |
RouteModel(render) |
RouteModel[M](RouteSource(render)), or routes on M inside it |
RouteModelBeam(render) |
RouteBeamModel[M](RouteBeam(render)) |
RouteSource[T], RouteBeam[T] route types |
SourceRoute[T], BeamRoute[T] |
MatchRoute, DeriveRoute builders |
Removed |
Render functions returning any gox.Comp type |
Return gox.Elem |
Migration
go get github.com/doors-dev/doors@v0.18.0Batching is opt-in; handlers that do not set Batch behave as before. Routing needs the changes below.
Wrap each branch's content in a leaf and pass it to its group; the fallback becomes a plain leaf at the end:
<>
~(doors.Route(
doors.RouteModel[Path](doors.RouteSource(Page)),
doors.RouteComp[doors.Location](NotFound{}),
))
</>
A RouteDerive built from func(T1) (T2, bool) splits into a match for the bool and a derive for the value:
<>
~(location.Route(
doors.RouteMatch(
func(l doors.Location) bool {
_, ok := parseCustom(l)
return ok
},
doors.RouteDerive(
func(s doors.Source[doors.Location]) doors.Source[Custom] {
return doors.DeriveSource(
s,
func(l doors.Location) Custom {
c, _ := parseCustom(l)
return c
},
setCustom,
)
},
doors.RouteSource(CustomPage),
),
),
))
</>
Two more things to check:
- Routers inside pages. A page that calls
Routeagain inside its own content keeps working once its routes are converted. To render everything in one container instead, move those routes into the parentRoutecall as a group, as in the example above. RouteValueon sources made withNewSourceEqual,DeriveSourceEqual, orDeriveBeamEqual.RouteValuenow matches whenever that comparator calls the values equal. With a nil comparator it never matches; useRouteMatchwith==in the predicate instead.