Skip to content

Scenario · State

Checkout

A cart, two forms and a few application settings. CartState and AppState are ordinary scoped services that implement IBevStateProvider, so every action on them is a named state change in BevTools, with a before/after diff and the click that made it. The forms show what validates them and what does not, and the coupon box has an accessibility problem to find.

The checkout

Add, remove, apply the coupon (the ✓ button) or clear the cart, then fill the shipping form in and place the order - nothing is sent anywhere. The newsletter form below it subscribes even with an empty address. The two toggles change AppState values only; the site's own light/dark theme is the button in the header.

Live Renderer: Static · prerendering CtrlShiftD opens BevTools

Cart (0 items)

Empty. Add products from the products page or:

Shipping details

State: history with diffs

Open State and select CartState: the change history names each action (Add, Remove, ApplyCoupon, Clear), with the values before and after and a link to the UI event that triggered it. AppState records SetTheme, ToggleNotifications, Click and a Visit per navigation - and shows its ApiKey redacted.

Forms: what validates each form

Press Place order with the form empty, then Subscribe with the newsletter field empty, and open Forms. It lists every EditForm on the page with its model:

  • The shipping form is validated by DataAnnotationsValidator and its messages are shown by ValidationSummary; the table has a row per field - modified or not, how many changes, and its validation messages.
  • The newsletter form is validated by nothing, next to its model's rules (Email: Required, EmailAddress): it has no <DataAnnotationsValidator />, so OnValidSubmit always runs. Diagnostics reports NewsletterModel's validation attributes are never checked.
  • The Timeline records every validation with its outcome: Form validated: NewsletterModel is valid.

Browser → Accessibility: a coupon box nobody can name

Add something to the cart, then open Browser and press Check the page. Two findings point at the coupon box, each naming the component that rendered it (Checkout) and the WCAG criterion, with Show to outline the element: the field has no label - a placeholder disappears as soon as something is typed, so it is not one - and the ✓ button has no accessible name, because its only content is hidden from screen readers. Diagnostics repeats them. What markup cannot decide - contrast, focus order, whether a name makes sense - the panel leaves to a person.

Exposing your own state

A state container becomes a provider by implementing IBevStateProvider - a name, a snapshot and a Changed event carrying the action - and being registered with AddBevToolsStateProvider. This is the cart above, shortened:

public sealed class CartState : IBevStateProvider
{
    public List<CartLine> Lines { get; } = [];

    public string Name => "CartState";

    public string? Description => "scoped service, one per circuit / app";

    public event Action<BevStateChangeInfo>? Changed;

    public object? GetSnapshot() => this;

    public void Add(Product product)
    {
        // … update Lines …
        Changed?.Invoke(new BevStateChangeInfo("Add", product.Name));
    }
}

// Program.cs - registers it as a scoped service and as a state provider
builder.Services.AddBevToolsStateProvider<CartState>();

A store that cannot implement the interface - a library's, or one you would rather keep free of BevTools - gets an adapter instead: AddBevToolsStateProvider<TService>(name, snapshot, subscribe), or an extension's AddStateProvider, which is how the FilterState of the Products page appears in the State panel (see Extending).

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.

An unhandled error has occurred. Reload 🗙