From c11c217004e7c9cf275bb882c0d599e6aa058871 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 12:59:03 -0400 Subject: [PATCH 01/11] Add Getting Started, API Reference, History, and utility docs New pages: - Getting Started: installation guide (CDN, package manager, TypeScript) - Browser Support: engine coverage, CI testing, ESM vs classic script - API Reference: quick-lookup index linking to detailed docs - Utility Functions: type checking, unwrapping, serialization, .fn - Loading & Saving Data: toJS/toJSON, fetch patterns, debugging - History: Knockout origins, Big Four era, enterprise adoption, why TKO in 2026 Also: - Update hero: "Battle-tested since 2010 / Ship in seconds" - Primary CTA links to Getting Started instead of Bindings - Sidebar: add Getting Started, API Reference, History sections Co-Authored-By: Claude Opus 4.6 (1M context) --- plans/legacy-docs.md | 72 +++++++++++ tko.io/astro.config.mjs | 5 +- tko.io/src/content/docs/api.md | 74 +++++++++++ .../docs/getting-started/browser-support.md | 42 +++++++ .../content/docs/getting-started/index.mdx | 119 ++++++++++++++++++ tko.io/src/content/docs/history.md | 75 +++++++++++ tko.io/src/content/docs/index.mdx | 6 +- .../src/content/docs/observables/json-data.md | 86 +++++++++++++ .../src/content/docs/observables/utilities.md | 92 ++++++++++++++ 9 files changed, 567 insertions(+), 4 deletions(-) create mode 100644 plans/legacy-docs.md create mode 100644 tko.io/src/content/docs/api.md create mode 100644 tko.io/src/content/docs/getting-started/browser-support.md create mode 100644 tko.io/src/content/docs/getting-started/index.mdx create mode 100644 tko.io/src/content/docs/history.md create mode 100644 tko.io/src/content/docs/observables/json-data.md create mode 100644 tko.io/src/content/docs/observables/utilities.md diff --git a/plans/legacy-docs.md b/plans/legacy-docs.md new file mode 100644 index 000000000..19256e83a --- /dev/null +++ b/plans/legacy-docs.md @@ -0,0 +1,72 @@ +# Plan: Write fresh docs for critical gaps + +**Risk class:** LOW — new documentation pages only, no code changes +**Owner:** brianmhunt + +## Context + +TKO's Starlight docs cover bindings, observables, computed, components, and binding context thoroughly — but lack foundational pages that new users need: installation, API reference, browser support, utility functions, and data serialization. Legacy Knockout docs covered these topics but are outdated. We're writing fresh content informed by the legacy structure. + +## New pages (5 files) + +### 1. `tko.io/src/content/docs/getting-started/index.md` — Installation & Setup +- Sidebar: `label: Overview, order: 0` +- CDN (ES module + classic script) for `build.reference` +- Package manager install (npm/bun/pnpm/yarn tabs) +- `build.knockout` as migration option with link to `/3to4/` +- First binding example (standalone HTML) +- TypeScript setup notes +- Landing page quick start stays as-is (quick taste vs full guide) + +### 2. `tko.io/src/content/docs/getting-started/browser-support.md` — Browser Support +- Modern browser engine coverage (Chromium, WebKit, Gecko) +- ES module support requirements +- Classic script fallback for older environments +- How TKO is tested (Vitest + Playwright, 3 engines in CI) + +### 3. `tko.io/src/content/docs/observables/utilities.md` — Utility Functions +- `ko.toJS` / `ko.toJSON` serialization +- `ko.unwrap` / `ko.isObservable` / `ko.isWritableObservable` / `ko.isComputed` +- `.fn` extensibility (`ko.observable.fn`, `ko.observableArray.fn`, etc.) +- Type hierarchy: subscribable → observable → observableArray/computed +- Working examples + +### 4. `tko.io/src/content/docs/observables/json-data.md` — Loading & Saving Data +- Serializing view models with `ko.toJS` / `ko.toJSON` +- Loading data into observables (manual assignment patterns) +- Fetch API examples (replacing legacy jQuery patterns) +- Debugging: rendering JSON in the UI + +### 5. `tko.io/src/content/docs/api.md` — API Reference +- Root-level page (add to sidebar config) +- Index/lookup table format: function name, one-liner, link to detailed page +- Organized by category: Observables, Computed, Components, Bindings, Utilities +- No full signatures — just names + links for quick navigation + +## Sidebar config update + +Edit `tko.io/astro.config.mjs` to add: +```js +{ label: 'Getting Started', autogenerate: { directory: 'getting-started' } }, +// ... existing sections ... +{ label: 'API Reference', slug: 'api' }, +``` + +Place "Getting Started" right after Introduction. Place "API Reference" before "Knockout 3 → 4 Guide". + +## Conventions to follow +- Frontmatter: `title` required, `description` + `sidebar` on index pages +- Filenames: kebab-case `.md` +- Code examples: use `` from `@astrojs/starlight/components` for multi-variant snippets (`.mdx` extension when using imports) +- Link to existing docs pages with relative paths +- Keep pages concise — reference detailed pages rather than duplicating + +## Files to modify +- `tko.io/astro.config.mjs` — sidebar config +- 5 new files as listed above + +## Verification +1. Run `bun run dev` in `tko.io/` and check all new pages render +2. Verify sidebar navigation order is correct +3. Check all internal links resolve +4. Review on mobile viewport diff --git a/tko.io/astro.config.mjs b/tko.io/astro.config.mjs index 69bfbbc40..84a8fa398 100644 --- a/tko.io/astro.config.mjs +++ b/tko.io/astro.config.mjs @@ -28,6 +28,7 @@ export default defineConfig({ }, sidebar: [ { label: 'Introduction', slug: 'index' }, + { label: 'Getting Started', autogenerate: { directory: 'getting-started' } }, { label: 'Examples', slug: 'examples' }, { label: 'Bindings', autogenerate: { directory: 'bindings' } }, { label: 'Observables', autogenerate: { directory: 'observables' } }, @@ -35,7 +36,9 @@ export default defineConfig({ { label: 'Components', autogenerate: { directory: 'components' } }, { label: 'Binding Context', autogenerate: { directory: 'binding-context' } }, { label: 'Advanced', autogenerate: { directory: 'advanced' } }, - { label: 'Knockout 3 → 4 Guide', slug: '3to4' } + { label: 'API Reference', slug: 'api' }, + { label: 'Knockout 3 → 4 Guide', slug: '3to4' }, + { label: 'History', slug: 'history' } ] }) ] diff --git a/tko.io/src/content/docs/api.md b/tko.io/src/content/docs/api.md new file mode 100644 index 000000000..edeece55f --- /dev/null +++ b/tko.io/src/content/docs/api.md @@ -0,0 +1,74 @@ +--- +title: API Reference +description: Quick-lookup index of TKO's public API. +--- + +# API Reference + +Quick-lookup table for TKO's public API. Each entry links to its detailed documentation. + +## Observables + +| Function | Description | +|----------|-------------| +| `ko.observable(value?)` | Create a reactive value. [Docs](/observables/) | +| `ko.observableArray(array?)` | Observable wrapper around an array with mutation methods. [Docs](/observables/observablearrays/) | +| `ko.isObservable(value)` | Check if a value is an observable. [Docs](/observables/utilities/) | +| `ko.isWritableObservable(value)` | Check if a value is a writable observable. [Docs](/observables/utilities/) | +| `ko.isSubscribable(value)` | Check if a value is any subscribable type. [Docs](/observables/utilities/) | +| `ko.unwrap(value)` | Read an observable's value, or return a plain value as-is. [Docs](/observables/utilities/) | +| `ko.toJS(object)` | Clone an object tree, replacing observables with their values. [Docs](/observables/utilities/) | +| `ko.toJSON(object, replacer?, space?)` | `ko.toJS` + `JSON.stringify`. [Docs](/observables/utilities/) | + +## Computed + +| Function | Description | +|----------|-------------| +| `ko.computed(evaluator, owner?, options?)` | Create a value that depends on other observables. [Docs](/computed/computedobservables/) | +| `ko.pureComputed(evaluator, owner?)` | Computed that sleeps when it has no subscribers. [Docs](/computed/computed-pure/) | +| `ko.isComputed(value)` | Check if a value is a computed observable. [Docs](/observables/utilities/) | +| `ko.when(predicate)` | Return a promise that resolves when the predicate becomes truthy. [Docs](/observables/) | + +## Extenders + +| Function | Description | +|----------|-------------| +| `observable.extend(extenders)` | Apply extenders to an observable or computed. [Docs](/observables/extenders/) | +| `rateLimit` | Throttle change notifications. [Docs](/observables/ratelimit-observable/) | +| `notify: 'always'` | Force notification even when value hasn't changed. [Docs](/observables/) | + +## Bindings + +| Function | Description | +|----------|-------------| +| `ko.applyBindings(viewModel, rootNode?)` | Activate bindings on a DOM subtree. [Docs](/observables/#activating-knockout) | +| `ko.applyBindingsToNode(node, bindings, viewModel?)` | Apply bindings to a single node programmatically. | +| `ko.cleanNode(node)` | Remove all TKO data and bindings from a node. | +| `ko.bindingHandlers` | Registry of built-in and custom binding handlers. [Docs](/binding-context/custom-bindings/) | + +### Built-in bindings + +**Text & HTML:** [`text`](/bindings/text-binding/), [`html`](/bindings/html-binding/), [`textInput`](/bindings/textinput-binding/), [`value`](/bindings/value-binding/) + +**Appearance:** [`visible`](/bindings/visible-binding/), [`css`](/bindings/css-binding/), [`style`](/bindings/style-binding/), [`attr`](/bindings/attr-binding/) + +**Control flow:** [`if`](/bindings/if-binding/), [`ifnot`](/bindings/ifnot-binding/), [`foreach`](/bindings/foreach-binding/), [`with`](/bindings/with-binding/), [`template`](/bindings/template-binding/) + +**Form:** [`click`](/bindings/click-binding/), [`event`](/bindings/event-binding/), [`submit`](/bindings/submit-binding/), [`enable`](/bindings/enable-binding/), [`disable`](/bindings/disable-binding/), [`checked`](/bindings/checked-binding/), [`options`](/bindings/options-binding/), [`selectedOptions`](/bindings/selectedoptions-binding/), [`hasfocus`](/bindings/hasfocus-binding/), [`uniqueName`](/bindings/uniquename-binding/) + +## Components + +| Function | Description | +|----------|-------------| +| `ko.components.register(name, config)` | Register a component. [Docs](/components/component-registration/) | +| `ko.components.get(name)` | Retrieve a registered component definition. [Docs](/components/component-loaders/) | +| `ko.components.isRegistered(name)` | Check if a component name is registered. | +| `ko.components.unregister(name)` | Remove a component registration. | + +## Utilities + +| Function | Description | +|----------|-------------| +| `ko.observable.fn` | Prototype for all observables — add methods here. [Docs](/observables/utilities/) | +| `ko.subscribable.fn` | Prototype for all subscribables. [Docs](/observables/utilities/) | +| `ko.computed.fn` | Prototype for all computeds. [Docs](/observables/utilities/) | diff --git a/tko.io/src/content/docs/getting-started/browser-support.md b/tko.io/src/content/docs/getting-started/browser-support.md new file mode 100644 index 000000000..61b94397e --- /dev/null +++ b/tko.io/src/content/docs/getting-started/browser-support.md @@ -0,0 +1,42 @@ +--- +title: Browser Support +--- + +TKO targets modern browsers — any browser that supports ES2020 and ` +``` + +An IIFE build is also available for environments that don't support ES modules: + +```html + + +``` + +## Server-side / Node.js + +TKO's observable and computed primitives have no DOM dependencies and can run in Node.js, Bun, or Deno. Binding and template features require a DOM environment. diff --git a/tko.io/src/content/docs/getting-started/index.mdx b/tko.io/src/content/docs/getting-started/index.mdx new file mode 100644 index 000000000..5b644798e --- /dev/null +++ b/tko.io/src/content/docs/getting-started/index.mdx @@ -0,0 +1,119 @@ +--- +title: Installation +description: How to add TKO to your project — CDN, ES module, or package manager. +sidebar: + label: Overview + order: 0 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +TKO ships two builds. Pick the one that fits your project: + +| Build | When to use | +|-------|-------------| +| `@tko/build.reference` | **New projects.** Modern features: TSX, `ko-*` attributes, native provider, strict equality. | +| `@tko/build.knockout` | **Migrating from Knockout 3.x.** Drop-in compatible. See the [migration guide](/3to4/). | + +The examples below use `build.reference`. Swap in `build.knockout` if you need Knockout compatibility. + +## CDN + +The fastest way to try TKO — no build step required. + + + + ```html + + ``` + + + ```html + + + ``` + + + +## Package manager + + + + ```sh + npm install @tko/build.reference + ``` + + + ```sh + bun add @tko/build.reference + ``` + + + ```sh + pnpm add @tko/build.reference + ``` + + + ```sh + yarn add @tko/build.reference + ``` + + + +Then import in your code: + +```js +import ko from '@tko/build.reference' +``` + +## First binding + +Here's a complete, self-contained example you can save as an HTML file and open in a browser: + +```html + + + +
+ +

Hello, .

+
+ + + + + +``` + +`ko.applyBindings` connects a view model to a DOM subtree. The `textInput` binding keeps the input in sync with the `name` observable, and the `text` binding displays its current value. + +## TypeScript + +TKO is written in TypeScript. Types are included — no separate `@types` package needed. + +```ts +import ko, { Observable } from '@tko/build.reference' + +const name: Observable = ko.observable('TKO') +``` + +## What to read next + +- [Bindings](/bindings/) — the full list of built-in bindings +- [Observables](/observables/) — how reactive state works +- [Examples](/examples/) — interactive demos diff --git a/tko.io/src/content/docs/history.md b/tko.io/src/content/docs/history.md new file mode 100644 index 000000000..5891bf538 --- /dev/null +++ b/tko.io/src/content/docs/history.md @@ -0,0 +1,75 @@ +--- +title: History +description: How Knockout became TKO — from Microsoft's Big Four framework to a modern observable UI library. +--- + +TKO is the modern successor to Knockout.js, the observable data-binding library created by Steve Sanderson in 2010. + +## Origins (2010) + +Steve Sanderson created Knockout independently, and the project's success led to him joining Microsoft's ASP.NET team. The core idea — observable values that automatically update the DOM when they change — was borrowed from the MVVM pattern that .NET developers already knew from WPF and Silverlight. + +The first release shipped in July 2010. By late that year, Scott Hanselman was interviewing Sanderson about it on Hanselminutes, and Microsoft began featuring it in official ASP.NET documentation. + +## The Big Four era (2011-2014) + +From 2011 to 2014, Knockout was one of the four dominant JavaScript frameworks alongside Backbone.js, AngularJS, and Ember.js. Every framework comparison article and conference talk of that era included all four. + +ThoughtWorks placed Knockout in their Technology Radar's "Trial" ring in October 2012, calling it one of the "front-runners" for building single-page applications. + +Knockout peaked in Stack Overflow question volume around 2013-2014, and had the highest answer rate (69.6%) of the Big Four — a sign of a mature, well-understood library. + +## Microsoft adoption + +Microsoft adopted Knockout extensively: + +- **Azure Portal** — described at VS Live 2016 as "the largest single-page application in the world," with 500+ developers building on Knockout and TypeScript +- **ASP.NET SPA template** — Visual Studio 2013 shipped with Knockout as the default JavaScript framework for new SPA projects +- **Hot Towel** — John Papa's widely-adopted SPA template combined Knockout with Durandal, Breeze, and Bootstrap +- **Office Add-ins** — Knockout was a supported framework for building Excel, Outlook, and Word add-ins + +The MVVM pattern felt natural to .NET developers, and the official template support made Knockout the de facto standard for .NET shops doing client-side work from roughly 2012 to 2015. + +## Enterprise adoption + +Beyond Microsoft, Knockout found deep roots in enterprise: + +- **Magento 2 (Adobe Commerce)** — chose Knockout as its frontend binding framework. Every Magento 2 e-commerce site runs Knockout. +- **Oracle JET** — Oracle built their entire enterprise JavaScript UI toolkit on top of Knockout rather than creating their own data-binding layer. +- **Financial services and government** — banks, insurance companies, and government agencies adopted Knockout for compliance-critical applications that are still running today. + +Notable adopters included Stack Overflow, Ancestry.com, BMW, TD Ameritrade, Royal Mail, Transport for London, and Bandcamp. Knockout accumulated over 36 million NuGet downloads. + +## The React shift (2015-2016) + +React changed the framework landscape starting around 2015. Unlike AngularJS — which had Google's continued investment and a full rewrite as Angular 2 — Knockout never received that level of sustained corporate backing for evolution. + +The library remained stable and reliable. Applications built on it continued to work. But new projects increasingly chose React, Vue, or Angular 2+, and Knockout gradually fell out of the top-tier framework conversation. + +By 2019, the last Knockout 3.x release (v3.5.1) shipped. The library wasn't broken — it just wasn't evolving. Steve Sanderson himself noted that maintaining a framework like Knockout requires a team, and without that sustained investment, the project stalled. + +## TKO (2016-present) + +TKO started as a monorepo reorganization of Knockout's codebase, breaking the monolithic library into 27 modular packages. The goals: + +- **Modernize the tooling** — TypeScript source, ES modules, Bun, Vitest, Biome +- **Keep backwards compatibility** — existing Knockout applications can migrate by swapping a script tag +- **Add modern features** — TSX support, `ko-*` attribute bindings, native providers, strict equality +- **Enable AI-assisted development** — comprehensive test coverage, machine-readable documentation, and tooling designed for autonomous maintenance. The "team" that Sanderson said a framework needs may no longer require humans for every role. + +## Why TKO in 2026 + +The frameworks that replaced Knockout optimized for large teams — elaborate build pipelines, complex state management, steep learning curves justified by organizational coordination. That tradeoff made sense when shipping required dozens of developers. + +AI changes the equation. When an AI agent can read, test, modify, and ship code autonomously, the bottleneck shifts from team coordination to iteration speed. The winning framework is the one with the lowest friction: fastest path from idea to working UI, instantly testable, trivially verifiable, easy to modify. + +TKO fits this model: + +- **Zero build step** — a single HTML file is a complete application +- **Instant feedback** — change an observable, the DOM updates. No reconciliation, no diffing +- **Small surface area** — 27 focused packages, each simple enough for an AI to fully understand +- **Fast verification** — 2700+ tests across three browser engines catch regressions instantly + +This is the [dark factory](https://github.com/knockout/tko/blob/main/plans/dark-factory.md) thesis: tooling, tests, and documentation robust enough that AI agents can handle routine maintenance autonomously. The "team" that every framework needs doesn't have to be all-human anymore. + +Over 70,000 websites still run Knockout today. TKO gives them — and a new generation of AI-assisted developers — a path forward. diff --git a/tko.io/src/content/docs/index.mdx b/tko.io/src/content/docs/index.mdx index 70d85db81..a097fafbf 100644 --- a/tko.io/src/content/docs/index.mdx +++ b/tko.io/src/content/docs/index.mdx @@ -7,10 +7,10 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';

TKO v4.0.1

-

Modern Knockout, clarified

-

Reactive data binding and UI templating with zero runtime dependencies. Start with the package you need and move from overview to working bindings.

+

Battle-tested since 2010

+

Observable data binding. Zero dependencies, zero ceremony. Ship in seconds.

diff --git a/tko.io/src/content/docs/observables/json-data.md b/tko.io/src/content/docs/observables/json-data.md new file mode 100644 index 000000000..0f9fb2450 --- /dev/null +++ b/tko.io/src/content/docs/observables/json-data.md @@ -0,0 +1,86 @@ +--- +title: Loading & Saving Data +--- + +# Loading and saving data + +TKO doesn't prescribe how you load or save data — use `fetch`, a library, or whatever your server expects. This page covers the patterns for moving data between your server and your observable view models. + +## Serializing a view model + +Use `ko.toJS` to strip observables from a view model before sending it to a server: + +```js +const vm = { + firstName: ko.observable('Jane'), + lastName: ko.observable('Doe'), + tags: ko.observableArray(['admin', 'active']) +} + +async function save() { + const data = ko.toJS(vm) + // data is { firstName: 'Jane', lastName: 'Doe', tags: ['admin', 'active'] } + + await fetch('/api/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) +} +``` + +Or use `ko.toJSON` as a shorthand for `JSON.stringify(ko.toJS(...))`: + +```js +await fetch('/api/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: ko.toJSON(vm) +}) +``` + +## Loading data into observables + +When you receive data from a server, update each observable individually: + +```js +async function load() { + const response = await fetch('/api/user') + const data = await response.json() + + vm.firstName(data.firstName) + vm.lastName(data.lastName) + vm.tags(data.tags) +} +``` + +For larger models, a helper function keeps things tidy: + +```js +function updateFrom(viewModel, data) { + for (const [key, value] of Object.entries(data)) { + if (ko.isWritableObservable(viewModel[key])) { + viewModel[key](value) + } + } +} + +// Usage +const data = await fetch('/api/user').then(r => r.json()) +updateFrom(vm, data) +``` + +## Debugging: showing JSON in the UI + +Bind `ko.toJSON` directly to see the live state of your view model: + +```html +

+```
+
+This updates in real time as observables change — useful during development.
+
+## See also
+
+- [Utility Functions](./utilities/) — `ko.toJS`, `ko.toJSON`, and type-checking helpers
+- [Observables](/observables/) — how reactive state works
diff --git a/tko.io/src/content/docs/observables/utilities.md b/tko.io/src/content/docs/observables/utilities.md
new file mode 100644
index 000000000..2bfeeb853
--- /dev/null
+++ b/tko.io/src/content/docs/observables/utilities.md
@@ -0,0 +1,92 @@
+---
+title: Utility Functions
+---
+
+# Utility functions
+
+TKO provides helper functions for inspecting and converting observables.
+
+## Type checking
+
+| Function | Returns `true` when |
+|----------|-------------------|
+| `ko.isObservable(value)` | The value is an observable, observable array, or computed |
+| `ko.isWritableObservable(value)` | The value is a writable observable (not a read-only computed) |
+| `ko.isComputed(value)` | The value is a computed observable |
+| `ko.isSubscribable(value)` | The value is any subscribable (observable, computed, or raw subscribable) |
+
+```js
+const name = ko.observable('TKO')
+const upper = ko.computed(() => name().toUpperCase())
+
+ko.isObservable(name)          // true
+ko.isObservable(upper)         // true
+ko.isObservable('plain')       // false
+ko.isWritableObservable(name)  // true
+ko.isWritableObservable(upper) // false
+ko.isComputed(upper)           // true
+```
+
+## Unwrapping
+
+`ko.unwrap(value)` reads the value if it's an observable, or returns it as-is if it isn't. Useful when a function accepts either an observable or a plain value:
+
+```js
+ko.unwrap(ko.observable(42))  // 42
+ko.unwrap(42)                 // 42
+```
+
+This is commonly used inside custom bindings where a parameter could be either type.
+
+## Serialization
+
+`ko.toJS` and `ko.toJSON` convert an object graph containing observables into plain data.
+
+`ko.toJS(viewModel)` — clones the object tree, replacing every observable with its current value:
+
+```js
+const vm = {
+  name: ko.observable('TKO'),
+  tags: ko.observableArray(['fast', 'reactive']),
+  nested: {
+    count: ko.observable(3)
+  }
+}
+
+ko.toJS(vm)
+// { name: 'TKO', tags: ['fast', 'reactive'], nested: { count: 3 } }
+```
+
+`ko.toJSON(viewModel, replacer?, space?)` — same as `ko.toJS` followed by `JSON.stringify`. Accepts the same optional `replacer` and `space` arguments as `JSON.stringify`:
+
+```js
+ko.toJSON(vm, null, 2)
+// Pretty-printed JSON string
+```
+
+See [Loading & Saving Data](./json-data/) for practical patterns.
+
+## Extending types with `.fn`
+
+Every observable inherits from a prototype chain you can extend:
+
+```
+subscribable.fn  →  observable.fn  →  observableArray (extends observable.fn)
+                 →  computed.fn
+```
+
+Adding a method to `ko.observable.fn` makes it available on all observables:
+
+```js
+ko.observable.fn.log = function (label) {
+  this.subscribe(value => console.log(label, value))
+  return this
+}
+
+const name = ko.observable('TKO').log('name changed:')
+name('v5')  // console: "name changed: v5"
+```
+
+Adding to `ko.subscribable.fn` affects everything — observables, computed, and observable arrays.
+
+Use `.fn` when the behavior is broadly useful. For one-off logic, prefer a regular function or [extender](/observables/extenders/) instead.

From 2004e80a360a1503c3f300bcabc4decd31b83027 Mon Sep 17 00:00:00 2001
From: Brian M Hunt 
Date: Thu, 16 Apr 2026 13:47:14 -0400
Subject: [PATCH 02/11] Address PR review feedback, restyle hero

- Add inline citations to history page (ThoughtWorks, SO blog, NuGet, VS Live)
- Fix .fn diagram to use observableArray.fn consistently
- Clarify IIFE as classic script-tag loading
- Wrap top-level await in async function
- Restyle version as clickable pill above hero (bun.sh-inspired)
- Hero heading: "Battle-tested since 2010 / Ship a dynamic web UI in seconds"
- Remove content panel border/radius for cleaner layout
- Add history page to sidebar

Co-Authored-By: Claude Opus 4.6 (1M context) 
---
 .../docs/getting-started/browser-support.md   |  2 +-
 tko.io/src/content/docs/history.md            |  8 +--
 tko.io/src/content/docs/index.mdx             |  7 ++-
 .../src/content/docs/observables/json-data.md |  6 +-
 .../src/content/docs/observables/utilities.md |  2 +-
 tko.io/src/styles/tko.css                     | 55 +++++++++++++------
 6 files changed, 51 insertions(+), 29 deletions(-)

diff --git a/tko.io/src/content/docs/getting-started/browser-support.md b/tko.io/src/content/docs/getting-started/browser-support.md
index 61b94397e..2e0ef8a9d 100644
--- a/tko.io/src/content/docs/getting-started/browser-support.md
+++ b/tko.io/src/content/docs/getting-started/browser-support.md
@@ -28,7 +28,7 @@ The recommended way to load TKO is as an ES module:
 
 ```
 
-An IIFE build is also available for environments that don't support ES modules:
+An IIFE build is also available for classic `
diff --git a/tko.io/src/content/docs/history.md b/tko.io/src/content/docs/history.md
index 5891bf538..11e4b066d 100644
--- a/tko.io/src/content/docs/history.md
+++ b/tko.io/src/content/docs/history.md
@@ -15,15 +15,15 @@ The first release shipped in July 2010. By late that year, Scott Hanselman was i
 
 From 2011 to 2014, Knockout was one of the four dominant JavaScript frameworks alongside Backbone.js, AngularJS, and Ember.js. Every framework comparison article and conference talk of that era included all four.
 
-ThoughtWorks placed Knockout in their Technology Radar's "Trial" ring in October 2012, calling it one of the "front-runners" for building single-page applications.
+[ThoughtWorks placed Knockout in their Technology Radar's "Trial" ring](https://www.thoughtworks.com/radar/languages-and-frameworks/angularjs-and-knockout) in October 2012, calling it one of the "front-runners" for building single-page applications.
 
-Knockout peaked in Stack Overflow question volume around 2013-2014, and had the highest answer rate (69.6%) of the Big Four — a sign of a mature, well-understood library.
+Knockout [peaked in Stack Overflow question volume](https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/) around 2013-2014, and had the highest answer rate (69.6%) of the Big Four — a sign of a mature, well-understood library.
 
 ## Microsoft adoption
 
 Microsoft adopted Knockout extensively:
 
-- **Azure Portal** — described at VS Live 2016 as "the largest single-page application in the world," with 500+ developers building on Knockout and TypeScript
+- **Azure Portal** — [described at VS Live 2016](https://learn.microsoft.com/en-us/shows/visual-studio-visual-studio-live-redmond-2016/t15) as "the largest single-page application in the world," with 500+ developers building on Knockout and TypeScript
 - **ASP.NET SPA template** — Visual Studio 2013 shipped with Knockout as the default JavaScript framework for new SPA projects
 - **Hot Towel** — John Papa's widely-adopted SPA template combined Knockout with Durandal, Breeze, and Bootstrap
 - **Office Add-ins** — Knockout was a supported framework for building Excel, Outlook, and Word add-ins
@@ -38,7 +38,7 @@ Beyond Microsoft, Knockout found deep roots in enterprise:
 - **Oracle JET** — Oracle built their entire enterprise JavaScript UI toolkit on top of Knockout rather than creating their own data-binding layer.
 - **Financial services and government** — banks, insurance companies, and government agencies adopted Knockout for compliance-critical applications that are still running today.
 
-Notable adopters included Stack Overflow, Ancestry.com, BMW, TD Ameritrade, Royal Mail, Transport for London, and Bandcamp. Knockout accumulated over 36 million NuGet downloads.
+Notable adopters included Stack Overflow, Ancestry.com, BMW, TD Ameritrade, Royal Mail, Transport for London, and Bandcamp. Knockout accumulated over [36 million NuGet downloads](https://www.nuget.org/packages/knockoutjs/).
 
 ## The React shift (2015-2016)
 
diff --git a/tko.io/src/content/docs/index.mdx b/tko.io/src/content/docs/index.mdx
index a097fafbf..d04897d08 100644
--- a/tko.io/src/content/docs/index.mdx
+++ b/tko.io/src/content/docs/index.mdx
@@ -5,10 +5,11 @@ description: Modern Knockout — reactive data binding and UI templating with ze
 
 import { Tabs, TabItem } from '@astrojs/starlight/components';
 
+TKO v4.0.1 is here →
+
 
-

TKO v4.0.1

-

Battle-tested since 2010

-

Observable data binding. Zero dependencies, zero ceremony. Ship in seconds.

+

Battle-tested since 2010

+

Observable data binding. Zero dependencies, zero ceremony. Ship a dynamic web UI in seconds.

Get started Try the Playground diff --git a/tko.io/src/content/docs/observables/json-data.md b/tko.io/src/content/docs/observables/json-data.md index 0f9fb2450..c7e91820e 100644 --- a/tko.io/src/content/docs/observables/json-data.md +++ b/tko.io/src/content/docs/observables/json-data.md @@ -66,8 +66,10 @@ function updateFrom(viewModel, data) { } // Usage -const data = await fetch('/api/user').then(r => r.json()) -updateFrom(vm, data) +async function load() { + const data = await fetch('/api/user').then(r => r.json()) + updateFrom(vm, data) +} ``` ## Debugging: showing JSON in the UI diff --git a/tko.io/src/content/docs/observables/utilities.md b/tko.io/src/content/docs/observables/utilities.md index 2bfeeb853..3dec11331 100644 --- a/tko.io/src/content/docs/observables/utilities.md +++ b/tko.io/src/content/docs/observables/utilities.md @@ -71,7 +71,7 @@ See [Loading & Saving Data](./json-data/) for practical patterns. Every observable inherits from a prototype chain you can extend: ``` -subscribable.fn → observable.fn → observableArray (extends observable.fn) +subscribable.fn → observable.fn → observableArray.fn → computed.fn ``` diff --git a/tko.io/src/styles/tko.css b/tko.io/src/styles/tko.css index 46c612d7b..3fb2b9960 100644 --- a/tko.io/src/styles/tko.css +++ b/tko.io/src/styles/tko.css @@ -134,11 +134,10 @@ header { .content-panel .sl-container, .right-sidebar-panel .sl-container { - background: var(--tko-surface); - border: 1px solid var(--tko-border); - border-radius: 1.25rem; - box-shadow: var(--tko-shadow); - backdrop-filter: blur(12px); + background: transparent; + border: none; + border-radius: 0; + box-shadow: none; } .content-panel .sl-container { @@ -231,15 +230,21 @@ header { box-shadow: 0 10px 24px -22px rgba(17, 24, 39, 0.22); } +/* Hide the Starlight page title when the hero is present */ +.content-panel:has(.landing-hero) [data-page-title], +.content-panel:has(.landing-hero) > .sl-container > :first-child:is(header, .content-title) { + display: none; +} + .landing-hero { margin: 0 0 1.4rem; - padding: clamp(1.25rem, 3vw, 2.1rem); + padding: clamp(1.5rem, 3.5vw, 2.8rem); border: 1px solid var(--tko-border); border-radius: 1.5rem; + box-shadow: var(--tko-shadow); background: radial-gradient(circle at top right, rgba(15, 98, 254, 0.12), transparent 34rem), linear-gradient(180deg, var(--tko-surface-strong), color-mix(in srgb, var(--tko-surface-strong) 78%, var(--sl-color-accent-low))); - box-shadow: var(--tko-shadow); } :root[data-theme='dark'] .landing-hero { @@ -257,20 +262,34 @@ header { color: var(--sl-color-text-accent); } -.landing-hero .landing-version { - margin: 0 0 0.3rem; - font-family: var(--tko-font-display); - font-size: clamp(2rem, 5vw, 3.2rem); - font-weight: 700; +.landing-version { + display: inline-block; + margin: 0 0 1rem; + padding: 0.25em 0.85em; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; + font-size: 0.82rem; + font-weight: 500; + letter-spacing: 0.03em; + color: var(--sl-color-gray-3); + text-decoration: none; + border: 1px solid var(--tko-border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.04); + transition: border-color 0.15s, color 0.15s; +} + +.landing-version:hover { color: var(--sl-color-white); + border-color: var(--sl-color-gray-4); } -.landing-hero h2:not(.landing-version) { - margin: 0; - max-width: 11ch; - font-size: clamp(1.55rem, 3.7vw, 2.55rem); - line-height: 1.02; - font-weight: 600; +.landing-hero h2 { + margin: 0 0 0.5rem; + font-family: var(--tko-font-display); + font-size: clamp(2.2rem, 5.5vw, 3.6rem); + line-height: 1.05; + font-weight: 800; + color: var(--sl-color-white); } .landing-lede { From 391951bff57e39d129753de3ff47dac36e3b8e11 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:21:01 -0400 Subject: [PATCH 03/11] Expand API reference, fix review feedback, restyle hero - Add missing APIs: isObservableArray, peek, isPureComputed, ignoreDependencies, contextFor, dataFor, applyBindingsToDescendants, BindingHandler, AsyncBindingHandler, bindingEvent, Component, JSX, domNodeDisposal, tasks - Add missing bindings: let, using, hidden, each, unless, else/elseif, component, slot, checkedValue, descendantsComplete, plus aliases - Add subscribable instance methods: when, yet, next, once, peek, dispose - Add isObservableArray to utilities.md type-checking table - Fix TypeScript example (remove non-existent named import) - Fix ko.computed and ko.when signatures - Add inline citations to history page - Restyle version as clickable pill, hero heading large/bold - Remove content panel border for cleaner layout Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/api.md | 65 ++++++++++++++++--- .../content/docs/getting-started/index.mdx | 4 +- .../src/content/docs/observables/utilities.md | 16 +++-- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/tko.io/src/content/docs/api.md b/tko.io/src/content/docs/api.md index edeece55f..d1c63cf39 100644 --- a/tko.io/src/content/docs/api.md +++ b/tko.io/src/content/docs/api.md @@ -14,8 +14,10 @@ Quick-lookup table for TKO's public API. Each entry links to its detailed docume | `ko.observable(value?)` | Create a reactive value. [Docs](/observables/) | | `ko.observableArray(array?)` | Observable wrapper around an array with mutation methods. [Docs](/observables/observablearrays/) | | `ko.isObservable(value)` | Check if a value is an observable. [Docs](/observables/utilities/) | +| `ko.isObservableArray(value)` | Check if a value is an observable array. [Docs](/observables/utilities/) | | `ko.isWritableObservable(value)` | Check if a value is a writable observable. [Docs](/observables/utilities/) | | `ko.isSubscribable(value)` | Check if a value is any subscribable type. [Docs](/observables/utilities/) | +| `ko.peek(value)` | Read an observable's value without creating a dependency. | | `ko.unwrap(value)` | Read an observable's value, or return a plain value as-is. [Docs](/observables/utilities/) | | `ko.toJS(object)` | Clone an object tree, replacing observables with their values. [Docs](/observables/utilities/) | | `ko.toJSON(object, replacer?, space?)` | `ko.toJS` + `JSON.stringify`. [Docs](/observables/utilities/) | @@ -24,10 +26,26 @@ Quick-lookup table for TKO's public API. Each entry links to its detailed docume | Function | Description | |----------|-------------| -| `ko.computed(evaluator, owner?, options?)` | Create a value that depends on other observables. [Docs](/computed/computedobservables/) | +| `ko.computed(evaluatorOrOptions, owner?, options?)` | Create a value that depends on other observables. Accepts a function or `{ read, write }` options object. [Docs](/computed/computedobservables/) | | `ko.pureComputed(evaluator, owner?)` | Computed that sleeps when it has no subscribers. [Docs](/computed/computed-pure/) | | `ko.isComputed(value)` | Check if a value is a computed observable. [Docs](/observables/utilities/) | -| `ko.when(predicate)` | Return a promise that resolves when the predicate becomes truthy. [Docs](/observables/) | +| `ko.isPureComputed(value)` | Check if a value is a pure computed. | +| `ko.ignoreDependencies(callback, owner?, args?)` | Run a function without tracking dependencies. | +| `ko.when(predicate, callback?, context?)` | Resolve when the predicate becomes truthy. Returns a promise, or calls callback if provided. [Docs](/observables/) | + +## Subscribable instance methods + +Every observable, computed, and observable array inherits these from `subscribable.fn`: + +| Method | Description | +|--------|-------------| +| `.subscribe(callback, target?, event?)` | Register a callback for changes. [Docs](/observables/) | +| `.when(testOrValue, returnValue?)` | Promise that resolves when the test passes. [Docs](/observables/) | +| `.yet(testOrValue, returnValue?)` | Promise that resolves when the test *fails* (negated `.when`). [Docs](/observables/) | +| `.next()` | Promise that resolves on the next value change. [Docs](/observables/) | +| `.once(callback)` | Call the callback on the next change, then auto-dispose. [Docs](/observables/) | +| `.peek()` | Read the current value without creating a dependency. | +| `.dispose()` | Tear down all subscriptions. | ## Extenders @@ -42,19 +60,31 @@ Quick-lookup table for TKO's public API. Each entry links to its detailed docume | Function | Description | |----------|-------------| | `ko.applyBindings(viewModel, rootNode?)` | Activate bindings on a DOM subtree. [Docs](/observables/#activating-knockout) | -| `ko.applyBindingsToNode(node, bindings, viewModel?)` | Apply bindings to a single node programmatically. | +| `ko.applyBindingsToNode(node, bindings, viewModelOrContext?)` | Apply bindings to a single node programmatically. | +| `ko.applyBindingsToDescendants(viewModelOrContext, rootNode)` | Apply bindings to descendants only (used in custom bindings). [Docs](/binding-context/custom-bindings-controlling-descendant-bindings/) | +| `ko.contextFor(node)` | Get the binding context for a DOM node. | +| `ko.dataFor(node)` | Get the view model bound to a DOM node. | | `ko.cleanNode(node)` | Remove all TKO data and bindings from a node. | | `ko.bindingHandlers` | Registry of built-in and custom binding handlers. [Docs](/binding-context/custom-bindings/) | +| `ko.bindingEvent` | Binding lifecycle event constants (e.g., `childrenComplete`, `descendantsComplete`). | +| `ko.BindingHandler` | Base class for class-based custom binding handlers. [Docs](/binding-context/custom-bindings/) | +| `ko.AsyncBindingHandler` | Async variant of `BindingHandler` for bindings that load resources. [Docs](/binding-context/custom-bindings/) | ### Built-in bindings **Text & HTML:** [`text`](/bindings/text-binding/), [`html`](/bindings/html-binding/), [`textInput`](/bindings/textinput-binding/), [`value`](/bindings/value-binding/) -**Appearance:** [`visible`](/bindings/visible-binding/), [`css`](/bindings/css-binding/), [`style`](/bindings/style-binding/), [`attr`](/bindings/attr-binding/) +**Appearance:** [`visible`](/bindings/visible-binding/), `hidden`, [`css`](/bindings/css-binding/) (alias: `class`), [`style`](/bindings/style-binding/), [`attr`](/bindings/attr-binding/) + +**Control flow:** [`if`](/bindings/if-binding/), [`ifnot`](/bindings/ifnot-binding/) (alias: `unless`), `else`, `elseif`, [`foreach`](/bindings/foreach-binding/) (alias: `each`), [`with`](/bindings/with-binding/), [`template`](/bindings/template-binding/) -**Control flow:** [`if`](/bindings/if-binding/), [`ifnot`](/bindings/ifnot-binding/), [`foreach`](/bindings/foreach-binding/), [`with`](/bindings/with-binding/), [`template`](/bindings/template-binding/) +**Context:** `let`, `using` -**Form:** [`click`](/bindings/click-binding/), [`event`](/bindings/event-binding/), [`submit`](/bindings/submit-binding/), [`enable`](/bindings/enable-binding/), [`disable`](/bindings/disable-binding/), [`checked`](/bindings/checked-binding/), [`options`](/bindings/options-binding/), [`selectedOptions`](/bindings/selectedoptions-binding/), [`hasfocus`](/bindings/hasfocus-binding/), [`uniqueName`](/bindings/uniquename-binding/) +**Form:** [`click`](/bindings/click-binding/), [`event`](/bindings/event-binding/) (alias: `on`), [`submit`](/bindings/submit-binding/), [`enable`](/bindings/enable-binding/), [`disable`](/bindings/disable-binding/), [`checked`](/bindings/checked-binding/), `checkedValue`, [`options`](/bindings/options-binding/), [`selectedOptions`](/bindings/selectedoptions-binding/), [`hasfocus`](/bindings/hasfocus-binding/), [`uniqueName`](/bindings/uniquename-binding/) + +**Components:** `component`, `slot` + +**Lifecycle:** `descendantsComplete` ## Components @@ -64,11 +94,30 @@ Quick-lookup table for TKO's public API. Each entry links to its detailed docume | `ko.components.get(name)` | Retrieve a registered component definition. [Docs](/components/component-loaders/) | | `ko.components.isRegistered(name)` | Check if a component name is registered. | | `ko.components.unregister(name)` | Remove a component registration. | +| `ko.components.clearCachedDefinition(name)` | Clear a cached component definition. | +| `ko.Component` | Base class for class-based components (`ComponentABC`). [Docs](/components/) | -## Utilities +## JSX (build.reference only) + +| Function | Description | +|----------|-------------| +| `ko.jsx.createElement(tag, props, ...children)` | Create a JSX element. Used as the JSX factory. | +| `ko.jsx.Fragment` | Fragment component for grouping elements without a wrapper node. | +| `ko.jsx.render(jsx)` | Render JSX to DOM nodes. Returns `{ node, dispose }`. | + +## DOM disposal + +| Function | Description | +|----------|-------------| +| `ko.domNodeDisposal.addDisposeCallback(node, callback)` | Run a callback when a DOM node is removed by TKO. | +| `ko.domNodeDisposal.removeDisposeCallback(node, callback)` | Remove a previously registered disposal callback. | + +## Extensibility | Function | Description | |----------|-------------| -| `ko.observable.fn` | Prototype for all observables — add methods here. [Docs](/observables/utilities/) | | `ko.subscribable.fn` | Prototype for all subscribables. [Docs](/observables/utilities/) | +| `ko.observable.fn` | Prototype for all observables — add methods here. [Docs](/observables/utilities/) | +| `ko.observableArray.fn` | Prototype for observable arrays (`remove`, `replace`, etc.). [Docs](/observables/utilities/) | | `ko.computed.fn` | Prototype for all computeds. [Docs](/observables/utilities/) | +| `ko.tasks` | Microtask scheduler for batching async work. | diff --git a/tko.io/src/content/docs/getting-started/index.mdx b/tko.io/src/content/docs/getting-started/index.mdx index 5b644798e..81c6ff3a8 100644 --- a/tko.io/src/content/docs/getting-started/index.mdx +++ b/tko.io/src/content/docs/getting-started/index.mdx @@ -107,9 +107,9 @@ Here's a complete, self-contained example you can save as an HTML file and open TKO is written in TypeScript. Types are included — no separate `@types` package needed. ```ts -import ko, { Observable } from '@tko/build.reference' +import ko from '@tko/build.reference' -const name: Observable = ko.observable('TKO') +const name = ko.observable('TKO') // inferred as Observable ``` ## What to read next diff --git a/tko.io/src/content/docs/observables/utilities.md b/tko.io/src/content/docs/observables/utilities.md index 3dec11331..8b5c2fa4d 100644 --- a/tko.io/src/content/docs/observables/utilities.md +++ b/tko.io/src/content/docs/observables/utilities.md @@ -11,20 +11,24 @@ TKO provides helper functions for inspecting and converting observables. | Function | Returns `true` when | |----------|-------------------| | `ko.isObservable(value)` | The value is an observable, observable array, or computed | +| `ko.isObservableArray(value)` | The value is an observable array | | `ko.isWritableObservable(value)` | The value is a writable observable (not a read-only computed) | | `ko.isComputed(value)` | The value is a computed observable | | `ko.isSubscribable(value)` | The value is any subscribable (observable, computed, or raw subscribable) | ```js const name = ko.observable('TKO') +const tags = ko.observableArray(['fast']) const upper = ko.computed(() => name().toUpperCase()) -ko.isObservable(name) // true -ko.isObservable(upper) // true -ko.isObservable('plain') // false -ko.isWritableObservable(name) // true -ko.isWritableObservable(upper) // false -ko.isComputed(upper) // true +ko.isObservable(name) // true +ko.isObservable(upper) // true +ko.isObservable('plain') // false +ko.isObservableArray(tags) // true +ko.isObservableArray(name) // false +ko.isWritableObservable(name) // true +ko.isWritableObservable(upper) // false +ko.isComputed(upper) // true ``` ## Unwrapping From a31bc593c33dacc0dcd4efe3246ef0f6cb089c22 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:22:42 -0400 Subject: [PATCH 04/11] Collapse reference sections in sidebar by default Bindings, Observables, Computed, Components, Binding Context, and Advanced all start collapsed. Getting Started, Examples, API Reference, and History remain visible at the top/bottom. Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/astro.config.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tko.io/astro.config.mjs b/tko.io/astro.config.mjs index 84a8fa398..efadbf403 100644 --- a/tko.io/astro.config.mjs +++ b/tko.io/astro.config.mjs @@ -30,12 +30,12 @@ export default defineConfig({ { label: 'Introduction', slug: 'index' }, { label: 'Getting Started', autogenerate: { directory: 'getting-started' } }, { label: 'Examples', slug: 'examples' }, - { label: 'Bindings', autogenerate: { directory: 'bindings' } }, - { label: 'Observables', autogenerate: { directory: 'observables' } }, - { label: 'Computed', autogenerate: { directory: 'computed' } }, - { label: 'Components', autogenerate: { directory: 'components' } }, - { label: 'Binding Context', autogenerate: { directory: 'binding-context' } }, - { label: 'Advanced', autogenerate: { directory: 'advanced' } }, + { label: 'Bindings', collapsed: true, autogenerate: { directory: 'bindings' } }, + { label: 'Observables', collapsed: true, autogenerate: { directory: 'observables' } }, + { label: 'Computed', collapsed: true, autogenerate: { directory: 'computed' } }, + { label: 'Components', collapsed: true, autogenerate: { directory: 'components' } }, + { label: 'Binding Context', collapsed: true, autogenerate: { directory: 'binding-context' } }, + { label: 'Advanced', collapsed: true, autogenerate: { directory: 'advanced' } }, { label: 'API Reference', slug: 'api' }, { label: 'Knockout 3 → 4 Guide', slug: '3to4' }, { label: 'History', slug: 'history' } From 104199644d804fb09bc79ff1b66abaaab7389709 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:26:13 -0400 Subject: [PATCH 05/11] Prefer arrow functions in doc examples Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/observables/json-data.md | 10 +++++----- tko.io/src/content/docs/observables/utilities.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tko.io/src/content/docs/observables/json-data.md b/tko.io/src/content/docs/observables/json-data.md index c7e91820e..19ec9c76c 100644 --- a/tko.io/src/content/docs/observables/json-data.md +++ b/tko.io/src/content/docs/observables/json-data.md @@ -17,7 +17,7 @@ const vm = { tags: ko.observableArray(['admin', 'active']) } -async function save() { +const save = async () => { const data = ko.toJS(vm) // data is { firstName: 'Jane', lastName: 'Doe', tags: ['admin', 'active'] } @@ -44,7 +44,7 @@ await fetch('/api/user', { When you receive data from a server, update each observable individually: ```js -async function load() { +const load = async () => { const response = await fetch('/api/user') const data = await response.json() @@ -54,10 +54,10 @@ async function load() { } ``` -For larger models, a helper function keeps things tidy: +For larger models, a helper keeps things tidy: ```js -function updateFrom(viewModel, data) { +const updateFrom = (viewModel, data) => { for (const [key, value] of Object.entries(data)) { if (ko.isWritableObservable(viewModel[key])) { viewModel[key](value) @@ -66,7 +66,7 @@ function updateFrom(viewModel, data) { } // Usage -async function load() { +const load = async () => { const data = await fetch('/api/user').then(r => r.json()) updateFrom(vm, data) } diff --git a/tko.io/src/content/docs/observables/utilities.md b/tko.io/src/content/docs/observables/utilities.md index 8b5c2fa4d..f5adcbfbd 100644 --- a/tko.io/src/content/docs/observables/utilities.md +++ b/tko.io/src/content/docs/observables/utilities.md @@ -82,8 +82,8 @@ subscribable.fn → observable.fn → observableArray.fn Adding a method to `ko.observable.fn` makes it available on all observables: ```js -ko.observable.fn.log = function (label) { - this.subscribe(value => console.log(label, value)) +ko.observable.fn.log = function (label) { // note: must be function, not arrow, to preserve `this` + this.subscribe(v => console.log(label, v)) return this } From c8f9d056f09c885eee30e2e93394d0589a7f2e1f Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:37:26 -0400 Subject: [PATCH 06/11] Add deploy guide, restyle hero heading, clarify browser support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New getting-started/deploy.md: GitHub Pages, Cloudflare Pages, GCS, Firebase Hosting, Netlify — fulfills the "ship in seconds" promise - Hero: "TKO" as massive heading, "Battle-tested since 2010" as kicker - Browser support: honest about untested older versions, invite bug reports Co-Authored-By: Claude Opus 4.6 (1M context) --- .../docs/getting-started/browser-support.md | 6 +- .../content/docs/getting-started/deploy.md | 101 ++++++++++++++++++ tko.io/src/content/docs/index.mdx | 3 +- tko.io/src/styles/tko.css | 9 +- 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 tko.io/src/content/docs/getting-started/deploy.md diff --git a/tko.io/src/content/docs/getting-started/browser-support.md b/tko.io/src/content/docs/getting-started/browser-support.md index 2e0ef8a9d..2b675be48 100644 --- a/tko.io/src/content/docs/getting-started/browser-support.md +++ b/tko.io/src/content/docs/getting-started/browser-support.md @@ -14,10 +14,14 @@ TKO targets modern browsers — any browser that supports ES2020 and ` + + +``` + +Upload that single file to any of the platforms below. That's it — a live, reactive web UI. + +## GitHub Pages + +Free, deploys from a git push. + +1. Create a repo and add your `index.html` +2. Go to **Settings → Pages → Source** and select your branch +3. Your site is live at `https://username.github.io/repo/` + +Or use the `gh` CLI: + +```sh +gh repo create my-app --public --clone +# add index.html +git add index.html && git commit -m "init" && git push +gh browse --settings # enable Pages under Settings → Pages +``` + +## Cloudflare Pages + +Free tier, global CDN, automatic HTTPS. + +1. Push your files to a GitHub or GitLab repo +2. Connect the repo at [dash.cloudflare.com](https://dash.cloudflare.com) → **Pages → Create** +3. No build command needed — just set the output directory to `/` (or wherever your HTML lives) + +Or deploy directly from the CLI: + +```sh +npx wrangler pages deploy . --project-name my-app +``` + +## Google Cloud Storage + +Good for projects already on GCP. + +```sh +gsutil mb gs://my-app.example.com +gsutil web set -m index.html gs://my-app.example.com +gsutil cp index.html gs://my-app.example.com/ +``` + +Add a load balancer or use [Firebase Hosting](https://firebase.google.com/docs/hosting) for automatic HTTPS and CDN. + +## Firebase Hosting + +Free tier, automatic HTTPS, global CDN. + +```sh +npm install -g firebase-tools +firebase init hosting # select your project, set public dir to "." +firebase deploy +``` + +## Netlify + +Free tier, drag-and-drop or git-based deploys. + +1. Go to [app.netlify.com/drop](https://app.netlify.com/drop) +2. Drag your project folder onto the page +3. Live in seconds + +Or via CLI: + +```sh +npx netlify-cli deploy --dir . --prod +``` + +## Why this works + +TKO loads from a CDN (`esm.run` or `jsdelivr`). Your app is just HTML + the browser's ES module loader. No server-side rendering, no Node.js, no build artifacts. The entire deploy is one file. + +As your app grows you can add a bundler, but you don't *have* to. Many production TKO apps are a handful of HTML files and a CSS stylesheet. diff --git a/tko.io/src/content/docs/index.mdx b/tko.io/src/content/docs/index.mdx index d04897d08..b8a3c8b04 100644 --- a/tko.io/src/content/docs/index.mdx +++ b/tko.io/src/content/docs/index.mdx @@ -8,7 +8,8 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; TKO v4.0.1 is here →
-

Battle-tested since 2010

+

TKO

+

Battle-tested since 2010

Observable data binding. Zero dependencies, zero ceremony. Ship a dynamic web UI in seconds.

Get started diff --git a/tko.io/src/styles/tko.css b/tko.io/src/styles/tko.css index 3fb2b9960..c6d26b47c 100644 --- a/tko.io/src/styles/tko.css +++ b/tko.io/src/styles/tko.css @@ -284,11 +284,12 @@ header { } .landing-hero h2 { - margin: 0 0 0.5rem; + margin: 0 0 0.2rem; font-family: var(--tko-font-display); - font-size: clamp(2.2rem, 5.5vw, 3.6rem); - line-height: 1.05; - font-weight: 800; + font-size: clamp(3.5rem, 8vw, 6rem); + line-height: 1; + font-weight: 900; + letter-spacing: -0.03em; color: var(--sl-color-white); } From 0b787489f5e3fe136d2f393eeeb988d99e644c4f Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:37:56 -0400 Subject: [PATCH 07/11] Fix deploy intro: TKO apps can be static, not always static Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/getting-started/deploy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tko.io/src/content/docs/getting-started/deploy.md b/tko.io/src/content/docs/getting-started/deploy.md index cf7a176ec..604e54aad 100644 --- a/tko.io/src/content/docs/getting-started/deploy.md +++ b/tko.io/src/content/docs/getting-started/deploy.md @@ -3,7 +3,7 @@ title: Deploy description: Put a TKO app online in seconds — no build step required. --- -TKO apps are static HTML files. No bundler, no server runtime, no build step. Drop a file anywhere that serves HTML and you're live. +TKO apps can be as simple as a single HTML file — no bundler, no server runtime, no build step. Drop a file anywhere that serves HTML and you're live. ## The simplest deploy From 28cb2e7a8cb532f0c116a7d4ca98180326dd73a6 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:38:12 -0400 Subject: [PATCH 08/11] Rename deploy page to Static Deploy Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/getting-started/deploy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tko.io/src/content/docs/getting-started/deploy.md b/tko.io/src/content/docs/getting-started/deploy.md index 604e54aad..7e932c04b 100644 --- a/tko.io/src/content/docs/getting-started/deploy.md +++ b/tko.io/src/content/docs/getting-started/deploy.md @@ -1,9 +1,9 @@ --- -title: Deploy -description: Put a TKO app online in seconds — no build step required. +title: Static Deploy +description: Deploy a TKO app to static hosting — no build step required. --- -TKO apps can be as simple as a single HTML file — no bundler, no server runtime, no build step. Drop a file anywhere that serves HTML and you're live. +A TKO app can be as simple as a single HTML file. No bundler, no server runtime, no build step — just upload to any static hosting provider and you're live. ## The simplest deploy From df0d2efae67e7e6fea8070ebc45c54af1cb2e21c Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:38:53 -0400 Subject: [PATCH 09/11] Rename to "Deploy in Seconds" Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/getting-started/deploy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tko.io/src/content/docs/getting-started/deploy.md b/tko.io/src/content/docs/getting-started/deploy.md index 7e932c04b..f7de3edfb 100644 --- a/tko.io/src/content/docs/getting-started/deploy.md +++ b/tko.io/src/content/docs/getting-started/deploy.md @@ -1,5 +1,5 @@ --- -title: Static Deploy +title: Deploy in Seconds description: Deploy a TKO app to static hosting — no build step required. --- From c7108eefd2999d5d79ed77fdc5335fc0ab0aefaa Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:39:15 -0400 Subject: [PATCH 10/11] Use gcloud storage instead of deprecated gsutil Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/getting-started/deploy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tko.io/src/content/docs/getting-started/deploy.md b/tko.io/src/content/docs/getting-started/deploy.md index f7de3edfb..cd63e296e 100644 --- a/tko.io/src/content/docs/getting-started/deploy.md +++ b/tko.io/src/content/docs/getting-started/deploy.md @@ -63,9 +63,9 @@ npx wrangler pages deploy . --project-name my-app Good for projects already on GCP. ```sh -gsutil mb gs://my-app.example.com -gsutil web set -m index.html gs://my-app.example.com -gsutil cp index.html gs://my-app.example.com/ +gcloud storage buckets create gs://my-app.example.com +gcloud storage buckets update gs://my-app.example.com --web-main-page-suffix=index.html +gcloud storage cp index.html gs://my-app.example.com/ ``` Add a load balancer or use [Firebase Hosting](https://firebase.google.com/docs/hosting) for automatic HTTPS and CDN. From 5425ba023a16a51c406c34dc289339d8f8732173 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Thu, 16 Apr 2026 14:40:36 -0400 Subject: [PATCH 11/11] Use IIFE build as primary deploy example, ESM as alternative The ESM import may not work in all playground/sandbox environments. IIFE via jsdelivr is more universally compatible. Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/src/content/docs/getting-started/deploy.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tko.io/src/content/docs/getting-started/deploy.md b/tko.io/src/content/docs/getting-started/deploy.md index cd63e296e..fe74a1568 100644 --- a/tko.io/src/content/docs/getting-started/deploy.md +++ b/tko.io/src/content/docs/getting-started/deploy.md @@ -17,14 +17,24 @@ Save this as `index.html`:

Hello, .

- + ``` +Or as an ES module (no IIFE needed): + +```html + +``` + Upload that single file to any of the platforms below. That's it — a live, reactive web UI. ## GitHub Pages