@@ -3,73 +3,55 @@ import {
33 Cell ,
44 cell ,
55 createCell ,
6+ Default ,
67 derive ,
78 h ,
89 handler ,
10+ ifElse ,
911 lift ,
1012 NAME ,
1113 navigateTo ,
1214 recipe ,
15+ toSchema ,
1316 UI ,
1417} from "commontools" ;
1518
19+ // full recipe state
20+ interface RecipeState {
21+ charm : any ;
22+ cellRef : Cell < { charm : any } > ;
23+ isInitialized : Cell < boolean > ;
24+ }
25+ const RecipeStateSchema = toSchema < RecipeState > ( ) ;
26+
27+ // what we pass into the recipe as input
28+ // wraps the charm reference in an object { charm: any }
29+ // instead of storing the charm directly. This avoids a "pointer of pointers"
30+ // error that occurs when a Cell directly contains another Cell/charm reference.
31+ type RecipeInOutput = {
32+ cellRef : Default < { charm : any } , { charm : null } > ;
33+ } ;
34+
1635// the simple charm (to which we'll store a reference within a cell)
17- const SimpleRecipe = recipe ( "Simple Recipe" , ( ) => ( {
18- [ NAME ] : "Some Simple Recipe" ,
19- [ UI ] : < div > Some Simple Recipe</ div > ,
36+ const SimpleRecipe = recipe < { id : string } > ( "Simple Recipe" , ( { id } ) => ( {
37+ [ NAME ] : derive ( id , ( idValue ) => `SimpleRecipe: ${ idValue } ` ) ,
38+ [ UI ] : < div > Simple Recipe id { id } </ div > ,
2039} ) ) ;
2140
22- // We are going to dynamically create a charm via the `createCounter` function
23- // and store it (the reference to it) in a cell. We create the cell here.
24- // There are a few ways to do this:
25- // - Default values
26- // - cell()
27- // - createCell within a lift or derive (we'll use this for now)
28- // Use isInitialized and storedCellRef to ensure we only create the cell once
29- const createCellRef = lift (
30- {
31- type : "object" ,
32- properties : {
33- isInitialized : { type : "boolean" , default : false , asCell : true } ,
34- storedCellRef : { type : "object" , asCell : true } ,
35- } ,
36- } ,
37- undefined ,
38- ( { isInitialized, storedCellRef } ) => {
39- if ( ! isInitialized . get ( ) ) {
40- console . log ( "Creating cellRef" ) ;
41- const newCellRef = createCell ( undefined , "cellRef" ) ;
42- storedCellRef . set ( newCellRef ) ;
43- isInitialized . set ( true ) ;
44- return {
45- cellRef : newCellRef ,
46- } ;
47- } else {
48- console . log ( "cellRef already initialized" ) ;
49- }
50- // If already initialized, return the stored cellRef
51- return {
52- cellRef : storedCellRef ,
53- } ;
54- } ,
55- ) ;
56-
57- // this will be called whenever charm or cellRef changes
58- // pass isInitialized to make sure we dont call this each time
59- // we change cellRef, otherwise creates a loop
60- // also, we need to only navigateTo if not initialized so that
61- // the other lifts we created compete and try to
62- // navigateTo at the same time.
63- // note there is a separate isInitialized for each created charm
41+ // Lift that stores a charm reference in a cell and navigates to it.
42+ // Triggered when any input changes (charm, cellRef, or isInitialized).
43+ //
44+ // The isInitialized flag prevents infinite loops:
45+ // - Without it: lift runs → sets cellRef → cellRef changes → lift runs again → loop
46+ // - With it: lift runs once → sets isInitialized → subsequent runs skip the logic
47+ //
48+ // Each handler invocation creates its own isInitialized cell, ensuring
49+ // independent tracking for multiple charm creations.
50+ //
51+ // We use a lift() here instead of executing inside of a handler because
52+ // we want to know the passed in charm is initialized
6453const storeCharmAndNavigate = lift (
65- {
66- type : "object" ,
67- properties : {
68- charm : { type : "object" } ,
69- cellRef : { type : "object" , asCell : true } ,
70- isInitialized : { type : "boolean" , asCell : true } ,
71- } ,
72- } ,
54+ RecipeStateSchema ,
7355 undefined ,
7456 ( { charm, cellRef, isInitialized } ) => {
7557 if ( ! isInitialized . get ( ) ) {
@@ -78,7 +60,7 @@ const storeCharmAndNavigate = lift(
7860 "storeCharmAndNavigate storing charm:" ,
7961 JSON . stringify ( charm ) ,
8062 ) ;
81- cellRef . set ( charm ) ;
63+ cellRef . set ( { charm } ) ;
8264 isInitialized . set ( true ) ;
8365 return navigateTo ( charm ) ;
8466 } else {
@@ -91,64 +73,72 @@ const storeCharmAndNavigate = lift(
9173 } ,
9274) ;
9375
94- // create a simple subrecipe
95- // we will save a reference to it in a cell so make it as simple as
96- // possible.
97- // we then call navigateTo() which will redirect the
98- // browser to the newly created charm
99- const createSimpleRecipe = handler < unknown , { cellRef : Cell < any > } > (
76+ // Handler that creates a new charm instance and stores its reference.
77+ // 1. Creates a local isInitialized cell to track one-time execution
78+ // 2. Instantiates SimpleRecipe charm
79+ // 3. Uses storeCharmAndNavigate lift to save reference and navigate
80+ const createSimpleRecipe = handler < unknown , { cellRef : Cell < { charm : any } > } > (
10081 ( _ , { cellRef } ) => {
10182 const isInitialized = cell ( false ) ;
10283
84+ // Create a random 5-digit ID
85+ const randomId = Math . floor ( 10000 + Math . random ( ) * 90000 ) . toString ( ) ;
86+
10387 // create the charm
104- const charm = SimpleRecipe ( { } ) ;
88+ const charm = SimpleRecipe ( { id : randomId } ) ;
10589
10690 // store the charm ref in a cell (pass isInitialized to prevent recursive calls)
10791 return storeCharmAndNavigate ( { charm, cellRef, isInitialized } ) ;
10892 } ,
10993) ;
11094
11195// Handler to navigate to the stored charm (just console.log for now)
112- const goToStoredCharm = handler < unknown , { cellRef : Cell < any > } > (
96+ const goToStoredCharm = handler < unknown , { cellRef : Cell < { charm : any } > } > (
11397 ( _ , { cellRef } ) => {
11498 console . log ( "goToStoredCharm clicked" ) ;
115- return navigateTo ( cellRef ) ;
99+ const cellValue = cellRef . get ( ) ;
100+ if ( ! cellValue . charm ) {
101+ console . error ( "No charm found in cell!" ) ;
102+ return ;
103+ }
104+ return navigateTo ( cellValue . charm ) ;
116105 } ,
117106) ;
118107
119- // create the named cell inside the recipe body, so we do it just once
120- export default recipe ( "Launcher" , ( ) => {
121- // cell to store to the last charm we created
122- const { cellRef } = createCellRef ( {
123- isInitialized : cell ( false ) ,
124- storedCellRef : cell ( ) ,
125- } ) ;
126-
127- return {
128- [ NAME ] : "Launcher" ,
129- [ UI ] : (
130- < div >
108+ export default recipe < RecipeInOutput , RecipeInOutput > (
109+ "Launcher" ,
110+ ( { cellRef } ) => {
111+ return {
112+ [ NAME ] : "Launcher" ,
113+ [ UI ] : (
131114 < div >
132- Stored charm ID: { derive ( cellRef , ( innerCell ) => {
133- if ( ! innerCell ) return "undefined" ;
134- return innerCell [ UI ] ;
135- } ) }
115+ < div >
116+ Stored charm ID: { derive ( cellRef , ( innerCell ) => {
117+ if ( ! innerCell ) return "undefined" ;
118+ if ( ! innerCell . charm ) return "no charm stored yet" ;
119+ return innerCell . charm [ UI ] || "charm has no UI" ;
120+ } ) }
121+ </ div >
122+ < ct-button
123+ onClick = { createSimpleRecipe ( { cellRef } ) }
124+ >
125+ Create Sub Charm
126+ </ ct-button >
127+
128+ { ifElse (
129+ cellRef . charm ,
130+ (
131+ < ct-button onClick = { goToStoredCharm ( { cellRef } ) } >
132+ Go to Stored Charm
133+ </ ct-button >
134+ ) ,
135+ (
136+ < div > no subcharm</ div >
137+ ) ,
138+ ) }
136139 </ div >
137- < ct-button
138- onClick = { createSimpleRecipe ( { cellRef } ) }
139- >
140- Create Sub Charm
141- </ ct-button >
142- { derive ( cellRef , ( innerCell ) => {
143- if ( ! innerCell ) return "no subcharm yet!" ;
144- return (
145- < ct-button onClick = { goToStoredCharm ( { cellRef : innerCell } ) } >
146- Go to Stored Charm
147- </ ct-button >
148- ) ;
149- } ) }
150- </ div >
151- ) ,
152- cellRef,
153- } ;
154- } ) ;
140+ ) ,
141+ cellRef,
142+ } ;
143+ } ,
144+ ) ;
0 commit comments