Skip to content

Commit 0c7bef4

Browse files
authored
Merge pull request #258 from knockout/tsx-html-tabs
Replace live-example placeholders with inline code, add TSX playground buttons
2 parents d043362 + 8e35cbf commit 0c7bef4

23 files changed

+918
-71
lines changed

tko.io/plugins/playground-button.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/**
22
* Expressive Code plugin: adds an "Open in Playground" button to code blocks.
33
*
4-
* Handles two patterns:
4+
* Handles three patterns:
55
* 1. Self-contained HTML blocks with inline <script> tags
66
* 2. Adjacent html + javascript code blocks (common in KO docs)
7+
* 3. TSX/HTML tab pairs followed by a javascript block
78
*
89
* For pattern 2, when a ```javascript block follows a ```html block,
910
* the plugin pairs them and adds the button to the HTML block.
11+
* For pattern 3 (tsx-tabs), the TSX block also gets a playground button.
1012
*
1113
* Replicates the exact same DOM structure and CSS as the built-in copy button:
1214
* <a class="playground-open"><div></div></a>
@@ -88,8 +90,9 @@ function addPlaygroundButton(renderData, html, js) {
8890
}
8991

9092
export function pluginPlaygroundButton() {
91-
// Track pending HTML block for pairing with a following JS block
93+
// Track pending blocks for pairing with a following JS block
9294
let pendingHtml = null
95+
let pendingTsx = null
9396

9497
return {
9598
name: 'playground-button',
@@ -173,28 +176,39 @@ export function pluginPlaygroundButton() {
173176

174177
hooks: {
175178
postprocessRenderedBlock: ({ codeBlock, renderData }) => {
176-
if (codeBlock.language === 'html') {
179+
if (codeBlock.language === 'tsx') {
180+
// TSX block (from tsx-tabs) — store for pairing with following JS block
181+
pendingTsx = { html: codeBlock.code.trim(), renderData }
182+
} else if (codeBlock.language === 'html') {
177183
const code = codeBlock.code
178184
const { html, js } = splitHtmlAndScript(code)
179185

180186
if (js) {
181187
// Self-contained HTML block with inline <script> — add button now
182188
pendingHtml = null
189+
pendingTsx = null
183190
addPlaygroundButton(renderData, html, js)
184191
} else {
185192
// HTML-only block — store for potential pairing with next JS block
186193
pendingHtml = { html: code.trim(), renderData }
187194
}
188-
} else if (codeBlock.language === 'javascript' && pendingHtml) {
189-
// JS block immediately after an HTML block — pair them
195+
} else if (codeBlock.language === 'javascript' && (pendingHtml || pendingTsx)) {
196+
// JS block after an HTML/TSX block — pair them
190197
const js = codeBlock.code.trim()
191198
if (js) {
192-
addPlaygroundButton(pendingHtml.renderData, pendingHtml.html, js)
199+
if (pendingHtml) {
200+
addPlaygroundButton(pendingHtml.renderData, pendingHtml.html, js)
201+
}
202+
if (pendingTsx) {
203+
addPlaygroundButton(pendingTsx.renderData, pendingTsx.html, js)
204+
}
193205
}
194206
pendingHtml = null
207+
pendingTsx = null
195208
} else {
196209
// Any other block type breaks the pairing
197210
pendingHtml = null
211+
pendingTsx = null
198212
}
199213
}
200214
}

tko.io/public/agent-guide.md

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,64 @@ foreach, if, ifnot, with, template:{name:'id'}, component:{name:'n',params:{}}
3030

3131
Context variables inside foreach: $data, $parent, $root, $index (observable), $element.
3232

33-
## JSX
33+
## TSX vs HTML Syntax
3434

35-
Use `ko-*` attributes instead of `data-bind`:
35+
TKO supports two binding syntaxes. The documentation shows both side-by-side in tabbed code blocks.
3636

37-
```jsx
38-
<span ko-text={obs} />
39-
<button ko-click={fn}>Label</button>
40-
<input ko-value={obs} />
41-
<ul ko-foreach={arr}><li ko-text="$data" /></ul>
42-
<div ko-if={cond}>...</div>
43-
<div ko-css={{ active: isActive }}>...</div>
37+
### HTML (data-bind) — runtime bindings
38+
39+
```html
40+
<span data-bind="text: message"></span>
41+
<button data-bind="click: handler">Label</button>
42+
<ul data-bind="foreach: items"><li data-bind="text: $data"></li></ul>
4443
```
4544

46-
Render pattern (required for ko-* bindings to activate):
45+
- Bindings are **strings** evaluated at runtime by `ko.applyBindings(viewModel, element)`
46+
- Binding-context variables (`$data`, `$parent`, `$index`, `$root`) resolve at runtime
47+
- No build step needed — works directly in the browser
48+
- Playground: put HTML in the HTML editor, JS in the TSX editor, run
4749

48-
```jsx
50+
### TSX (ko-*) — compile-time JSX expressions
51+
52+
```tsx
53+
<span ko-text={message} />
54+
<button ko-click={handler}>Label</button>
55+
<ul ko-foreach={items}><li ko-text="$data" /></ul>
56+
```
57+
58+
- `ko-*` attribute values in `{}` are **JavaScript expressions** evaluated at compile time by esbuild
59+
- Top-level variables (`message`, `handler`, `items`) must be defined in scope before the JSX
60+
- Binding-context variables inside `ko-foreach` children (like `$data`, `$parent`) use **string** syntax: `ko-text="$data"` (not `ko-text={$data}`)
61+
- Requires esbuild JSX transform + `tko.jsx.render()` to produce DOM nodes
62+
- `ko.applyBindings({}, container)` then activates the `ko-*` bindings on the rendered DOM
63+
64+
### Key difference: compile-time vs runtime
65+
66+
In HTML, `data-bind="foreach: people"` is a string — knockout evaluates `people` in the view model at runtime.
67+
68+
In TSX, `ko-foreach={people}` is a JSX expression — esbuild resolves `people` as a JavaScript variable at compile time. The variable must exist in scope:
69+
70+
```tsx
71+
// Variables must be defined before the JSX expression
72+
const people = ko.observableArray([...])
73+
74+
const view = (
75+
<ul ko-foreach={people}>
76+
<li ko-text="name" /> {/* string — resolved by knockout at runtime */}
77+
</ul>
78+
)
79+
80+
const { node } = tko.jsx.render(view)
81+
root.appendChild(node)
82+
ko.applyBindings({}, root)
83+
```
84+
85+
### Render pattern (required for ko-* bindings)
86+
87+
```tsx
4988
const { node, dispose } = tko.jsx.render(<Component />)
5089
container.appendChild(node)
51-
tko.applyBindings({}, container) // activates ko-* bindings
90+
tko.applyBindings({}, container) // activates ko-* bindings on the DOM
5291
```
5392

5493
## Browser TSX transform (esbuild-wasm)

tko.io/public/agent-testing.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,62 @@ Console output appears in `#console-messages` as child elements.
7070

7171
esbuild-wasm takes a few seconds to initialize on first load. The playground shows "esbuild ready" in `#esbuild-status` when it's ready. Code auto-runs after compilation.
7272

73+
## Option 3: Testing doc page examples
74+
75+
The TKO docs at tko.io show code examples as paired HTML + JavaScript blocks. Each pair has:
76+
- **TSX/HTML tabs** on the HTML block (showing both `ko-*` and `data-bind` syntax)
77+
- **"Open in Playground" button** on both tabs (opens the example in the playground)
78+
79+
### Testing an HTML example from the docs
80+
81+
Extract the HTML and JS from the code blocks and use Option 1 (static HTML file):
82+
83+
```html
84+
<!DOCTYPE html>
85+
<html><body>
86+
<!-- paste the HTML code block here -->
87+
<script src="https://tko.io/lib/tko.js"></script>
88+
<script>
89+
window.ko = window.tko
90+
// paste the JS code block here
91+
</script>
92+
</body></html>
93+
```
94+
95+
### Testing a TSX example from the docs
96+
97+
TSX examples use `ko-*` attributes which require JSX compilation. Use Option 2 (playground).
98+
99+
Important: `ko-*` attribute values in `{braces}` are **compile-time JavaScript expressions**, not runtime binding strings. Variables referenced in `ko-*={expr}` must be defined in the TSX scope before the JSX expression. Binding-context variables inside `ko-foreach` children should use **string syntax**: `ko-text="name"` not `ko-text={name}`.
100+
101+
```tsx
102+
// 1. Define variables that ko-* attributes reference
103+
const people = ko.observableArray([...])
104+
105+
// 2. JSX template using ko-* attributes
106+
const view = (
107+
<ul ko-foreach={people}>
108+
<li ko-text="$data" />
109+
</ul>
110+
)
111+
112+
// 3. Render and activate bindings
113+
const root = document.getElementById('root')
114+
const { node } = tko.jsx.render(view)
115+
root.appendChild(node)
116+
ko.applyBindings({}, root)
117+
```
118+
119+
### Verifying playground links from doc pages
120+
121+
Each "Open in Playground" button encodes `{ html, js }` in the URL hash. To verify:
122+
123+
1. Navigate to the doc page (e.g., `http://localhost:4321/bindings/foreach-binding/`)
124+
2. Click "Open in Playground" on the HTML tab
125+
3. The playground should show the HTML in the HTML editor, JS in the TSX editor
126+
4. Wait for "esbuild ready", then the preview should render the example
127+
5. For TSX tab links: the playground currently receives TSX-style HTML — this requires manual restructuring to run (defining variables, wrapping in JSX, using `tko.jsx.render()`)
128+
73129
## Which option to use
74130

75131
| Scenario | Option |
@@ -78,3 +134,4 @@ esbuild-wasm takes a few seconds to initialize on first load. The playground sho
78134
| JSX/TSX code | **Option 2** — playground has esbuild-wasm |
79135
| Quick observable/computed logic test | **Option 1** — no DOM needed, just script |
80136
| Sharing a runnable example with a human | **Option 2** — give them the playground URL |
137+
| Verifying doc page examples work | **Option 1** for HTML tab, **Option 2** for TSX tab |

tko.io/public/js/examples.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,6 @@ import { oneDark } from 'https://esm.sh/@codemirror/theme-one-dark@6.1.3';
55

66
let esbuildInitialized = false;
77

8-
function parseLegacyExampleId(rawParams) {
9-
const match = rawParams?.match(/id:\s*["']?([^"'}\s]+)["']?/i);
10-
return match?.[1] || 'legacy-example';
11-
}
12-
13-
function createLegacyExamplePlaceholder(exampleNode) {
14-
const exampleId = parseLegacyExampleId(exampleNode.getAttribute('params') || '');
15-
const container = document.createElement('div');
16-
container.className = 'example-placeholder';
17-
container.dataset.exampleId = exampleId;
18-
container.innerHTML = `
19-
<div class="example-placeholder__label">Live example in progress</div>
20-
<p>This page still references the legacy example <code>${exampleId}</code>.</p>
21-
<p>The interactive example system is being rebuilt for the Starlight site. Use the code snippets on this page as the current reference for now.</p>
22-
`;
23-
exampleNode.replaceWith(container);
24-
}
25-
268
async function initEsbuild() {
279
if (!esbuildInitialized) {
2810
await esbuild.initialize({
@@ -158,8 +140,6 @@ if (document.readyState === 'loading') {
158140
}
159141

160142
function initExamples() {
161-
document.querySelectorAll('live-example').forEach(createLegacyExamplePlaceholder);
162-
163143
// Find all code blocks with language 'jsx'
164144
const jsxBlocks = document.querySelectorAll('pre code.language-jsx');
165145
jsxBlocks.forEach(createExampleContainer);

tko.io/public/llms.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
- Playground: /playground
2020
- GitHub: https://github.com/knockout/tko
2121

22+
## Two Binding Syntaxes
23+
24+
HTML: `data-bind="text: msg"` — runtime strings, works with `ko.applyBindings(vm, el)`
25+
TSX: `ko-text={msg}` — compile-time JSX expressions, needs esbuild + `tko.jsx.render()`
26+
27+
Inside `ko-foreach` children, binding-context vars use strings: `ko-text="$data"` (not `{$data}`)
28+
29+
See /agent-guide.md for details.
30+
2231
## Docs
2332

2433
/observables/ · /computed/ · /bindings/ · /components/ · /binding-context/ · /advanced/

tko.io/src/content/docs/bindings/attr-binding.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,20 @@ The `attr` binding provides a generic way to set the value of any attribute for
99

1010
### Example
1111

12-
<live-example params='id: "attr"'></live-example>
12+
```html
13+
<a data-bind="attr: { href: url, title: details }">
14+
Report
15+
</a>
16+
```
17+
18+
```javascript
19+
var viewModel = {
20+
url: ko.observable("year-end.html"),
21+
details: ko.observable("Report including final year-end statistics")
22+
};
23+
24+
ko.applyBindings(viewModel);
25+
```
1326

1427
This will set the element's `href` attribute to `year-end.html` and the element's `title` attribute to `Report including final year-end statistics`.
1528

tko.io/src/content/docs/bindings/click-binding.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,24 @@ title: Click Binding
88
The `click` binding adds an event handler so that your chosen JavaScript function will be invoked when the associated DOM element is clicked. This is most commonly used with elements like `button`, `input`, and `a`, but actually works with any visible DOM element.
99

1010
### Example
11-
<live-example params='id: "click"'></live-example>
11+
```html
12+
<div>
13+
You've clicked <span data-bind="text: numberOfClicks"></span> times
14+
<button data-bind="click: incrementClickCounter">Click me</button>
15+
</div>
16+
```
17+
18+
```javascript
19+
var viewModel = {
20+
numberOfClicks: ko.observable(0),
21+
incrementClickCounter: function() {
22+
var previousCount = this.numberOfClicks();
23+
this.numberOfClicks(previousCount + 1);
24+
}
25+
};
26+
27+
ko.applyBindings(viewModel);
28+
```
1229

1330

1431
Each time you click the button, this will invoke `incrementClickCounter()` on the view model, which in turn changes the view model state, which causes the UI to update.
@@ -30,7 +47,27 @@ Each time you click the button, this will invoke `incrementClickCounter()` on th
3047
When calling your handler, Knockout will supply the current model value as the first parameter. This is particularly useful if you're rendering
3148
some UI for each item in a collection, and you need to know which item's UI was clicked. For example,
3249

33-
<live-example params='id: "click-places"'></live-example>
50+
```html
51+
<ul data-bind="foreach: places">
52+
<li>
53+
<span data-bind="text: $data"></span>
54+
<button data-bind="click: $parent.removePlace">Remove</button>
55+
</li>
56+
</ul>
57+
```
58+
59+
```javascript
60+
function MyViewModel() {
61+
var self = this;
62+
self.places = ko.observableArray(['London', 'Paris', 'Tokyo']);
63+
64+
self.removePlace = function(place) {
65+
self.places.remove(place);
66+
};
67+
}
68+
69+
ko.applyBindings(new MyViewModel());
70+
```
3471

3572
Two points to note about this example:
3673

@@ -45,7 +82,25 @@ Two points to note about this example:
4582

4683
In some scenarios, you may need to access the DOM event object associated with your click event. Knockout will pass the event as the second parameter to your function, as in this example:
4784

48-
<live-example params='id: "click-event"'></live-example>
85+
```html
86+
<button data-bind="click: myFunction">
87+
Click me
88+
</button>
89+
```
90+
91+
```javascript
92+
var viewModel = {
93+
myFunction: function(data, event) {
94+
if (event.shiftKey) {
95+
//do something different when user has shift key down
96+
} else {
97+
//do normal action
98+
}
99+
}
100+
};
101+
102+
ko.applyBindings(viewModel);
103+
```
49104

50105
If you need to pass more parameters, one way to do it is by wrapping your handler in a function literal that takes in a parameter, as in this example:
51106

tko.io/src/content/docs/bindings/enable-binding.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,25 @@ The `enable` binding causes the associated DOM element to be enabled only when t
1010

1111
### Example
1212

13-
<live-example params='id: "enable-binding"'></live-example>
13+
```html
14+
<p>
15+
<input type="checkbox" data-bind="checked: hasCellphone" />
16+
I have a cellphone
17+
</p>
18+
<p>
19+
Your cellphone number:
20+
<input type="text" data-bind="value: cellphoneNumber, enable: hasCellphone" />
21+
</p>
22+
```
23+
24+
```javascript
25+
var viewModel = {
26+
hasCellphone: ko.observable(false),
27+
cellphoneNumber: ""
28+
};
29+
30+
ko.applyBindings(viewModel);
31+
```
1432

1533
In this example, the "Your cellphone number" text box will initially be disabled. It will be enabled only when the user checks the box labelled "I have a cellphone".
1634

0 commit comments

Comments
 (0)