Concept

JS Interop

How BlazOrbit splits JavaScript into per-feature interop services, and how components opt into behaviors through IJsBehavior interfaces.

Topic-specific interop services

Rather than a single monolithic JS module, interop is split by feature. Each interface lives in BlazOrbit.Services.JsInterop and has a matching TypeScript module bundled by Vite into wwwroot/js/.

  • IThemeJsInterop — reads / writes data-theme on <html> and persists to localStorage.
  • IBehaviorJsInterop — applies lightweight DOM behaviors (e.g. ripples).
  • IPatternJsInterop — date / time / number input patterns.
  • IDropdownJsInterop — positioning + outside-click handling for dropdowns and tooltips.
  • IClipboardJsInterop — copy-to-clipboard.
  • ITextAreaJsInterop — auto-grow textarea sizing.
  • IDraggableJsInterop — pointer capture for BOBDraggable.
  • IColorPickerJsInterop — color picker interactions.
  • IModalJsInterop — scroll lock + focus trap for dialogs and drawers.

All of them are registered as Scoped by AddBlazOrbit() — you don't need to touch them unless you're building a custom component that reuses one.

The IHas*Behavior pipeline

Some capabilities go beyond CSS — a ripple effect, say, needs a click-time DOM tweak. Those are expressed as interfaces deriving from IJsBehavior: IHasRipple, IHasAutoGrow, and so on.

During OnAfterRenderAsync(firstRender), BOBComponentBase walks the implemented behavior interfaces and runs BOBComponentJsBehaviorBuilder to invoke the matching JS hook on the rendered element. You get automatic cleanup on dispose.

Opting into ripple
public partial class MyCustom : BOBComponentBase, IHasRipple
{
    [Parameter] public bool DisableRipple { get; set; }
    [Parameter] public string? RippleColor { get; set; }
    [Parameter] public int? RippleDuration { get; set; }
}

// Renders: --bob-inline-ripple-color / --bob-inline-ripple-duration.
// BOBComponentJsBehaviorBuilder reads IHasRipple state directly and wires up
// the pointer listener — no DOM data-attribute is needed.

Directly calling an interop from a component

If you need imperative JS (e.g. to copy to clipboard, focus an input, or toggle a theme), inject the relevant interop:

MyComponent.razor
@inject IClipboardJsInterop Clipboard
@inject IThemeJsInterop Themes

<BOBButton Text="Copy" OnClick="OnCopy" />
<BOBButton Text="Toggle theme" OnClick="OnToggle" />

@code {
    private Task OnCopy() =>
        Clipboard.CopyTextAsync("Copied from a custom component!");

    private Task<string> OnToggle() =>
        Themes.ToggleThemeAsync(new[] { "light", "dark" });
}

Where the JS lives

TypeScript sources live in src/BlazOrbit/Types/<Feature>/ and are bundled per feature into wwwroot/js/Types/<Feature>/<Feature>Interop.min.js by the build pipeline (see the Getting started page for how that is wired up). The generated JS is referenced by each interop C# class via IJSRuntime.InvokeAsync.

You don't ship the JS bundles yourself — they're part of the BlazOrbit NuGet's static web assets and are served under _content/BlazOrbit/js/….

Building a custom component with JS

  1. Write a TypeScript module and arrange for it to ship with your app (or a feature package).
  2. Expose an interop service with an interface + InvokeAsync-based implementation, and register it as scoped.
  3. Inject the service into your component and call it from OnAfterRenderAsync / event handlers.
  4. If the behavior should auto-wire to a marker interface, derive an interface from IJsBehavior and wire it into BOBComponentJsBehaviorBuilder.