Firelight.Observers
Reactive controllers for the browser's resize, intersection, mutation and performance observers.
Why use it
The browser's observers report things a component often needs to know: its own size, whether it's on screen, changes to its children. Using them directly means creating the observer when the component connects, disconnecting it when it's removed, and asking Lit to render when something changes.
Each controller here does that work. Give it a callback that turns the observer's entries into a
value, then read .value in render. CSS container queries can restyle a
component by its size; a ResizeController can also change what it renders.
Example
The panel picks its layout from its own width, not the window's. Drag its bottom-right corner to resize it.
module Snippets.ResizePanel
open Fable.Core
open Firelight
open Firelight.Observers
open type Firelight.Lit
/// Lays itself out by its own width, not the window's. Drag the bottom-right corner to resize it.
[<AttachMembers>]
type ResizePanel() as this =
inherit LitElement()
let width =
ResizeController<float>(
this,
ResizeControllerConfig(
callback =
ResizeValueCallback(fun entries _ ->
if entries.Length = 0 then
0.0
else
entries.[0].contentRect.width
)
)
)
static member styles =
css
$$"""
:host {
display: block; resize: horizontal; overflow: auto; box-sizing: border-box; width: 100%;
min-width: 10rem; max-width: 100%; padding: 1rem;
border: 1px dashed var(--border); border-radius: 0.5rem;
}
.boxes { display: grid; gap: 0.5rem; }
.wide .boxes { grid-template-columns: repeat(3, 1fr); }
.box { padding: 0.75rem; border-radius: 0.5rem; background: var(--accent); color: var(--accent-fg); text-align: center; }
"""
override _.render() =
let px = width.value |> Option.defaultValue 0.0
let layout = if px >= 300.0 then "wide" else "narrow"
html
$"""
<div class={layout}>
<p>{int px}px wide, so the <strong>{layout}</strong> layout</p>
<div class="boxes"><div class="box">One</div><div class="box">Two</div><div class="box">Three</div></div>
</div>"""
defineElement<ResizePanel> "my-resize-panel"
0px wide, so the narrow layout
Observe an element in the template
A controller observes its host unless told otherwise. To observe an element the component renders,
give the controller target = null and mark the element with a hole in its opening tag:
<div {controller.target ()}>. The element is observed while it's rendered. Scroll the box until
the marker shows:
module Snippets.ScrollMarker
open Fable.Core
open Firelight
open Firelight.Observers
open type Firelight.Lit
/// Says whether the marker inside its scrolling box is in view.
[<AttachMembers>]
type ScrollMarker() as this =
inherit LitElement()
// target = null: observe only the element that target () marks in the template, not the host.
let marker =
IntersectionController(
this,
IntersectionControllerConfig(
target = null,
callback = IntersectionValueCallback(fun entries _ -> entries |> Array.exists _.isIntersecting)
)
)
static member styles =
css
$$"""
:host { display: grid; gap: 0.5rem; }
p { margin: 0; }
.box { height: 7rem; overflow-y: auto; border: 1px solid var(--border, #ccc); border-radius: 0.5rem; padding: 0 0.75rem; }
.spacer { height: 14rem; padding-top: 0.5rem; }
.marker { margin-bottom: 0.75rem; padding: 0.4rem 0.75rem; border-radius: 0.4rem; background: #1971c2; color: white; }
"""
override _.render() =
let inView = marker.value = Some true
html
$"""
<p role="status">The marker is {if inView then "in view" else "out of view"}.</p>
<div class="box" tabindex="0" aria-label="Scrolling box">
<div class="spacer">Scroll down.</div>
<div class="marker" {marker.target ()}>Marker</div>
</div>"""
defineElement<ScrollMarker> "my-scroll-marker"
The marker is out of view.
ResizeController.target is Lit's own directive. @lit-labs/observers has none for
IntersectionController, so Firelight's is Lit's ref with one callback per controller: it
observes the element a render puts there, and stops observing one a render removes. That makes it
one element per controller. Put target () on two, and Lit moves the ref between them on every
render, so only the last is observed, and the component renders again and again. For several
elements, give each its own controller.
Install
dotnet add package Firelight.Observers
npm install @lit-labs/observers