From 6601cb4a254fdfd5703bdd868f8d3765bb8f0f7f Mon Sep 17 00:00:00 2001 From: Ellyse Date: Thu, 18 Sep 2025 16:53:20 +0000 Subject: [PATCH 1/4] simplify a bit, removed uneccesasary lifts, use Default to create cell instead of calling cell() although i still create isInitialized from cell(), not sure how to turn that into a default via the json schema if thats possible. improved comments --- packages/patterns/charm-ref-in-cell.tsx | 154 +++++++++++------------- 1 file changed, 69 insertions(+), 85 deletions(-) diff --git a/packages/patterns/charm-ref-in-cell.tsx b/packages/patterns/charm-ref-in-cell.tsx index 133305a789..0140fd474e 100644 --- a/packages/patterns/charm-ref-in-cell.tsx +++ b/packages/patterns/charm-ref-in-cell.tsx @@ -3,6 +3,7 @@ import { Cell, cell, createCell, + Default, derive, h, handler, @@ -19,55 +20,34 @@ const SimpleRecipe = recipe("Simple Recipe", () => ({ [UI]:
Some Simple Recipe
, })); -// We are going to dynamically create a charm via the `createCounter` function -// and store it (the reference to it) in a cell. We create the cell here. -// There are a few ways to do this: -// - Default values -// - cell() -// - createCell within a lift or derive (we'll use this for now) -// Use isInitialized and storedCellRef to ensure we only create the cell once -const createCellRef = lift( - { - type: "object", - properties: { - isInitialized: { type: "boolean", default: false, asCell: true }, - storedCellRef: { type: "object", asCell: true }, - }, - }, - undefined, - ({ isInitialized, storedCellRef }) => { - if (!isInitialized.get()) { - console.log("Creating cellRef"); - const newCellRef = createCell(undefined, "cellRef"); - storedCellRef.set(newCellRef); - isInitialized.set(true); - return { - cellRef: newCellRef, - }; - } else { - console.log("cellRef already initialized"); - } - // If already initialized, return the stored cellRef - return { - cellRef: storedCellRef, - }; - }, -); - -// this will be called whenever charm or cellRef changes -// pass isInitialized to make sure we dont call this each time -// we change cellRef, otherwise creates a loop -// also, we need to only navigateTo if not initialized so that -// the other lifts we created compete and try to -// navigateTo at the same time. -// note there is a separate isInitialized for each created charm +// Lift that stores a charm reference in a cell and navigates to it. +// Triggered when any input changes (charm, cellRef, or isInitialized). +// +// The isInitialized flag prevents infinite loops: +// - Without it: lift runs → sets cellRef → cellRef changes → lift runs again → loop +// - With it: lift runs once → sets isInitialized → subsequent runs skip the logic +// +// Each handler invocation creates its own isInitialized cell, ensuring +// independent tracking for multiple charm creations. +// +// We use a lift() here instead of executing inside of a handler because +// we want to know the passed in charm is initialized const storeCharmAndNavigate = lift( { type: "object", properties: { charm: { type: "object" }, - cellRef: { type: "object", asCell: true }, - isInitialized: { type: "boolean", asCell: true }, + cellRef: { + type: "object", + properties: { + charm: { type: "object" }, + }, + asCell: true, + }, + isInitialized: { + type: "boolean", + asCell: true, + }, }, }, undefined, @@ -78,7 +58,7 @@ const storeCharmAndNavigate = lift( "storeCharmAndNavigate storing charm:", JSON.stringify(charm), ); - cellRef.set(charm); + cellRef.set({ charm }); isInitialized.set(true); return navigateTo(charm); } else { @@ -91,12 +71,11 @@ const storeCharmAndNavigate = lift( }, ); -// create a simple subrecipe -// we will save a reference to it in a cell so make it as simple as -// possible. -// we then call navigateTo() which will redirect the -// browser to the newly created charm -const createSimpleRecipe = handler }>( +// Handler that creates a new charm instance and stores its reference. +// 1. Creates a local isInitialized cell to track one-time execution +// 2. Instantiates SimpleRecipe charm +// 3. Uses storeCharmAndNavigate lift to save reference and navigate +const createSimpleRecipe = handler }>( (_, { cellRef }) => { const isInitialized = cell(false); @@ -109,46 +88,51 @@ const createSimpleRecipe = handler }>( ); // Handler to navigate to the stored charm (just console.log for now) -const goToStoredCharm = handler }>( +const goToStoredCharm = handler }>( (_, { cellRef }) => { console.log("goToStoredCharm clicked"); - return navigateTo(cellRef); + return navigateTo(cellRef.get().charm); }, ); -// create the named cell inside the recipe body, so we do it just once -export default recipe("Launcher", () => { - // cell to store to the last charm we created - const { cellRef } = createCellRef({ - isInitialized: cell(false), - storedCellRef: cell(), - }); +// Recipe state wraps the charm reference in an object { charm: any } +// instead of storing the charm directly. This avoids a "pointer of pointers" +// error that occurs when a Cell directly contains another Cell/charm reference. +type RecipeState = { + cellRef: Default<{ charm: any }, { charm: undefined }>; +}; - return { - [NAME]: "Launcher", - [UI]: ( -
+export default recipe( + "Launcher", + ({ cellRef }) => { + return { + [NAME]: "Launcher", + [UI]: (
- Stored charm ID: {derive(cellRef, (innerCell) => { - if (!innerCell) return "undefined"; - return innerCell[UI]; +
+ Stored charm ID: {derive(cellRef, (innerCell) => { + if (!innerCell) return "undefined"; + if (!innerCell.charm) return "no charm stored yet"; + return innerCell.charm[UI] || "charm has no UI"; + })} +
+ + Create Sub Charm + + {derive(cellRef, (innerCell) => { + if (!innerCell) return "no subcharm yet!"; + if (!innerCell.charm) return "no subcharm yet!"; + return ( + + Go to Stored Charm + + ); })}
- - Create Sub Charm - - {derive(cellRef, (innerCell) => { - if (!innerCell) return "no subcharm yet!"; - return ( - - Go to Stored Charm - - ); - })} -
- ), - cellRef, - }; -}); + ), + cellRef, + }; + }, +); From e8d659287d2aed9c75fd14849fcfb1101f9cf584 Mon Sep 17 00:00:00 2001 From: Ellyse Date: Thu, 18 Sep 2025 17:35:07 +0000 Subject: [PATCH 2/4] use toSchema and ifElse to simplify code --- packages/patterns/charm-ref-in-cell.tsx | 67 +++++++++++++------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/patterns/charm-ref-in-cell.tsx b/packages/patterns/charm-ref-in-cell.tsx index 0140fd474e..5ea8e06dc3 100644 --- a/packages/patterns/charm-ref-in-cell.tsx +++ b/packages/patterns/charm-ref-in-cell.tsx @@ -7,13 +7,31 @@ import { derive, h, handler, + ifElse, lift, NAME, navigateTo, recipe, + toSchema, UI, } from "commontools"; +// full recipe state +interface RecipeState { + charm: any; + cellRef: Cell<{ charm: any }>; + isInitialized: Cell; +} +const RecipeStateSchema = toSchema(); + +// what we pass into the recipe as input +// wraps the charm reference in an object { charm: any } +// instead of storing the charm directly. This avoids a "pointer of pointers" +// error that occurs when a Cell directly contains another Cell/charm reference. +type RecipeInOutput = { + cellRef: Default<{ charm: any }, { charm: undefined }>; +}; + // the simple charm (to which we'll store a reference within a cell) const SimpleRecipe = recipe("Simple Recipe", () => ({ [NAME]: "Some Simple Recipe", @@ -33,23 +51,7 @@ const SimpleRecipe = recipe("Simple Recipe", () => ({ // We use a lift() here instead of executing inside of a handler because // we want to know the passed in charm is initialized const storeCharmAndNavigate = lift( - { - type: "object", - properties: { - charm: { type: "object" }, - cellRef: { - type: "object", - properties: { - charm: { type: "object" }, - }, - asCell: true, - }, - isInitialized: { - type: "boolean", - asCell: true, - }, - }, - }, + RecipeStateSchema, undefined, ({ charm, cellRef, isInitialized }) => { if (!isInitialized.get()) { @@ -91,18 +93,16 @@ const createSimpleRecipe = handler }>( const goToStoredCharm = handler }>( (_, { cellRef }) => { console.log("goToStoredCharm clicked"); - return navigateTo(cellRef.get().charm); + const cellValue = cellRef.get(); + if (!cellValue.charm) { + console.error("No charm found in cell!"); + return; + } + return navigateTo(cellValue.charm); }, ); -// Recipe state wraps the charm reference in an object { charm: any } -// instead of storing the charm directly. This avoids a "pointer of pointers" -// error that occurs when a Cell directly contains another Cell/charm reference. -type RecipeState = { - cellRef: Default<{ charm: any }, { charm: undefined }>; -}; - -export default recipe( +export default recipe( "Launcher", ({ cellRef }) => { return { @@ -121,15 +121,18 @@ export default recipe( > Create Sub Charm - {derive(cellRef, (innerCell) => { - if (!innerCell) return "no subcharm yet!"; - if (!innerCell.charm) return "no subcharm yet!"; - return ( + + {ifElse( + cellRef.charm, + ( Go to Stored Charm - ); - })} + ), + ( +
no subcharm
+ ), + )} ), cellRef, From 2dc14ea2f0242289f2d07b1f9e6f7e5f82f206e5 Mon Sep 17 00:00:00 2001 From: Ellyse Date: Thu, 18 Sep 2025 17:46:23 +0000 Subject: [PATCH 3/4] set the sub charms name so its easier distinguish --- packages/patterns/charm-ref-in-cell.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/patterns/charm-ref-in-cell.tsx b/packages/patterns/charm-ref-in-cell.tsx index 5ea8e06dc3..cb78c729d9 100644 --- a/packages/patterns/charm-ref-in-cell.tsx +++ b/packages/patterns/charm-ref-in-cell.tsx @@ -33,9 +33,9 @@ type RecipeInOutput = { }; // the simple charm (to which we'll store a reference within a cell) -const SimpleRecipe = recipe("Simple Recipe", () => ({ - [NAME]: "Some Simple Recipe", - [UI]:
Some Simple Recipe
, +const SimpleRecipe = recipe<{ id: string }>("Simple Recipe", ({ id }) => ({ + [NAME]: derive(id, (idValue) => `SimpleRecipe: ${idValue}`), + [UI]:
Simple Recipe id {id}
, })); // Lift that stores a charm reference in a cell and navigates to it. @@ -81,8 +81,11 @@ const createSimpleRecipe = handler }>( (_, { cellRef }) => { const isInitialized = cell(false); + // Create a random 5-digit ID + const randomId = Math.floor(10000 + Math.random() * 90000).toString(); + // create the charm - const charm = SimpleRecipe({}); + const charm = SimpleRecipe({ id: randomId }); // store the charm ref in a cell (pass isInitialized to prevent recursive calls) return storeCharmAndNavigate({ charm, cellRef, isInitialized }); From 3a4d56195adf9b3095f3371c844eba254f79e322 Mon Sep 17 00:00:00 2001 From: Ellyse Date: Fri, 19 Sep 2025 17:23:52 +0000 Subject: [PATCH 4/4] undefined is not legal in JSONSchema, changed cellRef to default to null instead --- packages/patterns/charm-ref-in-cell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patterns/charm-ref-in-cell.tsx b/packages/patterns/charm-ref-in-cell.tsx index cb78c729d9..55ebe4d797 100644 --- a/packages/patterns/charm-ref-in-cell.tsx +++ b/packages/patterns/charm-ref-in-cell.tsx @@ -29,7 +29,7 @@ const RecipeStateSchema = toSchema(); // instead of storing the charm directly. This avoids a "pointer of pointers" // error that occurs when a Cell directly contains another Cell/charm reference. type RecipeInOutput = { - cellRef: Default<{ charm: any }, { charm: undefined }>; + cellRef: Default<{ charm: any }, { charm: null }>; }; // the simple charm (to which we'll store a reference within a cell)