Skip to content

Commit 2188232

Browse files
authored
simplify patterns for charm references in cell (commontoolsinc#1799)
* 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 * use toSchema and ifElse to simplify code * set the sub charms name so its easier distinguish * undefined is not legal in JSONSchema, changed cellRef to default to null instead
1 parent 79bffce commit 2188232

File tree

1 file changed

+87
-97
lines changed

1 file changed

+87
-97
lines changed

packages/patterns/charm-ref-in-cell.tsx

Lines changed: 87 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -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
6453
const 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

Comments
 (0)