npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kopular

v1.4.3

Published

Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container

Readme

Kopular

Kopular is a small component framework for KopScript, built to give Angular's separation of concerns — components own UI, services own logic, a router owns navigation — without Angular's steepest learning-curve pieces: no RxJS, no dependency-injection container, and templates that are real, compiled, type-checked KopScript rather than a separate interpreted template language.

Generating Kopular code with an AI coding assistant? Point it at LLM.md — a dense, complete reference designed to be loaded straight into an LLM's context.

Highlights

  • Component, with real vdom diffing: a base class with virtual Render() (describes the current state as a VElement tree — a lightweight description of a DOM element, not a real one) and Update() (diffs the new tree against the previous one and patches only what changed, reusing a real DOM node wherever its tag stays the same — not a full subtree rebuild). Render() is provided as a real, separate markup file compiled by KopScript's template from (see "Templates" below), or written by hand building a VElement tree against kopular/velement, the way you'd write careful vanilla-JS UI code — your choice, and both compile to the exact same thing. An optional virtual RenderError(message) renders a fallback instead of a hard crash if Render() throws — purely additive; not overriding it keeps today's exact (uncaught) behavior. Every real DOM event handler Kopular attaches (OnClick/OnInput/OnBlur/OnChange) batches the Update() calls made while it runs into one render per component, applied once the handler returns, fully synchronously — a click handler that mutates more than one field only ever renders once, reading every field's final value, regardless of which statement happens to trigger it. See LLM.md's "Component" section for the full rationale and the real bug this closes.
  • Templates, compiled and type-checked, not interpreted: markup lives in its own .html file — interpolation ({{ }}), event/property bindings ((click)="...", [prop]="..."), and *if/*for structural directives — desugared by the KopScript compiler into the exact same code a hand-written Render() would produce, with automatic Subscribe/Update() wiring for the state<T> fields the markup reads, directly or through the component's own methods. See "Templates" below.
  • Reactive state, no RxJS: components hold state<number>/state<string>/... (a KopScript language feature — see the KopScript repo) and subscribe once, in their constructor, to call Update() on change. No Observables, no operators, no manual unsubscribe bookkeeping.
  • Services, no DI container: "injecting" a service is just passing it as a constructor argument. No injector hierarchy, no provider tokens, no decorators — and a service stays fully testable with zero Component/DOM involvement, since it's just a class.
  • Router: real URLs (/about, not #/about) via the History API (pushState/popstate), with route registration as plain method calls, not a config DSL. See "Router, and deploying it" below — every deployment target needs its own SPA-fallback config, not just local dev.
  • Structural directives: *ngIf/*ngFor/*ngSwitch's job — build a subtree conditionally, repeat one per item, pick one of several cases. In a template, that's *if/*for (real if/for under the hood — see "Templates"); in a hand-written Render(), the same job is a plain function call (If(...)) or existing KopScript expression (array.ForEach(...), match) — no special syntax needed there either way. See "Structural directives" below.
  • Http: a thin, static wrapper over the real Fetch API (Http.Get(url), Http.Post(url, jsonBody), ...) — no HttpClient injection tokens, no RxJS observables/operators. See "HTTP" below.
  • FormField<T>: a single input's value/error/touched state, built on state<T> — no two-way-binding magic, no FormGroup config object. See "Forms" below.

What's here

  • src/dom.ks — ambient DOM bindings (document, Element, Event, window, location) that component.ks/router.ks/directives.ks are built on.
  • src/velement.ks — VElement, the lightweight description of a DOM element Render() returns.
  • src/vdom.ks — the diff/patch engine (Materialize/Patch/PatchChildren) behind real vdom diffing — see "Component" above.
  • src/component.ks — the Component base class.
  • src/router.ks — the Router.
  • src/directives.ks — If(), the structural-directive equivalents' one genuinely new piece (see below).
  • src/http.ks — Http, a thin wrapper over fetch (see below). src/http_runtime.js is its one companion file — the single hand-written (not compiled from .ks) file in Kopular, and why is explained in its own header comment.
  • src/forms.ks — FormField<T> and Validators (see "Forms" below).
  • src/testing.js — runKopularApp/runKopularFixture (see "Testing your own app" below); hand-written for the same reason as http_runtime.js — filesystem/process orchestration isn't a Kopular Component.
  • bin/kp.mjs — the kp new scaffolding CLI (see "Starting a new project" below); also hand-written, same reason.

That's the whole framework — ten files, plus the scaffolding CLI. Everything else (a real app built on top of it) lives in a separate consumer repo, KopularDemo.

Templates

A component's Render() can be a real markup file instead of hand-written imperative DOM code — template from "./x.html"; in the class body, a KopScript language feature (see KopScript's own README/LLM.md for the full syntax reference). Kopular itself needed zero framework code changes for this — the compiler desugars a template straight into calls against the same VElement.Create/.AppendChild/.TextContent/named event fields velement.ks already declares, so a template-generated Render() is indistinguishable from one you'd write by hand:

// counter.ks
class Counter : Component {
  public state<number> Count;
  constructor() : base() { this.Count = state(0); }
  public void Increment() { this.Count.Value = this.Count.Value + 1; }
  template from "./counter.html";
}
<!-- counter.html -->
<button (click)="Increment()">Count: {{ Count.Value }}</button>

Note there's no this.Count.Subscribe(...) anywhere — a state<T> field referenced directly in the template (Count.Value above) gets it wired automatically. The exact same component, hand-written, needs that Subscribe call itself:

class Counter : Component {
  private state<number> Count;

  constructor() : base() {
    this.Count = state(0);
    this.Count.Subscribe((number v) => this.Update());
  }

  public override VElement Render() {
    VElement button = VElement.Create("button");
    button.TextContent = "Count: " + this.Count.Value;
    button.OnClick = (Event e) => {
      this.Count.Value = this.Count.Value + 1;
    };
    return button;
  }
}

Both produce the same Render(), and can be mixed freely across a codebase — nothing about Component, Update(), or any other Kopular API differs between them. The manual Subscribe call is still exactly what you need the moment state lives on another object — an injected service's own state (this.Service.Count, say) — which is why the real, production Counter on the KopularDemo site (it injects a CounterService rather than holding Count itself) uses a template but still has one manual Subscribe. *if/*for in a template are covered under "Structural directives" below, alongside their hand-written-Render() equivalents.

Scoped component styles

A class-body styles from "./x.css"; — also a KopScript language feature (see KopScript's own README/LLM.md for the full syntax/CSS-parsing reference) — scopes a real stylesheet so it only matches elements that class itself renders, never a sibling's or a child's:

class Widget : Component {
  constructor() : base() { }
  template from "./widget.html";
  styles from "./widget.css";
}
/* widget.css */
.title { color: royalblue; }

Unlike template from, this did need real framework code — ScopedStyles.Inject (vdom.ks) is the runtime half, and using "kopular"; brings it in along with everything else — do not write your own extern class ScopedStyles ... from "kopular/vdom";, which is now a real KS4002 Declaration 'ScopedStyles' conflicts with a name brought in by 'using'. (Before package using existed, every Kopular export did need a hand-written extern in your project; that is what using "kopular"; replaced.) It's idempotent, injecting one real <style> per component type into document.head the first time any instance of that type is constructed (dedup is per-type, not per-instance — every instance's constructor calls Inject with the same compile-time id/rewritten-css, so only the first actually creates a tag). Never removed once injected — a scoped stylesheet is global infrastructure for as long as the page lives, not per-instance content Teardown() would ever clean up. document.head (dom.ks) is new too, added specifically for this.

Independent of template from — works with a hand-written Render() as well, which gets a protected string ScopeId; field to apply manually (template from elements get their data-kop-scope attribute automatically, so this is only needed by hand-written code):

public override VElement Render() {
  VElement el = VElement.Create("div");
  el.ClassName = "title";
  el.SetAttr("data-kop-scope", this.ScopeId);
  return el;
}

Dependency injection: the composition root pattern

Kopular has no injector because KopScript has nothing for one to hook into — no decorators, no reflection, and no generic functions (KopScript's generics are classes/interfaces only — see the KopScript repo) for a type-safe Resolve<T>(). Instead, the whole app's service/page graph gets built exactly once, by hand, in one place: a plain class with no Component base and no framework code in it at all, sometimes called an app container or (in the wider DI literature) a composition root. Everything else just takes what it needs as constructor arguments and never constructs its own dependencies.

// app_container.ks — the one place that decides what's shared and builds
// the graph, in dependency order.
class AppContainer {
  public Router Nav;

  constructor() {
    // Built once, passed to every page that needs it below — that's the
    // whole mechanism for a shared singleton. A page that constructed its
    // own `new CounterService()` instead would get an independent one; the
    // difference is which variable gets passed in, not a config flag.
    CounterService counter = new CounterService();

    this.Nav = new Router(new NotFoundPage());
    this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
    this.Nav.AddRoute("/about", new AboutPage(this.Nav));
  }
}

// routed_app.ks — the root Component. Takes the already-built graph; never
// builds one of its own.
class RoutedApp : Component {
  private Router Nav;

  constructor(AppContainer services) : base() {
    this.Nav = services.Nav;
  }

  public override VElement Render() {
    return VElement.Create("div");
  }

  // VElement.Mount(this.Nav) in Render() (see "Nested component composition"
  // below) would work here too, but a single, always-present root slot has
  // no reordering or removal to get right, so the older, simpler
  // AfterRender is just as correct — it's called with the real DOM node
  // Render()'s tree just became, once Mount()/Update() has actually
  // materialized/patched it, here that's the container itself.
  protected override void AfterRender(Element root) {
    this.Nav.Mount(root);
  }
}

RoutedApp app = new RoutedApp(new AppContainer());
app.Mount(document.body);

This is sometimes called "Pure DI" — the same benefits a container gives you (nothing hardcodes its own dependencies, everything is swappable in a test) with none of a container's cost:

  • Compile-time checked. A missing or mistyped dependency is expected N arguments, got M from the KopScript compiler, not a NullInjectorError your users hit at runtime after a container fails to resolve something.
  • Fully legible. The entire dependency graph is ordinary, readable code in one file — grep for new in the composition root and you've read the whole wiring diagram. Nothing is constructed by a framework inspecting metadata behind the scenes.
  • No new concepts. If you already know how to call a constructor, you already know Kopular's DI story — there's no separate injector API, provider syntax, or injection-token vocabulary to learn.

See KopularDemo's src/app_container.ks and src/routed_app.ks for the real, working version this example is drawn from.

Router, and deploying it

Router nav = new Router(new NotFoundPage());   // fallback page required up front — no null route
nav.AddRoute("/", new HomePage(nav));           // pages built once, kept alive for Router's lifetime
nav.AddRoute("/about", new AboutPage(nav));
nav.Mount(document.body);
nav.Navigate("/about");                          // pushState + immediate re-render

Real URLs via the History API (pushState/popstate), not #/about hash routing. AddRoute takes an already-constructed Component, not a factory, so a page's own state<T> survives navigating away and back — see "Dependency injection" above for how the whole page graph typically gets built once, in a composition root.

A trailing slash matches the same route as without one (/about/ == /about, / is left alone) — real-world links and directory-style static hosting (a build-time prerendered route's own /about/index.html, most concretely — see KopularDemo's own scripts/prerender.mjs) routinely produce trailing-slash URLs, and every mainstream router normalizes this the same way.

Dynamic route segments: a path segment written :name (e.g. /dogs/:id) matches any single non-empty segment. With just one per route, its value is captured into Router.Param:

nav.AddRoute("/dogs/:id", new DogPage(nav));
// inside DogPage.Render():
el.TextContent = "Dog #" + this.Nav.Param;

Param is a plain string, deliberately not state<T> — Router's own Render() already re-embeds the matched page into its outlet (via VElement.Mount) on every Navigate()/popstate, which re-runs that page's Render() (reading the fresh Param) with no extra step. No Subscribe() needed on it.

More than one dynamic segment, and a trailing wildcard segment, both work too — read each by name via Router.Params(name) instead (Param above still holds the first captured value either way, so existing single-segment code needs no change):

nav.AddRoute("/dogs/:id/toys/:toyId", new ToyPage(nav));
// inside ToyPage.Render():
el.TextContent = "Dog " + this.Nav.Params("id") + " / Toy " + this.Nav.Params("toyId");

nav.AddRoute("/files/*", new FilesPage(nav));
// "/files/2026/reports/q1.pdf" -> Params("*") == "2026/reports/q1.pdf"

Query strings are always available via Router.Query(key), independent of which route matched (never part of route matching itself):

// "/search?sort=name&order=asc"
this.Nav.Query("sort")   // "name"
this.Nav.Query("missing") // "" — not present

Query values are not percent-decoded — a deliberate v1 cut (no decodeURIComponent binding exists yet); a value containing %20 or + for a space arrives exactly as written in the URL.

Navigation guards: protect a route (or any set of routes) behind a check — SetGuard takes a redirect path plus a single (string) => bool checked before every navigation, including a direct load/refresh:

nav.SetGuard("/login", (string path) => {
  if (path == "/admin") { return authService.IsLoggedIn.Value; }
  return true;
});

One guard for the whole Router, not per-route — the guard function itself decides which paths it cares about, the same "a function, not a config object" style Http/DI already use. Defaults to always-allow when SetGuard is never called. Redirecting updates the URL too (via pushState), so refreshing a blocked path lands on the redirect again rather than back on the page the guard just rejected — pick a redirectPath the guard itself always allows, or it loops.

Lazy routes (real code-splitting): AddRoute takes an already-built page — its whole JS chunk loads eagerly, with everything else, at startup. AddLazyRoute takes a loader instead, so that chunk is only ever fetched the first time its route actually matches:

nav.AddLazyRoute("/dogs", LoadDogsPage);

LoadDogsPage is a () => task<Component> — a real dynamic import(), since KopScript's own syntax has no expression for one. Write a tiny hand-written loader (not compiled from .ks) and reach it via a relative extern (see KopScript's own README.md/LLM.md "extern" section for the full mechanics — this needed a real compiler fix, [email protected], to work reliably):

// dogs_page_loader.js — hand-written
export async function LoadDogsPage() {
  const { DogsPage } = await import("./dogs_page.js");
  return new DogsPage();
}
extern task<Component> LoadDogsPage() from "./dogs_page_loader";

The outlet shows a plain loading placeholder (<div class="router-loading">Loading...</div> by default; override protected virtual VElement BuildLoadingPlaceholder() on a class extending Router to customize it) while the fetch is in flight, then the real page once it resolves — and, like an eager page, it's only ever fetched once: navigating away and back reuses the same already-loaded instance, keeping whatever state it built up. A lazy and an eager route mix freely in the same Router; nothing about AddRoute's own existing signature changes.

Building and testing a lazy route: dogs_page.ks is deliberately not using'd from your app's own entry — that's what keeps it out of the eager bundle — which also means your normal ks build src/app.ks never compiles it, since ks build only walks the using graph reachable from the entry you give it. Build it as its own separate entry too: ks build src/app.ks && ks build src/dogs_page.ks (already-compiled shared dependencies just get written again with identical output, so this is safe to add). For tests, kopular/testing's runKopularApp (0.24.0+) compiles any other real .ks file present in your srcDir that the entry doesn't reach — a lazy-route target is exactly that — so AddLazyRoute needs no special test setup at all.

AllPaths() returns every registered path (both AddRoute and AddLazyRoute), in registration order — for a caller that needs to enumerate real routes (a build-time static prerender step, most likely) without hand-maintaining a second, driftable list next to the composition root's own AddRoute calls.

Deploying a Router-based app needs SPA/history-fallback configured on whatever you deploy to — this is true of every client-side router in every framework, not a Kopular gap. A direct load or a refresh at /about is a plain HTTP request that reaches your host before any JS has run, so nothing client-side (Router included) can intercept it; the host has to serve the app shell itself for any route it has no literal file for. KopularDemo hit exactly this in production (worked when navigated to via a link, 404'd on refresh) before its Cloudflare Workers config had this set:

// wrangler.jsonc
"assets": {
  "directory": "./public",
  "not_found_handling": "single-page-application"
}

Every static host has an equivalent option (Netlify, Vercel, nginx, ...) — search that host's docs for "SPA fallback" or "single-page application routing", the terminology is standard. For local dev, see KopularDemo's scripts/serve.mjs.

Structural directives

Angular's *ngIf/*ngFor/*ngSwitch are template syntax that expands, at compile time, into imperative view-container calls. In a Kopular template, *if/*for are exactly that — real KopScript if/for statements underneath (see "Templates" above), compiled by KopScript itself, not interpreted by Kopular at runtime. In a hand-written Render(), there's no separate directive syntax to reach for: each job maps onto a plain expression, and two of the three need nothing new at all.

| Angular | Kopular template | Kopular hand-written Render() | New code? | | ----------------- | ---------------- | -------------------------------------------- | :-------: | | *ngFor | *for="Type v of expr" | array.ForEach((item) => ...) | none — already a KopScript array method | | *ngSwitch | (not supported — use *if, or switch in the backing class) | match value { ... } | none — already a KopScript expression, and exhaustiveness-checked (*ngSwitch isn't) | | *ngIf / *ngIf-else | *if="expr" | If(condition, () => ..., () => ...) | directives.ks (hand-written form only — a template's *if needs no helper, it's a real if) |

The rest of this section is about the hand-written Render() column above — a template's *if/*for need no further explanation, they're covered under "Templates". *ngIf is the one case in hand-written Render() that needs something new: if is a statement in KopScript, so without a helper you'd need a throwaway mutable local just to get a conditional value out of it. If() is that helper — nothing more than:

VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse) {
  if (condition) {
    return whenTrue();
  }
  return whenFalse();
}

Both branches are required (same reasoning Router uses for requiring a NotFoundPage up front — see router.ks): v1 has no nullable types, so "render nothing" has no value to hand back. Only the branch actually taken runs — the other lambda is never called, so an explicit empty branch (() => VElement.Create("span")) costs nothing when there's genuinely nothing to show.

All three read the same way, right inside a hand-written Render() — no directive registration, nothing to import beyond the function itself:

public override VElement Render() {
  VElement root = VElement.Create("div");

  // *ngIf
  root.AppendChild(If(this.User.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLoginButton()));

  // *ngFor
  VElement list = VElement.Create("ul");
  this.Items.ForEach((Item item) => { list.AppendChild(this.BuildItemRow(item)); });
  root.AppendChild(list);

  // *ngSwitch
  root.AppendChild(match this.Status {
    "loading" => this.BuildSpinner(),
    "error" => this.BuildError(),
    _ => this.BuildContent()
  });

  return root;
}

Give each Item's VElement a stable .Id (e.g. the item's own id) to make *ngFor trackBy-style row reuse automatic — Update()'s diff engine matches children by Id across a re-render, reusing a matched child's real DOM node (and anything stateful attached to it, like focus) rather than rebuilding it, the same way *ngFor's own trackBy avoids rebuilding unchanged rows. Without a stable Id, a list still renders correctly on reorder, but a given item's real node isn't guaranteed to follow its data.

Nested component composition

Everything above builds content into a VElement tree — a real, independent child Component (its own fields, its own Render(), its own reactive state<T>) is a different thing to embed than a plain element. VElement.Mount(component) wraps one as a slot the diff engine treats like any other content: created on first render, patched in place (not torn down and rebuilt) across a re-render as long as the same instance still occupies that slot, reordered by .Id exactly like any other keyed child, and torn down — calling OnUnmount() — the moment it's replaced or removed:

class TodoItem : Component {
  public string Id;
  private string Text;
  constructor(string id, string text) : base() {
    this.Id = id;
    this.Text = text;
  }

  public override VElement Render() {
    VElement li = VElement.Create("li");
    li.TextContent = this.Text;
    return li;
  }
}

class TodoList : Component {
  private TodoItem[] Items;
  constructor() : base() { this.Items = []; }

  public override VElement Render() {
    VElement ul = VElement.Create("ul");
    this.Items.ForEach((TodoItem item) => {
      VElement slot = VElement.Mount(item);
      slot.Id = item.Id; // stable key — the same convention as any other list, above
      ul.AppendChild(slot);
    });
    return ul;
  }

  public void Add(string id, string text) {
    this.Items = this.Items.Push(new TodoItem(id, text));
    this.Update();
  }

  public void Remove(string id) {
    this.Items = this.Items.Filter((TodoItem i) => i.Id != id);
    this.Update(); // the removed TodoItem's OnUnmount() fires here
  }
}

Each TodoItem is a genuinely independent Component — it can hold its own local state<T>, subscribe to a shared service, or mount further children of its own (Update() called from inside one re-renders just that item's own subtree, without disturbing its siblings, exactly like the plain-VElement keyed-list case above). Override OnUnmount() to release anything a removed instance was holding onto — most commonly, calling the unsubscribe handle state<T>.Subscribe now returns (see kopscript's own LLM.md) for a subscription made in the constructor:

class TodoItem : Component {
  private () => void UnsubscribeShared;
  constructor(SharedService shared) : base() {
    this.UnsubscribeShared = shared.Count.Subscribe((number v) => this.Update());
  }
  protected override void OnUnmount() {
    this.UnsubscribeShared();
  }
}

A component built once at app startup and never removed (every page Router manages today, most real apps' top-level structure) never needs OnUnmount() at all — it's there for the case a real dynamic list like TodoList above actually needs: a subscription that must stop, not just outlive an app that was going to unload anyway. Removing a component that itself mounted further children tears the whole subtree down, however many levels deep — a middle component overriding its own OnUnmount() doesn't skip its children's.

Router's own outlet uses this mechanism too — its Render() embeds the matched page via VElement.Mount, not the older AfterRender-based pattern still shown in "Router, and deploying it" below for mounting the Router itself into an app's root container (a single, always-present slot, where there's nothing this mechanism would add). Reach for VElement.Mount for anything with real add/remove/reorder: a list of components, a modal that comes and goes, anything genuinely dynamic. A template gets the same capability via *mount="expr" — see "Templates" in kopscript's own README/LLM.md for the syntax; it desugars to exactly this call.

Content projection — a parent giving a child what to render

React's children prop and Angular's <ng-content> both let a parent hand a child arbitrary markup to render at a spot the child itself decides. Kopular needs no separate mechanism for this at all: pass a () => VElement into the child's constructor, the same as any other constructor argument, and have the child call it from inside its own Render():

class Card : Component {
  private () => VElement ContentBuilder;
  constructor(() => VElement contentBuilder) : base() {
    this.ContentBuilder = contentBuilder;
  }
  public override VElement Render() {
    VElement div = VElement.Create("div");
    div.ClassName = "card";
    div.AppendChild(this.ContentBuilder());
    return div;
  }
}

// Usage, from inside the parent's own Render() — the parent decides what's inside the
// card, Card decides the chrome around it. `this` only means anything in a method, so
// that is where the callback is built:
class Page : Component {
  constructor() : base() { }
  public override VElement Render() {
    Card card = new Card(() => this.BuildCardBody());
    return VElement.Mount(card);
  }
  private VElement BuildCardBody() { return VElement.Create("p"); }
}

Because the callback is invoked fresh on every one of Card's own renders (not a value captured once and frozen), and it's a closure over the parent's own this, the projected content reflects the parent's current state every time — bump a field the parent's own BuildCardBody() reads, call the parent's Update(), and Card's own next patch shows the new value, patched in place like any other content. No new API: this is just VElement.Mount plus an ordinary function-typed constructor argument, the same "no hidden magic, explicit constructor args" pattern dependency injection above already uses.

More than one projection point works the same way — one named callback per slot, the same fixed-named-slot convention VElement's own OnClick/OnInput/OnBlur/OnChange already use rather than a generic, unordered bag:

class Panel : Component {
  private () => VElement HeaderBuilder;
  private () => VElement BodyBuilder;
  constructor(() => VElement headerBuilder, () => VElement bodyBuilder) : base() {
    this.HeaderBuilder = headerBuilder;
    this.BodyBuilder = bodyBuilder;
  }
  public override VElement Render() {
    VElement div = VElement.Create("div");
    VElement header = VElement.Create("header");
    header.AppendChild(this.HeaderBuilder());
    div.AppendChild(header);
    VElement body = VElement.Create("section");
    body.AppendChild(this.BodyBuilder());
    div.AppendChild(body);
    return div;
  }
}

Raw HTML, and its real security boundary

VElement.RawHtml sets a real, opaque innerHTML — an undiffed leaf, in place of TextContent/children, for a raw-markup-plus-one-delegated-listener pattern (this site's own header.ks/nav.ks use it exactly this way):

VElement header = VElement.Create("header");
header.RawHtml = SiteHeaderHtml; // a raw string ... from "./header.html"; constant
header.OnClick = (Event e) => { };  // one delegated listener over the whole subtree

It's unescaped, real innerHTML — never assign it anything reachable from user input. Every real use in this ecosystem is a raw string ... from "<path>.html"; compile-time constant (see KopScript's own docs) — genuinely static markup, baked in at build time, never a runtime value. Nothing in the type system stops el.RawHtml = someFetchedString; or el.RawHtml = formField.Value.Value; from compiling — both are a real XSS hole if that content is ever attacker-influenced. Use TextContent (always escaped) for any dynamic string; RawHtml is for static markup only.

HTTP

using "kopular";

Response r = await Http.Get("/api/dogs");
if (r.ok) {
  string body = await r.text();
  print(body);
}

await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
await Http.Put("/api/dogs/1", "{\"name\":\"Rexy\"}");
await Http.Patch("/api/dogs/1", "{\"name\":\"Max\"}");
await Http.Delete("/api/dogs/1");

Http is a thin, static wrapper over the real Fetch API — Get/Post/Put/Patch/ Delete, each returning task<Response> (.ok, .status, async text()). No HttpClient to inject, no RxJS Observable/operators, no interceptors — call it from anywhere, including straight out of a service's own methods.

No typed JSON deserialization — Response.text() gets you the raw body, nothing more. This isn't a corner cut for v1; it's a direct consequence of two things KopScript doesn't have: generic functions/methods (KopScript's generics are classes/interfaces only, so there's no safe way to write a general task<T> Get<T>(string url)) and object-literal syntax ({ ... } as a value — see below). If you want a typed response, describe its shape as its own extern class and parse it yourself with a per-shape extern ... as "JSON.parse" declaration — the same trust-based approach extern already uses for everything else, not a new mechanism:

extern class DogDto {
  string name { get; }
};
extern DogDto ParseDog(string json) as "JSON.parse";

string body = await (await Http.Get("/api/dogs/1")).text();
DogDto dog = ParseDog(body);   // unchecked, like a TypeScript `as DogDto` cast

Why Post/Put/Patch/Delete aren't just extern bindings straight to fetch, the way Get is: setting a request method/body/headers means passing fetch a second argument that's a plain JS object literal ({ method, headers, body }) — and KopScript has no object-literal syntax at all, so it can't construct one. src/http_runtime.js is one small hand-written function that does, and Get/Delete-with-no-body skip it entirely (fetch(url) alone needs no options object, so Get binds straight to the real global). It's the one file in this package not compiled from .ks — everywhere else avoids the problem by only wrapping JS APIs that take plain positional arguments (see dom.ks's addEventListener(string, handler), never an options-object-taking API).

Forms

using "kopular";

FormField<string> email = new FormField<string>("", (string v) => {
  string? required = Validators.Required(v);
  if (required != null) { return required; }
  return Validators.Email(v);
});

email.Value.Value = "not-an-email";
print(email.Error.Value);   // "Must be a valid email"
print(email.Valid());       // false

// inside a hand-written Render(), building emailInput as a VElement:
VElement emailInput = VElement.Create("input");
emailInput.OnInput = (Event e) => {
  email.Value.Value = e.target.value;   // revalidates automatically
};
emailInput.OnBlur = (Event e) => { email.Touch(); };

FormField<T> holds one input's value, error, and touched state as three ordinary state<T> boxes — .Value (the input's current value, revalidating on every assignment), .Error (string?, the current validator's message or null), and .Touched (bool, set by calling .Touch() — typically on blur, so a fresh field with an invalid initial value like an empty required field doesn't show an error before the user has typed anything). Subscribe to any of the three from your Component's constructor exactly like Counter's own state<number>, to re-render when they change.

A validator is a plain (T) => string? — null means valid, the same convention KopScript's own nullable types use elsewhere. There's no array-of-validators constructor parameter — KopScript has no syntax for an array of function values — so combining more than one check (as email does above) is an if-chain in one lambda, or, for the common case of just chaining a couple of already-built validators with no custom logic of their own, the CombineValidators2/CombineValidators3 free functions:

FormField<string> email = new FormField<string>("",
  CombineValidators2((string v) => Validators.Required(v), (string v) => Validators.Email(v)));

Free functions, not Validators methods, and fixed-arity (2 and 3) rather than a general Validators.All(...) — a class's own static method can't introduce a new type parameter beyond the class's own (see "Generics" in KopScript's own docs), so a generic combinator has to live as a free function instead. Validators ships the handful of checks almost every form needs (Required, MinLength, MaxLength, Email, Min, Max), each returning its own message; write your own validator function for anything more specific.

No two-way data binding in a hand-written Render() — wiring Value to a real <input> is the OnInput assignment shown above, the same manual pattern Counter already uses for its click handler (VElement.Value itself is one-way, host-to-DOM only — reading the current DOM value back out is always the real event's e.target.value, not something Kopular mirrors into Value for you). This is deliberate, not a missing feature: hand-written code already has direct field/handler access, so there's nothing for a magic binding to save you from writing. A template gets real sugar for exactly this — [(value)]="Field" desugars to [value]="Field" + (input)="Field = e.target.value" (see KopScript's own "Templates" docs) — since a markup file has no equivalent direct access to fall back on.

Computed values

React's useMemo/Angular signals' computed() equivalent — a read-only value derived from one or more state<T> sources, recomputed and re-notified whenever a source changes:

using "kopular";

state<number> price = state(10);
state<number> qty = state(2);
Computed2<number, number, number> total = new Computed2<number, number, number>(
  price, qty, (number p, number q) => p * q);

print(total.Value.Value);              // 20
total.Value.Subscribe((number v) => print("total: " + v));
price.Value = 15;                      // prints "total: 30"

Value is state<R>, not a bare R — KopScript's property grammar has no custom-getter syntax (only auto-implemented { get; }/{ get; set; }), so a computed value can't expose a property that recomputes itself on read. state<R> already is the right shape for "read the current value, or subscribe to it changing" — total.Value.Value/ total.Value.Subscribe(...) are the exact same two members any other state<T> has, not a parallel API to learn.

Dependencies are explicit, not automatically tracked the way Angular signals infer their own by watching which signals get read during evaluation — that needs a live "currently evaluating" context threaded through every read, a different kind of feature from anything else here. Naming each source directly is one honest step short of automatic, consistent with explicit constructor arguments everywhere else in Kopular (dependency injection, routing). Computed1<A, R> covers one source; Computed2<A, B, R> covers two — fixed arity for the same reason CombineValidators2/CombineValidators3 (above) are: KopScript has no array-of-function-values type, and no way to express "N sources of N different types" without one type parameter per source. Add a Computed3<A, B, C, R> the same way if a real derivation ever needs three.

Async data — Resource<T>

The loading/success/failure shape almost every real app needs around a task<T> — most commonly wrapping an Http call — as reactive state<T> a Component can render and Subscribe to, instead of hand-rolling the same three fields and try/catch every time:

using "kopular";

class DogsPage : Component {
  private Resource<Response> dogs;
  constructor() : base() {
    this.dogs = new Resource<Response>(Http.Get("https://dog.ceo/api/breeds/list/all"));
    this.dogs.Status.Subscribe((AsyncStatus s) => this.Update());
  }
}
public override VElement Render() {
  return match this.dogs.Status.Value {
    AsyncStatus.Loading => this.BuildSpinner(),
    AsyncStatus.Success => this.BuildList(this.dogs.Data.Value),
    AsyncStatus.Failure => this.BuildError(this.dogs.Error.Value)
  };
}

Status starts at AsyncStatus.Loading the moment Resource is constructed — the operation is already in flight (Http.Get(...) above is called before Resource ever sees it; a task<T> value can only be produced by calling an async function, and Resource's own constructor isn't one, so it always receives an already-started operation, never starts one itself). Its internal handling of that task is genuinely fire-and-forget from the constructor's point of view (there's no way to construct a task value outside an async function body to await it there instead — see KopScript's own "Async" docs) — Status/Data/Error transition together, exactly once, whichever branch the task actually takes. match over AsyncStatus gets the same real exhaustiveness checking any other enum match does — leaving out a case is a compile error, not a runtime blank screen.

Starting a new project: kp new

npx kp new my-app scaffolds a real, working project: a Counter component (src/counter.ks), an index.html with every Kopular module already mapped, and the vendor/serve scripts needed to run in a browser (a browser can't resolve a bare specifier like "kopular/component" the way Node's own module resolution does, so vendor-kopular.mjs copies Kopular's runtime into vendor/ and index.html's import map points there).

npx kp new my-app
cd my-app
npm install
npm start   # builds, vendors kopular's browser files, and serves at :8080

Its README.md points an AI agent at this package's own GUIDE.md before it starts generating code. kp ships from this package (not from kopscript's own ks CLI) since scaffolding a Kopular app is a framework concern, not a language one — ks stays a pure-language tool with no framework knowledge baked in.

Using Kopular from another KopScript project

using "kopular"; ([email protected]+) resolves this package's own KopScript declarations through node_modules — one line, every Kopular type in scope, with each file's own compiled output importing only what it actually references:

using "kopular";

class MyWidget : Component {
  public override VElement Render() {
    VElement el = VElement.Create("div");
    el.TextContent = "Hello from MyWidget";
    return el;
  }
}

See KopularDemo for a full working example (components, a service, and routing).

Before [email protected], every project hand-declared its own copy of Kopular's extern bindings instead — using couldn't reach into a package yet. That still works (kp new above can generate it), and is still the right call if you want a visibly trimmed subset — see LLM.md's "Consuming Kopular from your own KopScript project" for the complete, copy-ready block, Element/Document/Event included.

extern class can carry its own <T> (kopscript >= 0.5.0), so a generic export like FormField<T> describes the same way a real generic class does — see LLM.md's FormField<T>/Validators section for the full example.

Testing your own app: kopular/testing

A Component/Router graph can only be exercised end-to-end by actually compiling its .ks sources and running the result against a real DOM — there's no way to unit test one otherwise. Doing that by hand is a real ~50-line dance (a fresh temp dir per test, since Node's ESM module cache means re-importing the same compiled path twice never re-runs an ambient extern binding's top-level code — silently binding every later test to the first test's jsdom instance — compiling via kopscript's compileGraph, binding jsdom onto globalThis for document/Element/... to find, then restoring it). Both Kopular's own test suite and KopularDemo's used to hand-roll this independently; kopular/testing is that dance, written once:

import { runKopularApp } from "kopular/testing";

const { window, cleanup } = await runKopularApp(join(__dirname, "..", "src"), "app.ks", {
  includeKopularPackage: true, // your app consumes Kopular via `extern`, not relative `using`
});
try {
  expect(window.document.querySelector("h1")?.textContent).toBe("Hello");
} finally {
  cleanup(); // always — even on a thrown assertion — or the next test inherits these globals
}

jsdom is an optional peer dependency — installing kopular alone doesn't pull it in; only a project that actually calls runKopularApp needs it added too. See Kopular/test/kopular.test.ts (runKopularFixture, the sibling export used for testing Kopular's own source against inline fixtures) and KopularDemo's test/routed_app.test.ts for two real, different call sites.

Getting started (developing Kopular itself)

npm install   # pulls in kopscript as a devDependency
npm run build # compiles src/*.ks -> src/*.js (compiled output is gitignored)
npm test      # runs test/kopular.test.ts against a real DOM via jsdom

kopscript is a real published dependency (see package.json's own devDependencies for the exact version this repo currently requires — not restated by hand here, since it changes far more often than this paragraph does) — this repo doesn't need KopScript checked out as a sibling directory or anything else local to build or test.

Status

v1 / hobby-project scope, same as KopScript itself. Update() diffs and patches real DOM (see "Component" above) rather than replacing a whole subtree on every re-render, and a live child Component can be embedded directly in a parent's own tree via VElement.Mount(child) — see "Nested component composition" above — so a parent's re-render really can create/patch/reorder/tear down nested children declaratively now, closing what used to be documented here as the framework's biggest reconciliation gap. A template (.html) embeds one too, via *mount="expr" ([email protected]+), the same way *if/*for embed structural logic — see "Templates" in kopscript's own docs. A list child (Mounted or plain) without a stable VElement.Id still renders correctly across a reorder, but isn't guaranteed to keep the same real DOM node (see "Structural directives" above). Update() called before Mount() is a safe no-op — see LLM.md's Component section for exactly when that happens (sibling pages sharing one injected service's state<T>).