Web APIs from MDN
Every web API the browser ships, in C#, as MDN names it. The Rask.Web package is generated from MDN's own
data — the same snapshot Rask's elements and events come from — so an MDN example ports line by line:
await Navigator.Clipboard.WriteText("hi"); // navigator.clipboard.writeText("hi")
var dark = await Window.MatchMedia("(prefers-color-scheme: dark)").Matches;
await LocalStorage.SetItem("theme", "dark");
var page = await Document.VisibilityState; // an MDN enum: DocumentVisibilityState.Visible
The globals are the window's own: Window for its members (MatchMedia, Fetch, Atob), and one each for what
it holds — Navigator, Document, Location, History, Screen, LocalStorage, SessionStorage, Performance,
Crypto, IndexedDB, Caches, CookieStore and the rest. Every interface they reach is a C# type in
Rask.Web.Types (Clipboard, MediaQueryList, Geolocation), and MDN's option dictionaries and enums are records
and enums (ShareData, DocumentVisibilityState).
Imported for you in every Server and WASM app. Both hosts depend on Rask.Web, which adds global using Rask.Web;
and the alias global using Types = Rask.Web.Types; — so the globals are bare and a field names an MDN type briefly:
private Types.IntersectionObserver? _io;
Rask.Web.Types itself is not imported: its interfaces share names with the globals (Navigator is both the global
and the interface it holds), so the samples on this page have no using at all. A component library references
Rask alone and gets neither; one that calls web APIs references Rask.Web and gets both.
One round trip per await
A chain is a path, recorded as you write it and run in the browser once when you await. Nothing crosses the wire
before that, so Navigator.Clipboard costs nothing on its own:
- An object is the next step of the path:
Navigator.Clipboard,Window.MatchMedia(q). - A value is read at the end of it:
await Navigator.Language,await Window.MatchMedia(q).Matches. - A write is
Set{Name}:await Window.SetName("checkout"). What the page renders has none: the page'sHeadsets the title, so there isawait Document.Titlebut noSetTitle. - A method runs there, and a promise is awaited there:
await Navigator.Clipboard.ReadText().
Keeping an object
Awaiting an object keeps it: the browser holds it for you, and every member you call on it starts from it rather
than from the window. Dispose of it when you are done — await using does that for you:
await using var wide = await Window.MatchMedia("(min-width: 800px)"); // kept: the same MediaQueryList each time
var matches = await wide.Matches;
A method whose promise resolves to an object (Navigator.MediaDevices.GetUserMedia(…)) keeps that object the same
way, and so does an attribute that is such a promise — the service worker's ready, which is where a Web Push
subscription starts:
await using var worker = await Navigator.ServiceWorker.Ready; // settles once the page's worker is active
await using var subscription = await worker.PushManager.Subscribe(new()
{
UserVisibleOnly = true,
ApplicationServerKey = Base64Url.DecodeFromChars(vapidPublicKey), // System.Buffers.Text
});
var json = await subscription.ToJSON(); // MDN's PushSubscriptionJSON: what /_rask/push/subscribe and Push.Subscribe keep
A method that returns an object at once is a step of the path like any other, so it runs when the path does — once
per await. Window.MatchMedia(q).Matches wants exactly that; a method you call for what it does, like
Performance.Mark("start"), wants awaiting on its own, which runs it once and keeps what it returns:
await using var mark = await Performance.Mark("start");.
A method that takes an object, and a writable attribute that holds one, take one you kept, which crosses as its handle —
or null, where MDN allows it. A path you have not awaited is refused with "await it first":
await using var hello = await SpeechSynthesisUtterance.Create("Hello");
await hello.SetRate(1.2);
await SpeechSynthesis.Speak(hello);
await Navigator.MediaSession.SetMetadata(await MediaMetadata.Create(new() { Title = "Song" }));
await Navigator.MediaSession.SetMetadata(null);
An attribute that holds one of several kinds of object is the first MDN lists, as a call's answer is: a media element's
srcObject is a MediaStream, so SetSrcObject(null) names one overload.
Constructors and static members
new X(…) is X.Create(…), and the new object is kept: await using var channel = await BroadcastChannel.Create("updates").
A static member is on the class, as in JavaScript: await URL.CanParse(link), await Notification.RequestPermission().
An event's class has its statics too, though the event itself is what a handler gets — iOS asks before it fires a
device's motion: var state = await DeviceOrientationEvent.RequestPermission(); (WebAssembly: in the click).
On an element ref
What an element's members hand over or answer with as a live object is on its typed
element ref once Rask.Web is imported, a chain from the element like any other:
await using var pip = await _video.RequestPictureInPicture(); // PictureInPictureWindow — WebAssembly: in the click
await _video.SetSrcObject(stream); // a MediaStream you kept; SetSrcObject(null) clears it
await using var fade = await _box.Animate([new() { ["opacity"] = 0 }, new() { ["opacity"] = 1 }], 300);
await using var slide = await _box.Animate(frames, new KeyframeAnimationOptions { Duration = "300", Easing = "ease-out" });
var running = await _box.GetAnimations(); // Animation[], each one kept
await using var camera = await _canvas.CaptureStream();
Keyframes are CSS property names to values, one map per keyframe, each value a string or a number as in JavaScript.
The options' new() has to name its type, since Animate(frames, 300) takes the duration alone too. Only calls and
writes are here: a read of the element's own state (its classList, style, dataset) is the render's, and
nothing that holds the page's nodes (an HTMLCollection) is generated. A ref whose element has left the page is an
error, never the window in its place.
A stream a Rask service hands over as an IJSObjectReference — Trigger.MediaCapture's OnStream, IWebRtc's
RtcHandlers.OnTrack — becomes a kept MediaStream with MediaStream.From(stream):
await _video.SetSrcObject(MediaStream.From(stream)), and GetTracks() then Stop() on each track to stop it.
An element the browser names — document.fullscreenElement, activeElement, pictureInPictureElement — cannot cross,
but whether it is yours can: it reads as an ElementRef equal to the ref you rendered it with, and null for none or
for one you gave no ref. A ref is its id, so == compares the element it names:
var current = await Document.FullscreenElement;
bool mine = current == _stage; // readonly ElementRef<HTMLDivElement> _stage = new();
Bytes, your own types, lists and loose objects
Bytes are a byte[], both ways — a BufferSource, an ArrayBuffer, a DataView or a Uint8Array alike. They
cross as base64 and arrive as a Uint8Array; a buffer the browser answers with comes back as a byte[]. A method that
fills the array it is handed and answers with it does the same:
var hash = await Crypto.Subtle.Digest("SHA-256", bytes);
var salt = await Crypto.GetRandomValues(new byte[16]); // the filled array
await device.TransferOut(1, payload); // a USBDevice
var value = await characteristic.ReadValue(); // a Bluetooth DataView, as bytes
var push = await PushManager.Subscribe(new() { UserVisibleOnly = true, ApplicationServerKey = vapidKey });
A method that fills the bytes it is handed but answers with something else (AnalyserNode.GetByteFrequencyData) is
not generated: the bytes cross as a copy, so you would never see them filled.
Where MDN says any, it is your own type. An argument is a type parameter, written by the host's own JSON options
as any JS interop argument is; a result is a generic read, and so is an event's any field:
await channel.PostMessage(new CartChanged(42)); // BroadcastChannel.postMessage(any)
var order = await response.Json<Order>(); // Response.json(): Promise<any>
var state = await History.State<CartState>(); // history.state
await using var _ = await channel.OnMessage(e => _last = e.Data<CartChanged>()); // MessageEvent.data
Your type is kept whole in a trimmed WebAssembly app, as for InvokeAsync<T>; under full AOT give it a
JsonSerializerContext, as Blazor asks. A promise of anything that settles with what your callback returned (a lock
request's) is only waited on. Bytes are bytes there too — a byte[] you hand over as an any, alone or anywhere inside
your own type, arrives as a Uint8Array, and one the browser answers with comes back as a byte[].
A dictionary the browser answers with that holds an any is generic on your type, which is how a stream is read:
await using var writer = await port.Writable.GetWriter();
await writer.Write(new byte[] { 0x01, 0x02 }); // WritableStreamDefaultWriter.write(any)
await using var reader = await port.Readable.GetReader(); // the default reader: ReadableStreamDefaultReader
var r = await reader.Read<byte[]>(); // ReadableStreamReadResult<byte[]>
if (!r.Done) Use(r.Value);
await reader.ReleaseLock(); // or reader.Cancel()
GetReader() is the reader MDN lists first, the one you get without asking for another; the overload that asks for a
BYOB reader is not generated, since C# could not tell which object it answers with.
A list of live objects is an array of kept objects, each disposed of on its own; an empty slot (a gamepad not
connected) is null:
var devices = await Navigator.Usb.GetDevices(); // USBDevice[], each one kept
var ports = await Navigator.Serial.GetPorts();
var pads = await Navigator.GetGamepads(); // Gamepad?[]
var files = await Window.ShowOpenFilePicker(); // WebAssembly: in the click
An argument MDN types only as object takes the dictionary its page documents, from a small table in the generator
(src/Rask.Dom.Tasks/WebObjectArgs.cs); without an entry it is not generated:
await using var status = await Navigator.Permissions.Query(new() { Name = "geolocation" });
var state = await status.State; // PermissionState.Granted
A union is one C# type where one type can stand for every case:
- A value or a list of it is the list — written as the list, and a lone value read back is a list of one: an
observer's
Threshold, an ICE server'sUrls, a notification'sVibrate. - A string or a number is a
string, and crosses as the number where it spells one:"6159"goes as6159,"battery_service"and"0x180F"as text. The number is the short form of what the text also says (a Bluetooth service UUID), and where the text is a name instead — an animation's"auto", a performance mark's name — a number written as text would be taken for one. Read back, a number is its text. - A
record<K, V>is aDictionary<string, V>: a file picker'sAccept,Headers.Create(…),PushSubscriptionJSON.Keys. - A boolean or a dictionary is the dictionary, nullable:
nullisfalse(left out, not asked for) and an empty one istrue, since any object is truthy in JS — a media request'sVideo/Audio. Read back, the browser'strueis an empty dictionary and itsfalseisnull. - A media constraint — MDN's
ConstrainBoolean,ConstrainDouble,ConstrainULong,ConstrainDOMString— is its plain value, nullable:bool?,double?,int?,string?, which the browser takes as the ideal. Read back, one a page wrote as an object is itsideal, else itsexact, a list is its first item, and a range alone isnull.
_stream = await Navigator.MediaDevices.GetUserMedia(new() { Video = new() }); // video: true; no audio
_stream = await Navigator.MediaDevices.GetUserMedia(new() { Video = new() { Width = 640, FacingMode = "user", Torch = true } });
The constraints with no single plain value — Pan/Tilt/Zoom (a boolean or a number), EchoCancellation (a
boolean or a string) and PointsOfInterest (points) — are not generated.
var battery = await Navigator.Bluetooth.RequestDevice(new() // WebAssembly: in the click
{
Filters = [new() { Services = ["battery_service"] }], OptionalServices = ["battery_service"],
});
var files = await Window.ShowOpenFilePicker(new()
{
Types = [new() { Description = "Text", Accept = new() { ["text/plain"] = [".txt"] } }],
});
An object that is nothing but values is a record, wherever the browser hands it over: in a callback (an
observer's entries), in a read-only list (await pad.Buttons is a GamepadButton[], await device.Configurations a
USBConfiguration[]), or in an event (a DeviceMotionEvent's Acceleration, an RTCErrorEvent's Error). Its
fields are typed as MDN says. One that holds a live object too (an XR input source, with its spaces) stays live, since
its record would lose it.
A list the browser hands over that is only data crosses as a record too, read with the event. It has an indexer,
Length, and Items (the array, for LINQ or foreach). Speech recognition's results are a list of results, each a
list of alternatives:
await _rec.OnResult(e =>
{
for (var i = e.ResultIndex; i < e.Results.Length; i++)
{
var r = e.Results[i]; // SpeechRecognitionResult
if (r.IsFinal) _text += r[0].Transcript; // its best SpeechRecognitionAlternative
}
_all = string.Join(" ", e.Results.Items.Select(r => r[0].Transcript)); // or the array, with LINQ
});
Events and callbacks
An object's events are On{Event} — MDN's event name, like the element events — and subscribing returns a
subscription to dispose of. The handler runs in its component's order and re-renders it, and takes the event or
nothing, sync or async:
_watch = await Window.MatchMedia("(min-width: 900px)").OnChange(e => _wide = e.Matches); // MediaQueryListEvent
await using var online = await Window.OnOnline(() => _online = true);
The event is MDN's type — Core's Event where an element event uses the same one, else a type in Rask.Web.Types
(MediaQueryListEvent, StorageEvent) deriving from it — holding the fields that are values. A field that is a live
object is kept for you, the same object the browser fired with:
await using var _ = await Navigator.Usb.OnDisconnect(async e =>
{
await using var device = e.Device; // which device left
_left = await device.ProductName;
});
An event made to be acted on later — a BeforeInstallPromptEvent, for a PWA's own install button — is kept whole, and
its method runs on it when you call it, in a click (WebAssembly):
_install = await Window.OnBeforeInstallPrompt(e => _deferred = e);
// … in the install button's click:
var answer = await _deferred.Prompt(); // PromptResponseObject
var accepted = answer.UserChoice == AppBannerPromptOutcome.Accepted;
The spec moved the outcome from the old userChoice promise onto what prompt() answers with, so it is read there.
A hot event is throttled on the subscription. A sensor fires ~60 times a second, and each one would cross to C#
and re-render; every: coalesces them in the browser before they cross. The first event goes at once, then at most one
per interval — the latest, so the last reading always arrives (the ones between are dropped before any payload is
built). Without every each event crosses, as before; disposing of the subscription clears its timer.
_tilt = await Window.OnDeviceOrientation(e => _angle = e.Alpha, every: 100.Milliseconds);
_moved = await Window.OnDeviceMotion(e => _accel = e.Acceleration, every: 100.Milliseconds);
Each one is a handle the browser holds until you dispose of it, or until the handler's component unmounts — so dispose
of what you do not keep. An event that only ever fires on the object it names (a HID device's inputreport, many a
second) leaves that field out: you are holding it already. A method or constructor
that takes a callback takes a C# handler for it, run and re-rendered the same way, and an element it takes is your
ElementRef:
await Navigator.Geolocation.GetCurrentPosition(p => _where = p.Coords);
await using var io = await IntersectionObserver.Create(entries =>
_visible = entries.Where(e => e.IsIntersecting).Select(e => e.IntersectionRatio).ToArray());
await io.Observe(_card); // readonly ElementRef _card = ElementRef.New();
await Navigator.Locks.Request("sync", async lk => await Sync()); // the lock is held until Sync() returns
await Navigator.Locks.Request("sync", new() { IfAvailable = true }, lk => _free = lk is not null); // null: held elsewhere
What a callback is handed is data, read when it ran: an IntersectionObserverEntry, a ResizeObserverEntry, a
MutationRecord, a Lock is a record under MDN's name, like DOMRect, its fields typed as MDN says (e.IsIntersecting
is a bool). A field that is a node (an entry's target) is left out. The handler takes the arguments it can use, in
order — an observer's entries, not the observer, which you already hold. A callback whose promise the browser waits on
(a lock's) is finished before the browser goes on; one whose result the browser reads (an Observable predicate) is not
generated.
A handler has to belong to a component — a lambda written in one, or a method of it — since that is the component it
re-renders; the component unmounting drops it. A handler the browser awaits is the exception: one written outside any
component (a hosted service's lock request) runs at once, re-renders nothing, and is dropped when the call that runs
it settles. Keep the subscription or the observer in a field and dispose of it in
OnUnmount to drop its handlers sooner; Disconnect() an observer to stop the browser watching.
Asking whether the browser has it
IsSupported asks the browser whether the object at the end of a path is there, instead of your guessing from its
user agent: await Navigator.Clipboard.IsSupported, await Navigator.IsSupported. A class has one too, for the
guard before Create — if (await EyeDropper.IsSupported) asks "EyeDropper" in window. A method has none, since a
C# method has no members to ask with: a method on a global (Window.ShowOpenFilePicker) that this browser lacks
answers the call with a JSException.
A web API is generated once ONE browser engine ships it, so the device APIs only Chromium has are here too — WebUSB,
WebHID, Web Bluetooth, the Battery Status and Network Information APIs, navigator.vibrate, the EyeDropper and Idle
Detection. Their doc comment opens with Chromium only. (or Firefox, or Safari), and IsSupported is the guard:
if (await Navigator.Usb.IsSupported)
{
var device = await Navigator.Usb.RequestDevice(new() { Filters = [new() { VendorId = 0x2341 }] });
}
Elements and their attributes stay cross-engine: markup only gets what two engines ship.
To hear about it at build time instead, name the oldest browsers the app supports in its project file:
<PropertyGroup>
<RaskBrowserTargets>safari >= 16; firefox >= 115</RaskBrowserTargets>
</PropertyGroup>
Each generated member carries MDN's first version per engine, and a call to one a named browser never shipped,
or shipped later, is warning RASK098 where it is made — unless it sits under an
IsSupported check: inside if (await Navigator.Usb.IsSupported) { … }, or after
if (!await Navigator.Usb.IsSupported) return;. The browsers are chrome, edge, firefox and safari;
unset, nothing is checked.
What is on no standards track is left out, except a short list named one by one in scripts/mdn/refresh.mjs
(NON_STANDARD), each something a real app needs and nothing standard does: today BeforeInstallPromptEvent. Their
doc comments say Non-standard.
Where a browser falls short
A few web APIs need help before they behave as MDN says. Rask patches each one by name, in one file —
src/Rask.Core/Resources/rask-web-patches.ts — and everything not listed runs exactly as the browser has it.
SpeechRecognitionis prefixed in Chromium before 139 and in Safari, aswebkitSpeechRecognition.SpeechRecognition.Create()andSpeechRecognition.IsSupportedfall back to it.WakeLock.request()is dropped when the page is hidden, in every browser. Rask requests it again each time the page becomes visible, soawait Navigator.WakeLock.Request(WakeLockType.Screen)holds until youawait sentinel.Release(). ItsReleasedturns true andOnReleasefires once: when you release it, or when the browser refuses it back. Disposing of the sentinel alone does not release the lock, as dropping one in JS doesn't.beforeinstallpromptfires once, at load, before any component can subscribe. Rask keeps it from boot and replays it to each laterWindow.OnBeforeInstallPrompt(…)subscriber until it is spent (prompted, or the app installed). Chromium'sprompt()answers with{ outcome }; Rask turns it into MDN's{ userChoice }, so(await e.Prompt()).UserChoicereads the same everywhere. The event is notpreventDefault()ed.
Testing
Fake() stands in for a web object in a test, for the test's own flow, until disposed of: every chain that starts at
it is answered by the fake, and nothing reaches a browser.
using var clipboard = Navigator.Clipboard.Fake();
clipboard.Returns(c => c.ReadText(), "pasted");
await page.Click("Paste");
Assert.Equal("writeText", clipboard.Calls.Single().Member);
A read or call nobody set up answers the type's default, a write is remembered for the next read, and an object kept
from a fake stays in it. Raise("change", new MediaQueryListEvent { Matches = true }) fires an event at the handlers
subscribed to it, which run in their components as the browser's would. The globals fake the same way:
using var storage = LocalStorage.Fake();.
Returns(l => l.IsSupported, false) makes a fake absent, Throws(s => s.Persist(), new JSException("denied")) fails
a member as the browser refusing it would, and CallsBack runs the handler a call such as a lock request was handed,
the call settling when it has:
using var locks = Navigator.Locks.Fake();
locks.CallsBack<Lock?>("request", (call, handler) => handler(new Lock { Name = (string)call.Args[0]! }));
Where to call it
From an event handler or OnRendered, where the page is live — on the server host each chain runs over the page's
socket, in WebAssembly in-process. A call made anywhere else throws, saying so. (Rask's own services that call one
outside a handler — the browser SQLite host at boot, the culture cookie — name the page they run on themselves.) Browser-gated members (clipboard,
geolocation) can still be refused; that arrives as a JSException from the awaited call.
Every member's doc comment carries its browser support and links to MDN and the spec, straight from MDN's data, and
says so when it only exists on an HTTPS page (or localhost): MDN's [SecureContext].
What only WebAssembly runs
Two kinds of member are generated into Rask.Wasm alone, as extensions of the same types, so in a WebAssembly app
they read like any other and in a server app they do not compile:
- A call the browser allows only during the user's click —
Navigator.Share(…),Notification.RequestPermission(),MediaDevices.GetDisplayMedia(…),ScreenOrientation.Lock(…),PaymentRequest.Show(),DeviceOrientationEvent.RequestPermission(), and on element refsRequestFullscreen(),RequestPointerLock(),ShowPicker()andRequestPictureInPicture(). On the server host the click's frame crosses the socket first, the browser lets the click go, and the call would fail withNotAllowedError. The IDL does not mark these; the list is Rask's (src/Rask.Dom.Tasks/WebHost.cs). - A family driven every frame — WebGL (and its extension objects), WebGPU and audio worklets, where a round trip per call is no way to draw.
await Navigator.Share(new ShareData { Title = "Rask", Url = "https://rask.sh" }); // WebAssembly: runs in the click
// Server: does not compile
On the server host, share from markup instead: Shareable and the Trigger.* components run the call inside the
click itself.
namespace Rask.Site.Features;
// MDN's own globals, generated from MDN's data by Rask.Web: each chain runs in the browser in one round trip when it
// is awaited, and an object you await is kept until it is disposed of.
public sealed partial class WebApiDemo : Component
{
private string _read = "";
private string _stored = "";
private string _kept = "";
private string _width = "";
private string _seen = "";
private string _lock = "";
private IAsyncDisposable? _watch;
private Types.IntersectionObserver? _observer;
private readonly ElementRef _panel = ElementRef.New();
protected override Component? Render() =>
Div[
Div.Class("flex gap-2 flex-wrap items-center mb-3")[
Ui.Button.Primary.Id("web-read").OnClick(Read)["Read the browser"],
Ui.Button.Id("web-store").OnClick(Store)["Round-trip localStorage"],
Ui.Button.Id("web-keep").OnClick(Keep)["Keep a media query"],
Ui.Button.Id("web-watch").OnClick(Watch)["Watch the width"],
Ui.Button.Id("web-observe").OnClick(ObservePanel)["Observe this panel"],
Ui.Button.Id("web-lock").OnClick(HoldLock)["Hold a lock"]
],
P.Id("web-read-out").Class("text-sm mb-1")[_read],
P.Id("web-store-out").Class("text-sm mb-1")[_stored],
P.Id("web-keep-out").Class("text-sm mb-1")[_kept],
P.Id("web-watch-out").Class("text-sm mb-1")[_width],
P.Id("web-observe-out").Ref(_panel).Class("text-sm mb-1")[_seen],
P.Id("web-lock-out").Class("text-sm mb-0")[_lock]
];
// navigator.language, document.visibilityState, matchMedia(…).matches: three round trips.
private async Task Read()
{
var language = await Navigator.Language;
var page = await Document.VisibilityState;
var dark = await Window.MatchMedia("(prefers-color-scheme: dark)").Matches;
_read = $"Language {language}, page {page}, dark mode {(dark ? "on" : "off")}";
}
private async Task Store()
{
await LocalStorage.SetItem("rask-web-demo", "stored by Rask.Web");
_stored = $"localStorage says: {await LocalStorage.GetItem("rask-web-demo")}";
}
// MediaQueryList's `change` event: the handler runs here each time the page crosses 900px, and re-renders this.
private async Task Watch()
{
_watch ??= await Window.MatchMedia("(min-width: 900px)").OnChange(e => _width = $"{e.Media} now matches: {e.Matches}");
_width = "Watching (min-width: 900px)";
}
// new IntersectionObserver(callback): the entries arrive as data, each time the panel crosses into or out of view.
private async Task ObservePanel()
{
if (_observer is not null)
{
return;
}
var observer = await IntersectionObserver.Create(entries =>
_seen = string.Join(", ", entries.Select(e => e.IsIntersecting ? $"In view, {e.IntersectionRatio:P0} of it" : "Out of view")));
await observer.Observe(_panel);
_observer = observer;
}
// navigator.locks.request: the browser holds the lock until the async handler has finished.
private async Task HoldLock()
{
await Navigator.Locks.Request("rask-web-demo", async lk =>
{
_lock = $"Holding {lk?.Name} ({lk?.Mode})";
await Task.Delay(100);
});
_lock += ", then let it go";
}
protected override async Task OnUnmount()
{
if (_watch is not null)
{
await _watch.DisposeAsync();
}
if (_observer is not null)
{
await _observer.DisposeAsync();
}
}
// Awaiting the MediaQueryList keeps it in the browser; `await using` lets it go.
private async Task Keep()
{
await using var query = await Window.MatchMedia("(min-width: 1px)");
await using var url = await URL.Create("/docs/guides/web-apis", await Location.Origin); // new URL(path, base)
_kept = $"Kept the MediaQueryList for {await query.Media}: matches {await query.Matches}; a URL for {await url.Pathname}";
}
}
What it leaves to the rest of Rask
- The DOM. Nothing that returns or rewrites DOM nodes is generated: the render owns the page. Reach an element's
own members through a typed element ref (
await _dialog.ShowModal()). - What needs a live object the C# side cannot name — a member that takes a
Documentor a text node, a callback that hands back a live object (anIdleDeadlineto ask the time left of), or one whose result the browser reads — is not generated; the remaining typed browser API wrappers (IIndexedDb,IWebAuthn,IViewTransitions,IWebRtc,ISignaling,IBackgroundSync) and your own scoped TypeScript cover those today. - Names it shares with the rest of Rask. Where a global is named like a Rask type, Rask's keeps the bare name:
EditContextis the form's,FormDataa submit's, andDataTransfer,EventTargetandTouchRask's event types, by global aliases the package adds beside the import. MDN's are a qualifier away when you want them:Rask.Web.FormData,Types.EditContext.