-
Notifications
You must be signed in to change notification settings - Fork 11
simplify patterns for charm references in cell #1799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6601cb4
e8d6592
2dc14ea
3a4d561
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,73 +3,55 @@ import { | |
| Cell, | ||
| cell, | ||
| createCell, | ||
| Default, | ||
| 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<boolean>; | ||
| } | ||
| const RecipeStateSchema = toSchema<RecipeState>(); | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. im not sure how to get isInitialized to get defaulted so that i dont have to create it with cell(), i tried passing a default here: but when i call the handler, it still requires the parameter, even if i made it optional.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you want your initial declaration to be: interface RecipeState {
charm: any;
cellRef: Cell<{ charm: any }>;
isInitialized: Cell<Default<boolean, false>>;
}Edit: actually, this may not be what you want, since you don't want to have to initialize it as a cell.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To merge options, I think you would have wanted: {
"properties": {
"isInitialized": {
"default": false
}
}
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Default<boolean, false> option would work. But it really does is replace undefined with false, so if your code says And yes, you have to call the lift with a cell, but it can be The underlying reason is an asymmetry we should anyway resolve: When a recipe gets static data, it just makes a new cell, so it's read/write, but for lift static inputs (and hence also defaults) are read-only. We should change that, and then you wouldn't need the cell creation in this case (<=> the cell is created implicitly by the input that wasn't passed in). |
||
| // 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: null }>; | ||
| }; | ||
|
|
||
| // the simple charm (to which we'll store a reference within a cell) | ||
| const SimpleRecipe = recipe("Simple Recipe", () => ({ | ||
| [NAME]: "Some Simple Recipe", | ||
| [UI]: <div>Some Simple Recipe</div>, | ||
| const SimpleRecipe = recipe<{ id: string }>("Simple Recipe", ({ id }) => ({ | ||
| [NAME]: derive(id, (idValue) => `SimpleRecipe: ${idValue}`), | ||
| [UI]: <div>Simple Recipe id {id}</div>, | ||
| })); | ||
|
|
||
| // 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 }, | ||
| }, | ||
| }, | ||
| RecipeStateSchema, | ||
| undefined, | ||
| ({ charm, cellRef, isInitialized }) => { | ||
| if (!isInitialized.get()) { | ||
|
|
@@ -78,7 +60,7 @@ const storeCharmAndNavigate = lift( | |
| "storeCharmAndNavigate storing charm:", | ||
| JSON.stringify(charm), | ||
| ); | ||
| cellRef.set(charm); | ||
| cellRef.set({ charm }); | ||
| isInitialized.set(true); | ||
| return navigateTo(charm); | ||
| } else { | ||
|
|
@@ -91,64 +73,72 @@ 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<unknown, { cellRef: Cell<any> }>( | ||
| // 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<unknown, { cellRef: Cell<{ charm: any }> }>( | ||
| (_, { cellRef }) => { | ||
| const isInitialized = cell(false); | ||
|
|
||
| // Create a random 5-digit ID | ||
| const randomId = Math.floor(10000 + Math.random() * 90000).toString(); | ||
ellyxir marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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 }); | ||
| }, | ||
| ); | ||
|
|
||
| // Handler to navigate to the stored charm (just console.log for now) | ||
| const goToStoredCharm = handler<unknown, { cellRef: Cell<any> }>( | ||
| const goToStoredCharm = handler<unknown, { cellRef: Cell<{ charm: any }> }>( | ||
| (_, { cellRef }) => { | ||
| console.log("goToStoredCharm clicked"); | ||
| return navigateTo(cellRef); | ||
| const cellValue = cellRef.get(); | ||
| if (!cellValue.charm) { | ||
| console.error("No charm found in cell!"); | ||
| return; | ||
| } | ||
| return navigateTo(cellValue.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(), | ||
| }); | ||
|
|
||
| return { | ||
| [NAME]: "Launcher", | ||
| [UI]: ( | ||
| <div> | ||
| export default recipe<RecipeInOutput, RecipeInOutput>( | ||
| "Launcher", | ||
| ({ cellRef }) => { | ||
| return { | ||
| [NAME]: "Launcher", | ||
| [UI]: ( | ||
| <div> | ||
| Stored charm ID: {derive(cellRef, (innerCell) => { | ||
| if (!innerCell) return "undefined"; | ||
| return innerCell[UI]; | ||
| })} | ||
| <div> | ||
| 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"; | ||
| })} | ||
| </div> | ||
| <ct-button | ||
| onClick={createSimpleRecipe({ cellRef })} | ||
| > | ||
| Create Sub Charm | ||
| </ct-button> | ||
|
|
||
| {ifElse( | ||
| cellRef.charm, | ||
| ( | ||
| <ct-button onClick={goToStoredCharm({ cellRef })}> | ||
| Go to Stored Charm | ||
| </ct-button> | ||
| ), | ||
| ( | ||
| <div>no subcharm</div> | ||
| ), | ||
| )} | ||
| </div> | ||
| <ct-button | ||
| onClick={createSimpleRecipe({ cellRef })} | ||
| > | ||
| Create Sub Charm | ||
| </ct-button> | ||
| {derive(cellRef, (innerCell) => { | ||
| if (!innerCell) return "no subcharm yet!"; | ||
| return ( | ||
| <ct-button onClick={goToStoredCharm({ cellRef: innerCell })}> | ||
| Go to Stored Charm | ||
| </ct-button> | ||
| ); | ||
| })} | ||
| </div> | ||
| ), | ||
| cellRef, | ||
| }; | ||
| }); | ||
| ), | ||
| cellRef, | ||
| }; | ||
| }, | ||
| ); | ||
Uh oh!
There was an error while loading. Please reload this page.