@powerhousedao/academy 5.1.0-dev.0 → 5.1.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/blog/BeyondCommunication-ABlueprintForDevelopment.md +2 -1
- package/blog/TheChallengeOfChange.md +1 -0
- package/docs/academy/01-GetStarted/00-ExploreDemoPackage.mdx +24 -27
- package/docs/academy/01-GetStarted/01-CreateNewPowerhouseProject.md +118 -9
- package/docs/academy/01-GetStarted/02-DefineToDoListDocumentModel.md +110 -28
- package/docs/academy/01-GetStarted/03-ImplementOperationReducers.md +191 -145
- package/docs/academy/01-GetStarted/04-WriteDocumentModelTests.md +378 -0
- package/docs/academy/01-GetStarted/05-BuildToDoListEditor.md +560 -0
- package/docs/academy/01-GetStarted/_04-BuildToDoListEditor +1 -1
- package/docs/academy/02-MasteryTrack/01-BuilderEnvironment/03-BuilderTools.md +2 -2
- package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/05-VetraStudio.md +48 -6
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/02-SpecifyTheStateSchema.md +75 -44
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/03-SpecifyDocumentOperations.md +28 -22
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/04-UseTheDocumentModelGenerator.md +28 -31
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/05-ImplementDocumentReducers.md +211 -206
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/06-ImplementDocumentModelTests.md +176 -62
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/07-ExampleToDoListRepository.md +21 -0
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/01-BuildingDocumentEditors.md +309 -319
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/00-DocumentToolbar.mdx +4 -0
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/01-OperationHistory.md +4 -0
- package/docs/academy/02-MasteryTrack/05-Launch/04-ConfigureEnvironment.md +1 -1
- package/docs/academy/03-ExampleUsecases/Chatroom/02-CreateNewPowerhouseProject.md +111 -35
- package/docs/academy/03-ExampleUsecases/Chatroom/03-DefineChatroomDocumentModel.md +255 -76
- package/docs/academy/03-ExampleUsecases/Chatroom/04-ImplementOperationReducers.md +281 -160
- package/docs/academy/03-ExampleUsecases/Chatroom/05-ImplementChatroomEditor.md +188 -35
- package/docs/academy/03-ExampleUsecases/Chatroom/06-LaunchALocalReactor.md +95 -7
- package/docs/academy/03-ExampleUsecases/Chatroom/_category_.json +1 -1
- package/docs/academy/03-ExampleUsecases/TodoList/00-GetTheStarterCode.md +24 -0
- package/docs/academy/03-ExampleUsecases/TodoList/01-GenerateTodoListDocumentModel.md +211 -0
- package/docs/academy/03-ExampleUsecases/TodoList/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +171 -0
- package/docs/academy/03-ExampleUsecases/TodoList/03-AddTestsForTodoListActions.md +462 -0
- package/docs/academy/03-ExampleUsecases/TodoList/04-GenerateTodoListDocumentEditor.md +45 -0
- package/docs/academy/03-ExampleUsecases/TodoList/05-ImplementTodoListDocumentEditorUIComponents.md +422 -0
- package/docs/academy/03-ExampleUsecases/TodoList/06-GenerateTodoDriveExplorer.md +61 -0
- package/docs/academy/03-ExampleUsecases/TodoList/07-AddSharedComponentForShowingTodoListStats.md +384 -0
- package/docs/academy/03-ExampleUsecases/TodoList/_category_.json +8 -0
- package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/VetraPackageLibrary.md +7 -0
- package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/_category_.json +9 -0
- package/docs/academy/04-APIReferences/00-PowerhouseCLI.md +6 -2
- package/docs/academy/04-APIReferences/01-ReactHooks.md +2 -2
- package/docs/academy/04-APIReferences/06-VetraRemoteDrive.md +160 -0
- package/docs/academy/04-APIReferences/renown-sdk/00-Overview.md +316 -0
- package/docs/academy/04-APIReferences/renown-sdk/01-Authentication.md +672 -0
- package/docs/academy/04-APIReferences/renown-sdk/02-APIReference.md +957 -0
- package/docs/academy/04-APIReferences/renown-sdk/_category_.json +8 -0
- package/docs/academy/05-Architecture/00-PowerhouseArchitecture.md +7 -39
- package/docs/academy/06-ComponentLibrary/00-DocumentEngineering.md +72 -24
- package/docs/academy/08-Glossary.md +7 -0
- package/docs/academy/10-TodoListTutorial/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +171 -0
- package/docs/academy/10-TodoListTutorial/03-AddTestsForTodoListActions.md +462 -0
- package/docs/academy/10-TodoListTutorial/05-ImplementTodoListDocumentEditorUIComponents.md +422 -0
- package/docs/academy/10-TodoListTutorial/07-AddSharedComponentForShowingTodoListStats.md +370 -0
- package/docusaurus.config.ts +28 -3
- package/package.json +1 -1
- package/sidebars.ts +49 -12
- package/src/css/custom.css +26 -18
- package/static/img/Vetra-logo-dark.svg +11 -0
- package/static/img/vetra-logo-light.svg +11 -0
- package/docs/academy/00-EthereumArgentinaHackathon.md +0 -207
- package/docs/academy/01-GetStarted/04-BuildToDoListEditor.md +0 -218
- package/docs/academy/01-GetStarted/06-ReactorMCP.md +0 -58
- package/docs/academy/05-Architecture/02-ReferencingMonorepoPackages +0 -65
- package/docs/academy/05-Architecture/04-MovingBeyondCRUD +0 -61
- /package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/images/Modules.png +0 -0
- /package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/images/VetraStudioDrive.png +0 -0
- /package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/{02-RevisionHistoryTimeline → 02-RevisionHistoryTimeline/360/237/232/247"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{01-WhatIsADocumentModel → 360/237/232/247 /01-WhatIsADocumentModel"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{02-DAOandDocumentsModelsQ+A → 360/237/232/247 /02-DAOandDocumentsModelsQ+A"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{02-domain-modeling → 360/237/232/247 /02-domain-modeling"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{03-BenefitsOfDocumentModels → 360/237/232/247 /03-BenefitsOfDocumentModels"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{04-UtilitiesAndTips → 360/237/232/247 /04-UtilitiesAndTips"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{05-best-practices → 360/237/232/247 /05-best-practices"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{_category_.json → 360/237/232/247 /_category_.json"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{three-data-layers.png → 360/237/232/247 /three-data-layers.png"} +0 -0
package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/05-ImplementDocumentReducers.md
CHANGED
|
@@ -10,9 +10,9 @@ Reducers are the core logic units of your document model. They are the functions
|
|
|
10
10
|
|
|
11
11
|
Before diving into the specifics of writing reducers, let's recall the preceding steps:
|
|
12
12
|
|
|
13
|
-
1. **State Schema Definition**: You designed the GraphQL `type` definitions for your document's data structure (e.g., `
|
|
13
|
+
1. **State Schema Definition**: You designed the GraphQL `type` definitions for your document's data structure (e.g., `TodoListState`, `TodoItem`).
|
|
14
14
|
2. **Document Operation Specification**: You defined the GraphQL `input` types that specify the parameters for each allowed modification to your document (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`). These were then associated with named operations (e.g., `ADD_TODO_ITEM`) in the Connect application.
|
|
15
|
-
3. **Code Generation**: You used `ph generate <YourModelName.
|
|
15
|
+
3. **Code Generation**: You used `ph generate <YourModelName.phd>` to create the necessary TypeScript types, action creators, and, crucially, the skeleton file for your reducers (typically `document-models/<your-model-name>/src/reducers/todos.ts`).
|
|
16
16
|
|
|
17
17
|
This generated reducer file is our starting point. It will contain function stubs or an object structure expecting your reducer implementations, all typed according to your schema.
|
|
18
18
|
|
|
@@ -27,7 +27,7 @@ Let's break down its components and principles:
|
|
|
27
27
|
- **`currentState`**: This is the complete, current state of your document model instance before the operation is applied. It's crucial to treat this as **immutable**.
|
|
28
28
|
- **`action`**: This is an object describing the operation to be performed. It typically has:
|
|
29
29
|
- A `type` property: A string identifying the operation (e.g., `'ADD_TODO_ITEM'`).
|
|
30
|
-
- An `input` property (or similar, like `payload`): An object containing the data necessary for the operation, matching the GraphQL `input` type you defined (e.g., `{
|
|
30
|
+
- An `input` property (or similar, like `payload`): An object containing the data necessary for the operation, matching the GraphQL `input` type you defined (e.g., `{ text: 'Buy groceries' }` for `AddTodoItemInput`).
|
|
31
31
|
- **`newState`**: The reducer must return a _new_ state object representing the state after the operation has been applied. If the operation does not result in a state change, the reducer should return the `currentState` object itself.
|
|
32
32
|
|
|
33
33
|
### Key principles guiding reducer implementation:
|
|
@@ -39,204 +39,211 @@ Let's break down its components and principles:
|
|
|
39
39
|
2. **Immutability**:
|
|
40
40
|
- **Never Mutate `currentState`**: You must never directly modify the `currentState` object or any of its nested properties.
|
|
41
41
|
- **Always Return a New Object for Changes**: If the state changes, you must create and return a brand new object. If the state does not change, you return the original `currentState` object.
|
|
42
|
-
- This is fundamental to Powerhouse's event sourcing architecture, enabling time travel, efficient change detection, and a clear audit trail.
|
|
42
|
+
- This is fundamental to Powerhouse's event sourcing architecture, enabling time travel, efficient change detection, and a clear audit trail.
|
|
43
|
+
|
|
44
|
+
:::tip Powerhouse uses Immer.js
|
|
45
|
+
Powerhouse uses **Immer.js** under the hood, which means you can write code that _looks like_ it's mutating the state directly (e.g., `state.items.push(...)`), but Immer ensures it results in an immutable update. This gives you the best of both worlds: readable code and immutable state.
|
|
46
|
+
:::
|
|
43
47
|
|
|
44
48
|
3. **Single Source of Truth**: The document state managed by reducers is the single source of truth for that document instance. All UI rendering and data queries are derived from this state.
|
|
45
49
|
|
|
46
50
|
4. **Delegation to specific operation handlers**:
|
|
47
|
-
While you can write one large reducer that uses a `switch` statement or `if/else if` blocks based on `action.type`, Powerhouse's generated code typically encourages a more modular approach. You'll often implement a separate function for each operation, which are then combined into a main reducer object or map. The `ph generate` command usually sets up this structure for you. For example, in your `document-models/
|
|
51
|
+
While you can write one large reducer that uses a `switch` statement or `if/else if` blocks based on `action.type`, Powerhouse's generated code typically encourages a more modular approach. You'll often implement a separate function for each operation, which are then combined into a main reducer object or map. The `ph generate` command usually sets up this structure for you. For example, in your `document-models/todo-list/src/reducers/todos.ts`, you'll find an object structure like this:
|
|
48
52
|
|
|
49
53
|
```typescript
|
|
50
|
-
import {
|
|
51
|
-
import { ToDoListState } from "../../gen/types.js"; // Generated type for state
|
|
54
|
+
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
52
55
|
|
|
53
|
-
export const
|
|
54
|
-
addTodoItemOperation(state
|
|
56
|
+
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
57
|
+
addTodoItemOperation(state, action) {
|
|
55
58
|
// Your logic for ADD_TODO_ITEM
|
|
56
|
-
// ...
|
|
57
|
-
return newState;
|
|
58
59
|
},
|
|
59
|
-
updateTodoItemOperation(state
|
|
60
|
+
updateTodoItemOperation(state, action) {
|
|
60
61
|
// Your logic for UPDATE_TODO_ITEM
|
|
61
|
-
// ...
|
|
62
|
-
return newState;
|
|
63
62
|
},
|
|
64
|
-
deleteTodoItemOperation(state
|
|
63
|
+
deleteTodoItemOperation(state, action) {
|
|
65
64
|
// Your logic for DELETE_TODO_ITEM
|
|
66
|
-
// ...
|
|
67
|
-
return newState;
|
|
68
65
|
},
|
|
69
|
-
// ... other operations
|
|
70
66
|
};
|
|
71
67
|
```
|
|
72
68
|
|
|
73
|
-
The `
|
|
74
|
-
|
|
75
|
-
The `dispatch` parameter is an advanced feature allowing a reducer to trigger subsequent operations. While powerful for complex workflows, it's often not needed for basic operations and can be ignored if unused.
|
|
69
|
+
The `TodoListTodosOperations` type is generated by Powerhouse and ensures your reducer object correctly implements all defined operations. The `state` and `action` parameters within these methods will also be strongly typed based on your schema.
|
|
76
70
|
|
|
77
71
|
## Implementing reducer logic: A practical guide
|
|
78
72
|
|
|
79
|
-
Let's use our familiar `
|
|
73
|
+
Let's use our familiar `TodoList` example to illustrate common patterns.
|
|
80
74
|
|
|
81
|
-
|
|
75
|
+
### Basic implementation (matching Get Started)
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
interface ToDoItem {
|
|
85
|
-
id: string;
|
|
86
|
-
text: string;
|
|
87
|
-
checked: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface ToDoListStats {
|
|
91
|
-
total: number;
|
|
92
|
-
checked: number;
|
|
93
|
-
unchecked: number;
|
|
94
|
-
}
|
|
77
|
+
The basic implementation matches what you built in the Get Started tutorial:
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
```typescript
|
|
80
|
+
import { generateId } from "document-model/core";
|
|
81
|
+
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
82
|
+
|
|
83
|
+
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
84
|
+
addTodoItemOperation(state, action) {
|
|
85
|
+
// Generate a unique ID for the new todo item
|
|
86
|
+
const id = generateId();
|
|
87
|
+
|
|
88
|
+
// Add the new item to the state (Immer handles immutability)
|
|
89
|
+
state.items.push({ ...action.input, id, checked: false });
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
updateTodoItemOperation(state, action) {
|
|
93
|
+
// Find the item to update by its ID
|
|
94
|
+
const item = state.items.find((item) => item.id === action.input.id);
|
|
95
|
+
|
|
96
|
+
// Return early if item not found
|
|
97
|
+
if (!item) return;
|
|
98
|
+
|
|
99
|
+
// Update only the fields that are provided (partial update)
|
|
100
|
+
item.text = action.input.text ?? item.text;
|
|
101
|
+
item.checked = action.input.checked ?? item.checked;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
deleteTodoItemOperation(state, action) {
|
|
105
|
+
// Filter out the item with the matching ID
|
|
106
|
+
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
100
109
|
```
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
:::info Key Pattern: ID Generation
|
|
112
|
+
Notice that `addTodoItemOperation` uses `generateId()` from `document-model/core` to create a unique ID. This is the recommended pattern — the ID is generated in the reducer, not passed from the UI. This ensures consistent, unique IDs across all operations.
|
|
113
|
+
:::
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
- `actions.updateTodoItem({ id: 'item-id', text: 'Updated Task Text', checked: true })`
|
|
106
|
-
- `actions.deleteTodoItem({ id: 'item-id' })`
|
|
115
|
+
### Advanced implementation (with statistics tracking)
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
:::info Advanced Feature
|
|
118
|
+
This section extends the basic reducers with statistics tracking, matching the advanced schema from the previous section. This demonstrates how to update computed/derived state alongside your primary data.
|
|
119
|
+
:::
|
|
109
120
|
|
|
110
|
-
|
|
121
|
+
For the advanced version with `stats`, we need to update the statistics whenever items are added, updated, or deleted:
|
|
111
122
|
|
|
112
123
|
```typescript
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
```
|
|
124
|
+
import { generateId } from "document-model/core";
|
|
125
|
+
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
126
|
+
|
|
127
|
+
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
128
|
+
addTodoItemOperation(state, action) {
|
|
129
|
+
// Generate a unique ID for the new todo item
|
|
130
|
+
const id = generateId();
|
|
131
|
+
|
|
132
|
+
// Update statistics
|
|
133
|
+
state.stats.total += 1;
|
|
134
|
+
state.stats.unchecked += 1;
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
// Add the new item to the state
|
|
137
|
+
state.items.push({
|
|
138
|
+
id,
|
|
139
|
+
text: action.input.text,
|
|
140
|
+
checked: false, // New items always start as unchecked
|
|
141
|
+
});
|
|
142
|
+
},
|
|
129
143
|
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
updateTodoItemOperation(state, action) {
|
|
145
|
+
// Find the specific item we want to update
|
|
146
|
+
const item = state.items.find((item) => item.id === action.input.id);
|
|
132
147
|
|
|
133
|
-
|
|
148
|
+
if (!item) {
|
|
149
|
+
throw new Error(`Item with id ${action.input.id} not found`);
|
|
150
|
+
}
|
|
134
151
|
|
|
135
|
-
|
|
152
|
+
// Update text if provided
|
|
153
|
+
if (action.input.text !== undefined) {
|
|
154
|
+
item.text = action.input.text;
|
|
155
|
+
}
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (item.id === id) {
|
|
146
|
-
// This is the item to update. Return a *new* item object.
|
|
147
|
-
return {
|
|
148
|
-
...item, // Copy existing properties of the item
|
|
149
|
-
// Update only fields that are provided in the action input
|
|
150
|
-
...(text !== undefined && { text: text }),
|
|
151
|
-
...(checked !== undefined && { checked: checked }),
|
|
152
|
-
};
|
|
157
|
+
// Handle checked status changes and update stats
|
|
158
|
+
if (action.input.checked !== undefined && action.input.checked !== item.checked) {
|
|
159
|
+
if (action.input.checked) {
|
|
160
|
+
state.stats.unchecked -= 1;
|
|
161
|
+
state.stats.checked += 1;
|
|
162
|
+
} else {
|
|
163
|
+
state.stats.unchecked += 1;
|
|
164
|
+
state.stats.checked -= 1;
|
|
153
165
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
item.checked = action.input.checked;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
deleteTodoItemOperation(state, action) {
|
|
171
|
+
// Find the item to determine its checked status for stats
|
|
172
|
+
const item = state.items.find((item) => item.id === action.input.id);
|
|
173
|
+
|
|
174
|
+
if (item) {
|
|
175
|
+
// Update statistics
|
|
176
|
+
state.stats.total -= 1;
|
|
177
|
+
if (item.checked) {
|
|
178
|
+
state.stats.checked -= 1;
|
|
179
|
+
} else {
|
|
180
|
+
state.stats.unchecked -= 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
160
183
|
|
|
161
|
-
|
|
184
|
+
// Remove the item from the list
|
|
185
|
+
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
```
|
|
162
189
|
|
|
163
|
-
|
|
164
|
-
- For the item that matches `action.input.id`, we create a new item object using the spread operator (`...item`) and then overwrite the properties (`text`, `checked`) that are present in `action.input`.
|
|
165
|
-
- The conditional spread (`...(condition && { property: value })`) is a concise way to only include a property in the new object if its corresponding input value is provided. This elegantly handles partial updates.
|
|
166
|
-
- If an item doesn't match the ID, it's returned as is.
|
|
190
|
+
### Common patterns explained
|
|
167
191
|
|
|
168
|
-
|
|
192
|
+
#### 1. Adding an item
|
|
169
193
|
|
|
170
194
|
```typescript
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
// Option 1: Throw an error (Powerhouse runtime might catch this)
|
|
175
|
-
throw new Error(`Item with id ${action.input.id} not found.`);
|
|
176
|
-
// Option 2: Return current state (no change)
|
|
177
|
-
// return state;
|
|
195
|
+
addTodoItemOperation(state, action) {
|
|
196
|
+
const id = generateId(); // Generate unique ID
|
|
197
|
+
state.items.push({ ...action.input, id, checked: false });
|
|
178
198
|
}
|
|
179
|
-
// ... proceed with map
|
|
180
199
|
```
|
|
181
200
|
|
|
182
|
-
|
|
201
|
+
- We use `generateId()` to create a unique identifier
|
|
202
|
+
- We spread `action.input` to get the text, add the generated ID and default `checked: false`
|
|
203
|
+
- With Immer, this "mutation" is actually immutable
|
|
183
204
|
|
|
184
|
-
|
|
205
|
+
#### 2. Updating an item
|
|
185
206
|
|
|
186
207
|
```typescript
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
items: state.items.filter(item => item.id !== id), // Create a new array excluding the item to delete
|
|
194
|
-
};
|
|
208
|
+
updateTodoItemOperation(state, action) {
|
|
209
|
+
const item = state.items.find((item) => item.id === action.input.id);
|
|
210
|
+
if (!item) return;
|
|
211
|
+
|
|
212
|
+
item.text = action.input.text ?? item.text;
|
|
213
|
+
item.checked = action.input.checked ?? item.checked;
|
|
195
214
|
}
|
|
196
215
|
```
|
|
197
216
|
|
|
198
|
-
|
|
217
|
+
- We find the item by ID
|
|
218
|
+
- We use nullish coalescing (`??`) to only update fields that were provided
|
|
219
|
+
- This allows partial updates (e.g., just changing `checked` without touching `text`)
|
|
220
|
+
|
|
221
|
+
#### 3. Deleting an item
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
deleteTodoItemOperation(state, action) {
|
|
225
|
+
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
199
228
|
|
|
200
|
-
- We use
|
|
229
|
+
- We use `filter` to create a new array without the deleted item
|
|
230
|
+
- Immer handles making this immutable
|
|
201
231
|
|
|
202
232
|
## Leveraging generated types
|
|
203
233
|
|
|
204
|
-
As highlighted in [Using the Document Model Generator](04-UseTheDocumentModelGenerator.md), `ph generate` produces TypeScript types for your state (e.g., `
|
|
234
|
+
As highlighted in [Using the Document Model Generator](04-UseTheDocumentModelGenerator.md), `ph generate` produces TypeScript types for your state (e.g., `TodoListState`, `TodoItem`) and the inputs for your operations (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`).
|
|
205
235
|
|
|
206
236
|
**Always use these generated types in your reducer implementations!**
|
|
207
237
|
|
|
208
238
|
```typescript
|
|
209
|
-
import {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// from ToDoListToDoListOperations. For complex actions, defining specific action types can be beneficial.
|
|
218
|
-
// For example:
|
|
219
|
-
// interface AddTodoItemAction {
|
|
220
|
-
// type: 'ADD_TODO_ITEM'; // Or the specific string constant used by the action creator
|
|
221
|
-
// input: AddTodoItemInput;
|
|
222
|
-
// }
|
|
223
|
-
|
|
224
|
-
export const reducer: ToDoListToDoListOperations = {
|
|
225
|
-
addTodoItemOperation(
|
|
226
|
-
state: ToDoListState,
|
|
227
|
-
action: { input: AddTodoItemInput /* plus type property */ },
|
|
228
|
-
dispatch,
|
|
229
|
-
) {
|
|
230
|
-
// Now 'action.input.text' and 'action.input.id' are type-checked
|
|
231
|
-
const newItem = {
|
|
232
|
-
id: action.input.id,
|
|
233
|
-
text: action.input.text,
|
|
234
|
-
checked: false,
|
|
235
|
-
};
|
|
236
|
-
return {
|
|
237
|
-
...state,
|
|
238
|
-
items: [...state.items, newItem],
|
|
239
|
-
};
|
|
239
|
+
import { generateId } from "document-model/core";
|
|
240
|
+
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
241
|
+
|
|
242
|
+
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
243
|
+
addTodoItemOperation(state, action) {
|
|
244
|
+
// TypeScript knows action.input has { text: string }
|
|
245
|
+
const id = generateId();
|
|
246
|
+
state.items.push({ id, text: action.input.text, checked: false });
|
|
240
247
|
},
|
|
241
248
|
// ... other reducers
|
|
242
249
|
};
|
|
@@ -248,101 +255,99 @@ Using these types provides:
|
|
|
248
255
|
- **Autocompletion and IntelliSense**: Improved developer experience in your IDE.
|
|
249
256
|
- **Clearer code**: Types serve as documentation for the expected data structures.
|
|
250
257
|
|
|
251
|
-
## Practical implementation: Writing the `
|
|
258
|
+
## Practical implementation: Writing the `TodoList` reducers
|
|
252
259
|
|
|
253
|
-
Now that you understand the principles, let's put them into practice by implementing the reducers for our `
|
|
260
|
+
Now that you understand the principles, let's put them into practice by implementing the reducers for our `TodoList` document model.
|
|
254
261
|
|
|
255
262
|
<details>
|
|
256
|
-
<summary>Tutorial: Implementing the
|
|
263
|
+
<summary>Tutorial: Implementing the TodoList reducers</summary>
|
|
257
264
|
|
|
258
|
-
This tutorial assumes you have followed the steps in the previous chapters, especially using `ph generate
|
|
265
|
+
This tutorial assumes you have followed the steps in the previous chapters, especially using `ph generate TodoList.phd` to scaffold your document model's code.
|
|
259
266
|
|
|
260
267
|
### Implement the operation reducers
|
|
261
268
|
|
|
262
|
-
Navigate to `document-models/
|
|
269
|
+
Navigate to `document-models/todo-list/src/reducers/todos.ts`. The generator will have created a skeleton file. Replace its contents with the following logic.
|
|
270
|
+
|
|
271
|
+
**Basic version (without stats):**
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { generateId } from "document-model/core";
|
|
275
|
+
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
276
|
+
|
|
277
|
+
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
278
|
+
addTodoItemOperation(state, action) {
|
|
279
|
+
const id = generateId();
|
|
280
|
+
state.items.push({ ...action.input, id, checked: false });
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
updateTodoItemOperation(state, action) {
|
|
284
|
+
const item = state.items.find((item) => item.id === action.input.id);
|
|
285
|
+
if (!item) return;
|
|
286
|
+
|
|
287
|
+
item.text = action.input.text ?? item.text;
|
|
288
|
+
item.checked = action.input.checked ?? item.checked;
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
deleteTodoItemOperation(state, action) {
|
|
292
|
+
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Advanced version (with stats):**
|
|
263
298
|
|
|
264
299
|
```typescript
|
|
265
|
-
import {
|
|
266
|
-
import {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// - state: The current document state. Powerhouse uses a library like Immer.js,
|
|
273
|
-
// so you can write code that looks like it's mutating the state directly.
|
|
274
|
-
// Behind the scenes, Powerhouse ensures this results in an immutable update.
|
|
275
|
-
// - action: Contains the operation's 'type' and 'input' data from the client.
|
|
276
|
-
// - dispatch: A function to trigger subsequent operations (advanced, not used here).
|
|
277
|
-
addTodoItemOperation(state, action, dispatch) {
|
|
278
|
-
// REMARKS: We update our statistics for total and unchecked items.
|
|
300
|
+
import { generateId } from "document-model/core";
|
|
301
|
+
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
302
|
+
|
|
303
|
+
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
304
|
+
addTodoItemOperation(state, action) {
|
|
305
|
+
const id = generateId();
|
|
306
|
+
|
|
279
307
|
state.stats.total += 1;
|
|
280
308
|
state.stats.unchecked += 1;
|
|
281
309
|
|
|
282
|
-
// REMARKS: We push the new to-do item into the items array.
|
|
283
|
-
// The data for the new item comes from the operation's input.
|
|
284
310
|
state.items.push({
|
|
285
|
-
id
|
|
311
|
+
id,
|
|
286
312
|
text: action.input.text,
|
|
287
|
-
checked: false,
|
|
313
|
+
checked: false,
|
|
288
314
|
});
|
|
289
315
|
},
|
|
290
316
|
|
|
291
|
-
|
|
292
|
-
// It handles partial updates for text and checked status.
|
|
293
|
-
updateTodoItemOperation(state, action, dispatch) {
|
|
294
|
-
// REMARKS: First, we find the specific item we want to update using its ID.
|
|
317
|
+
updateTodoItemOperation(state, action) {
|
|
295
318
|
const item = state.items.find((item) => item.id === action.input.id);
|
|
296
|
-
|
|
297
|
-
// REMARKS: It's good practice to handle cases where the item might not be found.
|
|
298
319
|
if (!item) {
|
|
299
320
|
throw new Error(`Item with id ${action.input.id} not found`);
|
|
300
321
|
}
|
|
301
322
|
|
|
302
|
-
|
|
303
|
-
// This allows for partial updates (e.g., just checking an item without changing its text).
|
|
304
|
-
if (action.input.text) {
|
|
323
|
+
if (action.input.text !== undefined) {
|
|
305
324
|
item.text = action.input.text;
|
|
306
325
|
}
|
|
307
326
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
item.checked = action.input.checked;
|
|
317
|
-
}
|
|
318
|
-
if (action.input.checked === false) {
|
|
319
|
-
// Note: This assumes the item was previously checked.
|
|
320
|
-
state.stats.unchecked += 1;
|
|
321
|
-
state.stats.checked -= 1;
|
|
327
|
+
if (action.input.checked !== undefined && action.input.checked !== item.checked) {
|
|
328
|
+
if (action.input.checked) {
|
|
329
|
+
state.stats.unchecked -= 1;
|
|
330
|
+
state.stats.checked += 1;
|
|
331
|
+
} else {
|
|
332
|
+
state.stats.unchecked += 1;
|
|
333
|
+
state.stats.checked -= 1;
|
|
334
|
+
}
|
|
322
335
|
item.checked = action.input.checked;
|
|
323
336
|
}
|
|
324
337
|
},
|
|
325
338
|
|
|
326
|
-
|
|
327
|
-
deleteTodoItemOperation(state, action, dispatch) {
|
|
328
|
-
// REMARKS: Before removing the item, we find it to determine its checked status.
|
|
329
|
-
// This is necessary to correctly decrement our statistics.
|
|
339
|
+
deleteTodoItemOperation(state, action) {
|
|
330
340
|
const item = state.items.find((item) => item.id === action.input.id);
|
|
331
341
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
if (item?.checked === false) {
|
|
341
|
-
state.stats.unchecked -= 1;
|
|
342
|
+
if (item) {
|
|
343
|
+
state.stats.total -= 1;
|
|
344
|
+
if (item.checked) {
|
|
345
|
+
state.stats.checked -= 1;
|
|
346
|
+
} else {
|
|
347
|
+
state.stats.unchecked -= 1;
|
|
348
|
+
}
|
|
342
349
|
}
|
|
343
350
|
|
|
344
|
-
// REMARKS: Finally, we create a new 'items' array that excludes the deleted item.
|
|
345
|
-
// Assigning to 'state.items' is handled by Powerhouse to produce a new immutable state.
|
|
346
351
|
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
347
352
|
},
|
|
348
353
|
};
|