Advanced Scope Usage
In practice, the submit handler will not respond instantly. What happens if we interact with the UI while it is still processing?
Let’s simulate conflicting actions:
location_selector.gox
elem (l locationSelector) submit(city driver.Place) {
/* ... */
<button
(doors.AClick{
Indicator: doors.IndicateAttr("aria-busy", "true"),
On: func(ctx context.Context, _ doors.RequestPointer) bool {
// Add delay.
<-time.After(1 * time.Second)
l.apply(ctx, city.Id)
return true
},
})
id="confirm">
Confirm
</button>
/* ... */
}

Nothing catastrophic happens if we leave it like this, but the interaction can be polished further with Concurrent Scope from the Scopes API.
For the full scope model, see Scopes.
Concurrent
A concurrent scope can be occupied only by events with the same group id.
Add a concurrent scope parent to the location selector:
func LocationSelector(apply func(ctx context.Context, city int)) gox.Comp {
/* ... */
return locationSelector{
/* ... */
scope: new(doors.ScopeConcurrent),
}
}
type locationSelector struct {
/* ... */
scope *doors.ScopeConcurrent
}
Add a scope field to the place selector:
type placeSelector struct {
/* ... */
scope doors.Scopes
}
Then add it in the existing scope pipeline:
elem (l placeSelector) place(p driver.Place) {
<h3>~(l.title): <b>~(p.Name)</b></h3>
<button
(doors.AClick{
Indicator: doors.IndicateAttr("aria-busy", "true"),
// Now the event needs to go through two scopes.
Scope: new(doors.ScopeBlocking).And(l.scope),
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.selected.Update(ctx, driver.Place{})
return true
},
})
class="secondary">
Change
</button>
}
Provide concurrent scope group 1 to both place selectors:
elem (l locationSelector) Main() {
/* ... */
<section>
~placeSelector{
/* ... */
scope: l.scope.Scope(1),
}
</section>
/* ... */
}
elem (l locationSelector) selectCity(country driver.Place) {
~(if country.IsValid() {
<section>
~placeSelector{
/* ... */
scope: l.scope.Scope(1),
}
</section>
})
}
Finally, apply the concurrent scope to the submit button with a different group id:
elem (l locationSelector) submit(city driver.Place) {
~/* ... */
<button
(doors.AClick{
Indicator: doors.IndicateAttr("aria-busy", "true"),
Scope: l.scope.Scope(0),
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.apply(ctx, city.Id)
return true
},
})
id="confirm">
Confirm
</button>
~/* ... */
}
The Scopes API is purely optional and is usually used at later stages to give the UI a more polished feel. Most of the time,
doors.ScopeBlockingis already enough.
Result
This setup ensures that either the submit action or the change-place action can run, but not both at the same time:

At the same time, changes to country and city do not block each other, because they share the same concurrent group:

Next: Query
Code
./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(apply func(ctx context.Context, city int)) gox.Comp {
loc := doors.NewSource(location{})
country := doors.DeriveSource(loc,
func(l location) driver.Place {
return l.country
},
func(_ location, c driver.Place) location {
return location{
country: c,
}
},
)
city := doors.DeriveSource(loc,
func(l location) driver.Place {
return l.city
},
func(l location, c driver.Place) location {
l.city = c
return l
},
)
return locationSelector{
location: loc,
city: city,
country: country,
apply: apply,
scope: new(doors.ScopeConcurrent),
}
}
type locationSelector struct {
location doors.Source[location]
country doors.Source[driver.Place]
city doors.Source[driver.Place]
apply func(ctx context.Context, city int)
scope *doors.ScopeConcurrent
}
elem (l locationSelector) Main() {
<article>
<section>
~placeSelector{
title: "Country",
options: new(doors.Door),
search: driver.Locations.CountriesSearch,
selected: l.country,
scope: l.scope.Scope(1),
}
</section>
~(l.country.Bind(l.selectCity))
~(l.city.Bind(l.submit))
</article>
}
elem (l locationSelector) submit(city driver.Place) {
~(if city.IsValid() {
<hr/>
<button
(doors.AClick{
Indicator: doors.IndicateAttr("aria-busy", "true"),
Scope: l.scope.Scope(0),
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.apply(ctx, city.Id)
return true
},
})
id="confirm">
Confirm
</button>
~(focus("confirm"))
})
}
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)
},
scope: l.scope.Scope(1),
selected: l.city,
}
</section>
})
}
type placeSelector struct {
title string
options *doors.Door
search func(input string) ([]driver.Place, error)
selected doors.Source[driver.Place]
scope doors.Scopes
}
elem (l placeSelector) Main() {
~(l.selected.Bind(elem(p driver.Place) {
~(if p.IsValid() {
~(l.place(p))
} else {
~(l.input())
})
}))
}
elem (l placeSelector) place(p driver.Place) {
<h3>~(l.title): <b>~(p.Name)</b></h3>
<button
(doors.AClick{
Indicator: doors.IndicateAttr("aria-busy", "true"),
Scope: new(doors.ScopeBlocking).And(l.scope),
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.selected.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.ScopeDebounce{
Duration: 300 * time.Millisecond,
Limit: 600 * time.Millisecond,
},
Indicator: doors.IndicateAttrQuery("#" + loaderID, "aria-busy", "true"),
On: func(ctx context.Context, r doors.RequestInput) bool {
l.options.Inner(ctx, l.results(r.Event().Value))
return false
},
})
type="search"
placeholder=(l.title)
autocomplete="off"/>
~(focus(inputID))
~{
l.options.Inner(ctx, nil)
}
~(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 := new(doors.ScopeBlocking)
return <ul>
~(for _, place := range results {
<li>
<a
(doors.AClick{
Scope: scope,
PreventDefault: true,
On: func(ctx context.Context, _ doors.RequestPointer) bool {
l.selected.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>
}