Skip to content

Commit c44cdc8

Browse files
bloveclaude
andauthored
fix(cockpit-render): numeric content bindings + scrubber handle lag (#771)
* fix(cockpit-render): render non-string element content (numeric $state bindings) Demo Text/Heading components coerced content with `typeof c === 'string' ? c : ''`, silently dropping numbers — e.g. state-management's User Profile bound /user/age (30) to a Text and rendered blank at completion. Add a shared, tested toDisplayText() helper that stringifies primitives (number/boolean/bigint), keeps '' for null/objects, and use it across all render example demo components. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cockpit-render): remove scrubber handle transition so it tracks progress exactly The timeline handle had `transition: left 0.075s linear` while the fill had none, so the handle continuously lagged the fill during rAF playback and visibly animated on seek/spec-switch jumps. Drop the transition; the handle now snaps to position each frame, matching the fill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cockpit-render): computed-functions specs use $computed, not $fn The specs bound values with { $fn: 'uppercase', args: {...} }, but the render library's computed-function binding key is $computed ({ $computed: name, args }). $fn is unrecognized, so the raw binding object was passed through and rendered as [object Object]. Rename $fn → $computed across the three specs; the registered functions (provideRender) and args shape were already correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6ea776f commit c44cdc8

10 files changed

Lines changed: 76 additions & 52 deletions

File tree

cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { COMPUTED_FUNCTIONS_SPECS } from './specs';
1414
import { highlightJson } from '../../../../shared/json-highlight';
15+
import { toDisplayText } from '../../../../shared/to-display-text';
1516

1617
// --- Inline view components registered in the demo registry ---
1718

@@ -73,10 +74,7 @@ class DemoValueComponent {
7374
})
7475
class DemoHeadingComponent {
7576
readonly content = input<unknown>('');
76-
readonly displayContent = computed(() => {
77-
const c = this.content();
78-
return typeof c === 'string' ? c : '';
79-
});
77+
readonly displayContent = computed(() => toDisplayText(this.content()));
8078
readonly childKeys = input<string[]>([]);
8179
readonly spec = input<Spec | null>(null);
8280
readonly bindings = input<Record<string, string>>({});

cockpit/render/computed-functions/angular/src/app/specs.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ export const COMPUTED_FUNCTIONS_SPECS: DemoSpec[] = [
1616
type: 'Value',
1717
props: {
1818
label: 'Uppercase',
19-
value: { $fn: 'uppercase', args: { value: 'hello world' } },
19+
value: { $computed: 'uppercase', args: { value: 'hello world' } },
2020
},
2121
},
2222
reversed: {
2323
type: 'Value',
2424
props: {
2525
label: 'Reversed',
26-
value: { $fn: 'reverse', args: { value: 'streaming' } },
26+
value: { $computed: 'reverse', args: { value: 'streaming' } },
2727
},
2828
},
2929
},
@@ -43,14 +43,14 @@ export const COMPUTED_FUNCTIONS_SPECS: DemoSpec[] = [
4343
type: 'Value',
4444
props: {
4545
label: 'Formatted Date',
46-
value: { $fn: 'formatDate', args: { value: '2024-06-15T12:00:00Z' } },
46+
value: { $computed: 'formatDate', args: { value: '2024-06-15T12:00:00Z' } },
4747
},
4848
},
4949
product: {
5050
type: 'Value',
5151
props: {
5252
label: 'Multiply 7 x 6',
53-
value: { $fn: 'multiply', args: { a: 7, b: 6 } },
53+
value: { $computed: 'multiply', args: { a: 7, b: 6 } },
5454
},
5555
},
5656
},
@@ -70,21 +70,21 @@ export const COMPUTED_FUNCTIONS_SPECS: DemoSpec[] = [
7070
type: 'Value',
7171
props: {
7272
label: 'Multiply 12 x 5',
73-
value: { $fn: 'multiply', args: { a: 12, b: 5 } },
73+
value: { $computed: 'multiply', args: { a: 12, b: 5 } },
7474
},
7575
},
7676
transform: {
7777
type: 'Value',
7878
props: {
7979
label: 'Uppercase',
80-
value: { $fn: 'uppercase', args: { value: 'computed functions' } },
80+
value: { $computed: 'uppercase', args: { value: 'computed functions' } },
8181
},
8282
},
8383
format: {
8484
type: 'Value',
8585
props: {
8686
label: 'Date',
87-
value: { $fn: 'formatDate', args: { value: '2025-01-01T00:00:00Z' } },
87+
value: { $computed: 'formatDate', args: { value: '2025-01-01T00:00:00Z' } },
8888
},
8989
},
9090
},

cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { ELEMENT_RENDERING_SPECS } from './specs';
1414
import { highlightJson } from '../../../../shared/json-highlight';
15+
import { toDisplayText } from '../../../../shared/to-display-text';
1516

1617
// --- Inline view components registered in the demo registry ---
1718

@@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
3031
})
3132
class DemoTextComponent {
3233
readonly content = input<unknown>('');
33-
readonly displayContent = computed(() => {
34-
const c = this.content();
35-
return typeof c === 'string' ? c : '';
36-
});
34+
readonly displayContent = computed(() => toDisplayText(this.content()));
3735
readonly childKeys = input<string[]>([]);
3836
readonly spec = input<Spec | null>(null);
3937
readonly bindings = input<Record<string, string>>({});
@@ -59,10 +57,7 @@ class DemoTextComponent {
5957
})
6058
class DemoHeadingComponent {
6159
readonly content = input<unknown>('');
62-
readonly displayContent = computed(() => {
63-
const c = this.content();
64-
return typeof c === 'string' ? c : '';
65-
});
60+
readonly displayContent = computed(() => toDisplayText(this.content()));
6661
readonly childKeys = input<string[]>([]);
6762
readonly spec = input<Spec | null>(null);
6863
readonly bindings = input<Record<string, string>>({});

cockpit/render/registry/angular/src/app/registry.component.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { REGISTRY_SPECS } from './specs';
1414
import { highlightJson } from '../../../../shared/json-highlight';
15+
import { toDisplayText } from '../../../../shared/to-display-text';
1516

1617
// --- Inline view components registered in the demo registry ---
1718

@@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
3031
})
3132
class DemoTextComponent {
3233
readonly content = input<unknown>('');
33-
readonly displayContent = computed(() => {
34-
const c = this.content();
35-
return typeof c === 'string' ? c : '';
36-
});
34+
readonly displayContent = computed(() => toDisplayText(this.content()));
3735
readonly childKeys = input<string[]>([]);
3836
readonly spec = input<Spec | null>(null);
3937
readonly bindings = input<Record<string, string>>({});
@@ -59,10 +57,7 @@ class DemoTextComponent {
5957
})
6058
class DemoHeadingComponent {
6159
readonly content = input<unknown>('');
62-
readonly displayContent = computed(() => {
63-
const c = this.content();
64-
return typeof c === 'string' ? c : '';
65-
});
60+
readonly displayContent = computed(() => toDisplayText(this.content()));
6661
readonly childKeys = input<string[]>([]);
6762
readonly spec = input<Spec | null>(null);
6863
readonly bindings = input<Record<string, string>>({});

cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { REPEAT_LOOPS_SPECS } from './specs';
1414
import { highlightJson } from '../../../../shared/json-highlight';
15+
import { toDisplayText } from '../../../../shared/to-display-text';
1516

1617
// --- Inline view components registered in the demo registry ---
1718

@@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
3031
})
3132
class DemoTextComponent {
3233
readonly content = input<unknown>('');
33-
readonly displayContent = computed(() => {
34-
const c = this.content();
35-
return typeof c === 'string' ? c : '';
36-
});
34+
readonly displayContent = computed(() => toDisplayText(this.content()));
3735
readonly childKeys = input<string[]>([]);
3836
readonly spec = input<Spec | null>(null);
3937
readonly bindings = input<Record<string, string>>({});
@@ -59,10 +57,7 @@ class DemoTextComponent {
5957
})
6058
class DemoHeadingComponent {
6159
readonly content = input<unknown>('');
62-
readonly displayContent = computed(() => {
63-
const c = this.content();
64-
return typeof c === 'string' ? c : '';
65-
});
60+
readonly displayContent = computed(() => toDisplayText(this.content()));
6661
readonly childKeys = input<string[]>([]);
6762
readonly spec = input<Spec | null>(null);
6863
readonly bindings = input<Record<string, string>>({});

cockpit/render/shared/streaming-timeline.component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ import { StreamingSimulator } from './streaming-simulator';
6060
border: 2px solid var(--tl-green-bright);
6161
transform: translate(-50%, -50%);
6262
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
63-
transition: left 0.075s linear;
6463
}
6564
.tl__count {
6665
flex-shrink: 0;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
import { toDisplayText } from './to-display-text';
3+
4+
describe('toDisplayText', () => {
5+
it('returns strings unchanged', () => {
6+
expect(toDisplayText('Alice')).toBe('Alice');
7+
expect(toDisplayText('')).toBe('');
8+
});
9+
10+
it('stringifies numbers (the bug: numeric $state bindings were dropped)', () => {
11+
expect(toDisplayText(30)).toBe('30');
12+
expect(toDisplayText(42.5)).toBe('42.5');
13+
});
14+
15+
it('preserves zero (falsy but must display)', () => {
16+
expect(toDisplayText(0)).toBe('0');
17+
});
18+
19+
it('stringifies booleans', () => {
20+
expect(toDisplayText(true)).toBe('true');
21+
expect(toDisplayText(false)).toBe('false');
22+
});
23+
24+
it('returns empty string for null / undefined', () => {
25+
expect(toDisplayText(null)).toBe('');
26+
expect(toDisplayText(undefined)).toBe('');
27+
});
28+
29+
it('returns empty string for objects and arrays (not display text)', () => {
30+
expect(toDisplayText({ a: 1 })).toBe('');
31+
expect(toDisplayText([1, 2])).toBe('');
32+
});
33+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
/**
4+
* Coerce a resolved element prop (which may be a string, number, boolean, or —
5+
* for an unresolved/object binding — something else) into display text.
6+
*
7+
* Demo view components bind props like `content`/`value` that can resolve to
8+
* non-string primitives via `$state`/`$fn` bindings (e.g. a numeric
9+
* `/user/age`). Returning `''` only for strings silently dropped those values;
10+
* this renders any primitive and treats objects/null as "no text".
11+
*/
12+
export function toDisplayText(value: unknown): string {
13+
if (value == null) return '';
14+
const t = typeof value;
15+
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
16+
return String(value);
17+
}
18+
return '';
19+
}

cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { SPEC_RENDERING_SPECS } from './specs';
1414
import { highlightJson } from '../../../../shared/json-highlight';
15+
import { toDisplayText } from '../../../../shared/to-display-text';
1516

1617
// --- Inline view components registered in the demo registry ---
1718

@@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
3031
})
3132
class DemoTextComponent {
3233
readonly content = input<unknown>('');
33-
readonly displayContent = computed(() => {
34-
const c = this.content();
35-
return typeof c === 'string' ? c : '';
36-
});
34+
readonly displayContent = computed(() => toDisplayText(this.content()));
3735
readonly childKeys = input<string[]>([]);
3836
readonly spec = input<Spec | null>(null);
3937
readonly bindings = input<Record<string, string>>({});
@@ -59,10 +57,7 @@ class DemoTextComponent {
5957
})
6058
class DemoHeadingComponent {
6159
readonly content = input<unknown>('');
62-
readonly displayContent = computed(() => {
63-
const c = this.content();
64-
return typeof c === 'string' ? c : '';
65-
});
60+
readonly displayContent = computed(() => toDisplayText(this.content()));
6661
readonly childKeys = input<string[]>([]);
6762
readonly spec = input<Spec | null>(null);
6863
readonly bindings = input<Record<string, string>>({});

cockpit/render/state-management/angular/src/app/state-management.component.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { STATE_MANAGEMENT_SPECS } from './specs';
1414
import { highlightJson } from '../../../../shared/json-highlight';
15+
import { toDisplayText } from '../../../../shared/to-display-text';
1516

1617
// --- Inline view components registered in the demo registry ---
1718

@@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
3031
})
3132
class DemoTextComponent {
3233
readonly content = input<unknown>('');
33-
readonly displayContent = computed(() => {
34-
const c = this.content();
35-
return typeof c === 'string' ? c : '';
36-
});
34+
readonly displayContent = computed(() => toDisplayText(this.content()));
3735
readonly childKeys = input<string[]>([]);
3836
readonly spec = input<Spec | null>(null);
3937
readonly bindings = input<Record<string, string>>({});
@@ -59,10 +57,7 @@ class DemoTextComponent {
5957
})
6058
class DemoHeadingComponent {
6159
readonly content = input<unknown>('');
62-
readonly displayContent = computed(() => {
63-
const c = this.content();
64-
return typeof c === 'string' ? c : '';
65-
});
60+
readonly displayContent = computed(() => toDisplayText(this.content()));
6661
readonly childKeys = input<string[]>([]);
6762
readonly spec = input<Spec | null>(null);
6863
readonly bindings = input<Record<string, string>>({});

0 commit comments

Comments
 (0)