Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ builds/**/meta
.vscode/settings.json
.playwright-mcp
.playwright-cli
test-results/
.claude/worktrees/
__screenshots__
1 change: 1 addition & 0 deletions tko.io/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default defineConfig({
sidebar: [
{ label: 'Introduction', slug: 'index' },
{ label: 'Knockout 3 to 4 Guide', slug: '3to4' },
{ label: 'Examples', slug: 'examples' },
{ label: 'Bindings', autogenerate: { directory: 'bindings' } },
{ label: 'Observables', autogenerate: { directory: 'observables' } },
{ label: 'Computed', autogenerate: { directory: 'computed' } },
Expand Down
4 changes: 4 additions & 0 deletions tko.io/public/agent-guide.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# TKO Agent Guide

For preferred state/binding/DOM architecture in examples and prototypes, read `/agents/contract.md`.

## Setup

```html
Expand Down Expand Up @@ -31,6 +33,8 @@ ko.when(() => viewModel.isReady() && viewModel.hasData()).then(() => console.log

Activate with `ko.applyBindings(viewModel, element)`.

Looking up that mount element with `document.getElementById(...)`, `querySelector(...)`, or another host reference is normal. The important rule is that once bindings are active, observable state should remain the source of truth instead of ad-hoc DOM mutation.

Syntax: `data-bind="bindingName: expression"` on HTML elements.

text, html, visible, hidden, css:{class:bool}, style:{prop:val}, attr:{name:val},
Expand Down
93 changes: 93 additions & 0 deletions tko.io/public/agents/contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# TKO Agent Contract

Use this file when deciding how state, bindings, and DOM work should be divided in TKO examples and prototypes.

## Core Rule

- Keep app state in observables, observableArrays, and computeds.
- Let standard bindings and custom bindings carry that state to the DOM.
- Use DOM lookup only to find the mount root for `ko.applyBindings(viewModel, element)`.

## Replace X With Y

- This section is about replacing ad-hoc DOM/event/state handling with bindings, not about binding-syntax style.
- If you are about to do `element.textContent = value`, use the `text` binding.
- If you are about to do `element.innerHTML = markup`, first ask whether the content should be plain text instead; prefer the `text` binding by default. Use `html` only when rendering trusted HTML is truly the point.
- If you are about to manually create, replace, or reconcile a repeated set of child nodes, use `foreach`.
- If you are about to toggle classes with `classList`, use `css`.
- If you are about to set attributes manually, use `attr`.
- If you are about to set inline styles from state, use `style`.
- If you are about to wire ordinary UI events with imperative listeners, use `click`, `event:{...}`, `value`, `textInput`, `checked`, or related built-in bindings.
- If you are about to call `focus()` or manage focus from state, use `hasFocus` when it fits.
- If you are about to mirror user input into plain mutable objects, store that input in observables instead.
- If you are about to keep counters, highlights, or explanatory UI state outside observables, move them into observables/computeds so the example demonstrates TKO rather than bypassing it.

## When Custom Bindings Are The Right Tool

Use a custom `bindingHandler` when the work is inherently DOM-specific and does not belong in the state layer.

Typical good fits:
- canvas drawing
- WebGL rendering
- SVG-specific effects
- animation
- resize / measurement
- focus orchestration when `hasFocus` is not enough
- third-party widget integration

In those cases:
- let observables remain the source of truth
- let the custom binding read observables and update the DOM
- avoid making the custom binding the authoritative owner of app state

## Security Preference

- Prefer `text` over `html`.
- Treat `html` as an exception for trusted markup, not the default way to render content.
- If the content originates from users, external services, or mixed trust levels, do not pass it through `html` unless it has been explicitly sanitized for that purpose.

## Mounting Is Allowed

These are normal:

```js
const root = document.getElementById('app')
ko.applyBindings(viewModel, root)
```

```js
const root = container.querySelector('[data-app-root]')
ko.applyBindings(viewModel, root)
```

The contract is not “never touch `document`”.
The contract is “do not let ad-hoc DOM mutation become your reactive state system”.

## Binding Syntax Preference

- `ko-*` and `data-bind` are both valid binding surfaces.
- The choice between them is primarily stylistic / authoring-oriented unless you specifically need classic provider-driven strings or are teaching the classic syntax directly.

## Render Loops

Render loops are acceptable when they belong to rendering:
- `requestAnimationFrame`
- canvas redraws
- WebGL frame submission
- resize observers

Prefer this split:
- state object: observables + domain actions
- renderer / custom binding: DOM, canvas, WebGL, RAF

## Tests

Direct DOM reads are fine in tests and verification code.
Example:

```js
ko.applyBindings(vm, document.getElementById('app'))
console.assert(document.querySelector('#app span').textContent === 'Hello')
```

That is verification code, not the app’s state/update architecture.
18 changes: 17 additions & 1 deletion tko.io/public/agents/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

Test-backed behavior summaries live under `/agents/verified-behaviors/`. Treat those files as the contract layer when prose docs and implementation need reconciliation.

For preferred state/binding/DOM architecture in examples and prototypes, read `/agents/contract.md`.

## Setup

```html
<script src="https://tko.io/lib/tko.js"></script>
<script>window.ko = window.tko</script>
<script>const ko = globalThis.tko</script>
```

## Observables
Expand Down Expand Up @@ -49,6 +51,18 @@ Binding notes:
<textarea data-bind="textInput: notes"></textarea>
```

## Example Discipline

When the goal is to demonstrate TKO itself, keep the state flow inside observables, computeds, and bindings.

- If you want a replacement-oriented checklist for DOM/state decisions, use `/agents/contract.md`.
- It is normal to look up a mount element with `document.getElementById(...)`, `querySelector(...)`, or another host-framework reference so you can call `ko.applyBindings(viewModel, element)`.
- Prefer `text`, `css`, `attr`, `event`, `foreach`, and `pureComputed` over manual DOM writes.
- Avoid driving visible state with `textContent`, `innerHTML`, `classList`, or ad-hoc `addEventListener` when bindings can express the same behavior.
- Use custom `bindingHandlers` only for DOM-specific effects that do not belong in the state layer, such as animation, focus, canvas, SVG, or third-party widget integration.
- If an example contrasts reactive models, the counters and highlighted state should also be observable-driven so the example demonstrates the pattern instead of bypassing it.
- The line to avoid is using the DOM itself as the mutable source of truth after bindings are active.

## Classic data-bind parsing and CSP

Classic `data-bind` parsing is provider-driven. Use `DataBindProvider` when you need binding strings, and combine it with other providers through `MultiProvider` as needed.
Expand Down Expand Up @@ -211,6 +225,8 @@ tko.applyBindings({ removeTodo: t => todos.remove(t) }, root)

Observable writes update DOM synchronously — assert immediately after setting:

Direct DOM reads are appropriate here because this is verification code, not the UI update path itself.

```js
const vm = { msg: ko.observable('Hello') }
ko.applyBindings(vm, document.getElementById('app'))
Expand Down
Loading
Loading