Events
In Doors, DOM events are handled through special attributes that connect a browser event to a Go handler.
Around that handler, you also manage client-side scheduling, pending indication, and follow-up actions.
Start
Most event attrs look like doors.AClick, doors.AInput, or doors.ASubmit[T].
The smallest useful example is:
<button
(doors.AClick{
On: func(ctx context.Context, r doors.RequestEvent[doors.PointerEvent]) bool {
return false
},
})>
Click
</button>
The core shape is still the same: an event attr has an On handler and that handler returns bool.
Return:
falseto keep the handler activetrueto mark it done and remove it
For normal DOM events, false is the common default.
Attach
Event attributes are regular Go values with names like doors.AClick, doors.AInput, or doors.ASubmit[T].
You can attach them in two ways.
As an attribute modifier:
<button
(doors.AClick{
On: func(ctx context.Context, r doors.RequestEvent[doors.PointerEvent]) bool {
return false
},
})>
Click
</button>
Or as a proxy:
<>
~>doors.AClick{
On: func(ctx context.Context, r doors.RequestEvent[doors.PointerEvent]) bool {
return false
},
} <button>Click</button>
</>
The modifier form attaches directly to the element you are editing.
The proxy form walks through the following subtree until it reaches the real rendered element and attaches there. That is useful when the final element is inside another component.
In general, there is no strict rule that says you should use a proxy only when you need to drill into a component to find the target. Use proxy syntax everywhere if you prefer it.
Handler
For normal DOM events, the handler receives doors.RequestEvent[T].
That gives you:
r.Event()for the typed event payloadr.SetCookie(...)andr.GetCookie(...)r.After(...)to schedule client-side actions after the request succeeds and all triggered DOM changes are applied
Example:
On: func(ctx context.Context, r doors.RequestEvent[doors.PointerEvent]) bool {
r.After(doors.ActionOnlyScroll("#top", true))
return false
}
Form handlers use:
doors.RequestForm[T]for decoded form datadoors.RequestRawFormfor raw multipart access
Options
Most event attrs share the same request-lifecycle fields:
On: backend handlerScope: request scheduling rules, covered in ScopesIndicator: temporary client-side feedback, covered in IndicationBefore: client-side actions before the requestOnError: client-side actions if the request fails
After is different: it is not an attribute field. You schedule it from inside the handler with r.After(...).
Some event families also add browser-event options such as:
PreventDefaultStopPropagationExactTargetFilterExcludeValue
Not every event family supports every one of these options.
Flow
When an event fires, the client/runtime flow is roughly:
- capture the browser event and build the payload
- apply client-side event options such as
PreventDefault,StopPropagation,ExactTarget, or keyFilter - run client-side scopes
- start indication
- run any
Beforeactions - send the request to the server
- run the Go handler
- run
Afteractions if the request succeeds - run
OnErroractions if the request fails
That is why scopes and indication feel immediate: they start on the client before the server finishes the request.
Pointer
Pointer attributes include:
doors.AClickdoors.APointerDowndoors.APointerUpdoors.APointerMovedoors.APointerOverdoors.APointerOutdoors.APointerEnterdoors.APointerLeavedoors.APointerCanceldoors.AGotPointerCapturedoors.ALostPointerCapture
Example:
<button
(doors.AClick{
PreventDefault: true,
On: func(ctx context.Context, r doors.RequestEvent[doors.PointerEvent]) bool {
x := r.Event().PageX
y := r.Event().PageY
_ = x
_ = y
return false
},
})>
Track click
</button>
The pointer payload includes the usual browser pointer fields, including coordinates, button state, pointer type, pressure, and timestamp.
Keyboard
Keyboard attributes are:
doors.AKeyDowndoors.AKeyUp
Use Filter to limit by event.key:
<input
(doors.AKeyDown{
Filter: []string{"Enter"},
On: func(ctx context.Context, r doors.RequestEvent[doors.KeyboardEvent]) bool {
return false
},
})/>
The keyboard payload includes Key, Code, Repeat, and modifier state such as CtrlKey, ShiftKey, AltKey, and MetaKey.
Focus
Focus attributes are:
doors.AFocusdoors.ABlurdoors.AFocusIndoors.AFocusOut
Use AFocusIn and AFocusOut when bubbling behavior matters.
Use AFocus and ABlur for the plain focus events.
Input
Input-related attributes are:
doors.AInputdoors.AChange
AInput fires as the user edits.
AChange fires when the value is committed.
<input
type="text"
(doors.AInput{
On: func(ctx context.Context, r doors.RequestEvent[doors.InputEvent]) bool {
value := r.Event().Value
_ = value
return false
},
})/>
The input and change payloads include browser-style fields such as:
NameValueNumberDateSelectedChecked
AInput{ExcludeValue: true} omits the normal input-derived fields from the payload. Use it when you want the event itself without sending the current input value data.
Forms
Form submission attributes are:
doors.ASubmit[T]doors.ARawSubmit
ASubmit[T] parses the multipart form and decodes it into your Go type.
type LoginForm struct {
Email string `form:"email"`
Code string `form:"code"`
}
<form
(doors.ASubmit[LoginForm]{
On: func(ctx context.Context, r doors.RequestForm[LoginForm]) bool {
data := r.Data()
_ = data
return false
},
})>
<input name="email"/>
<input name="code"/>
<button>Send</button>
</form>
Use ARawSubmit when you want direct multipart access for streaming, custom parsing, or uploads.
For form decoding, Doors uses go-playground/form v4.
Reuse
Use doors.A(ctx, ...) when you want to prepare one activated attribute value and reuse it.
<>
~{
radio := doors.A(ctx, doors.AChange{
On: func(ctx context.Context, r doors.RequestEvent[doors.ChangeEvent]) bool {
return false
},
})
}
<input type="radio" name="pick" value="a" (radio)/>
<input type="radio" name="pick" value="b" (radio)/>
</>
For a one-off attribute on one element, you usually do not need doors.A(...).
Each activated event attr has its own backend hook instance.
Calls to that same instance are serialized, so rapid repeated events on one active handler do not run concurrently on the backend.
If you reuse one activated attr across several elements, those elements also share the same hook instance and the same execution queue.
Unsupported
If the browser event you need is not supported by the built-in doors.A... event attributes, wire it yourself in JavaScript and call a custom hook.
That is the normal extension path for:
- browser events Doors does not expose directly
- custom DOM integrations
- third-party widgets that already have their own client-side event system
The usual pattern is:
- listen to the event in JavaScript
- call
$hook(...)or$fetch(...) - handle it on the Go side with
doors.AHook[...]ordoors.ARawHook
Example:
<script
(doors.AHook[string]{
Name: "visibility",
On: func(ctx context.Context, r doors.RequestHook[string]) (any, bool) {
println(r.Data())
return nil, false
},
})>
document.addEventListener("visibilitychange", async () => {
await $hook("visibility", document.visibilityState)
})
</script>
See JavaScript.
Rules
- Use the
ctxthat Doors gives you in the handler. - Use
falseas the normal return value unless the handler really should be one-shot. - Use
AInputfor live edits andAChangefor committed values. - Use
ASubmit[T]when typed decoding is enough; useARawSubmitfor upload-heavy or custom multipart flows. - Use scopes for interaction policy instead of rebuilding debounce/blocking logic by hand.
- Use indication when the user needs immediate feedback before the server responds.