fix(components): type-coerce string properties + reseat on pininfo-change

Root cause of the 'digits=4 display seated with the 1-digit COM pinout'
bug: property values arrive as STRINGS (agent set_component_property,
the property dialog's text inputs) and were assigned to the web
component verbatim — wokwi's 7segment does switch(this.digits) with
numeric cases, so el.digits='4' silently fell back to the 1-digit
pinout (and 'false' stayed truthy for boolean props like colon).

- DynamicComponent now coerces string values to the TYPE of the
  metadata default for that key (number/boolean) before assigning.
- New pininfo-change listener: when a property swaps the element's pin
  set (digits, flip, pins edge), the elements announce it — re-derive
  the breadboard seating then, with the fresh pinout, instead of never.
This commit is contained in:
David Montero 2026-07-18 19:18:27 +02:00
parent aab5e1be08
commit 206cda78af
2 changed files with 46 additions and 2 deletions

View File

@ -293,20 +293,57 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
const isInteractive = logic?.attachEvents !== undefined;
/**
* Sync React properties to Web Component
* Sync React properties to Web Component.
*
* Values arriving as strings (agent set_component_property, the text
* inputs in the property dialog) are coerced to the type of the
* metadata DEFAULT for that key. Without this, `el.digits = '4'`
* (string) silently breaks wokwi elements that strict-match
* (`switch (this.digits) { case 4: ... }` -> falls back to the 1-digit
* pinout), and `'false'` stays truthy for boolean props like colon.
*/
useEffect(() => {
if (!elementRef.current) return;
Object.entries(properties).forEach(([key, value]) => {
try {
(elementRef.current as any)[key] = value;
let coerced: any = value;
if (typeof value === 'string') {
const def = metadata.defaultValues?.[key];
if (typeof def === 'number' && value.trim() !== '' && !Number.isNaN(Number(value))) {
coerced = Number(value);
} else if (typeof def === 'boolean') {
coerced = value === 'true' || value === '1';
}
}
(elementRef.current as any)[key] = coerced;
} catch (error) {
console.warn(`Failed to set property ${key} on ${metadata.tagName}:`, error);
}
});
}, [properties, metadata.tagName]);
/**
* Property changes that swap the element's pin SET (7segment digits,
* LED flip, display pins edge) re-render asynchronously and announce
* themselves with a 'pininfo-change' event. Re-derive the breadboard
* seating then reseating synchronously on the property write would
* read the STALE pinout and seat ghost pins.
*/
useEffect(() => {
const el = elementRef.current;
if (!el) return;
const onPinInfoChange = () => {
try {
useSimulatorStore.getState().reseatComponentOnBreadboard(id);
} catch {
// headless / tests
}
};
el.addEventListener('pininfo-change', onPinInfoChange);
return () => el.removeEventListener('pininfo-change', onPinInfoChange);
}, [id, metadata.tagName]);
/**
* Extract pinInfo from web component after it initializes
*/

View File

@ -2479,6 +2479,13 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
updates.properties && 'rotation' in updates.properties;
if (updates.x !== undefined || updates.y !== undefined || rotationChanged) {
get().updateWirePositions(id);
// Reseat ONLY on geometry changes (move/rotate) — the DOM pinInfo is
// still valid for those. Property changes that swap the pin SET
// (7segment digits, LED flip) re-render asynchronously; reseating
// now would read the STALE pinout and seat ghost pins (seen live:
// a digits=4 display seated with the 1-digit COM pinout). Those go
// through the element's 'pininfo-change' event instead
// (DynamicComponent listener).
get().reseatComponentOnBreadboard(id);
}