Skip to content

Commit

Permalink
feat: add default value support for node types and update documentati…
Browse files Browse the repository at this point in the history
…on examples
  • Loading branch information
xaviergonz committed Feb 28, 2025
1 parent 6d7103e commit 62e4fb4
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 86 deletions.
11 changes: 8 additions & 3 deletions apps/site/docs/examples/todoList/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ export type Todo = TNode<
}
>

export const TTodo = nodeType<Todo>(todoType).withKey("id").settersFor("done", "text")
export const TTodo = nodeType<Todo>(todoType)
.withKey("id")
.defaults({
done: () => false,
})
.settersFor("done", "text")

const todoListType = "todoSample/TodoList"

Expand Down Expand Up @@ -45,8 +50,8 @@ export function createDefaultTodoList(): TodoList {
todos: [
// we could just use the objects here directly, but then we'd need to
// generate the ids and add the [nodeType] property ourselves
TTodo({ text: "make mobx-bonsai awesome!", done: false }),
TTodo({ text: "spread the word", done: false }),
TTodo({ text: "make mobx-bonsai awesome!" }),
TTodo({ text: "spread the word" }),
TTodo({ text: "buy some milk", done: true }),
],
})
Expand Down
48 changes: 44 additions & 4 deletions apps/site/docs/nodes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,15 @@ Use untyped node types when you don't need a type identifier. These are simpler
```ts
import { nodeType } from 'mobx-bonsai';

// Create an untyped node type
const TTodo = nodeType<Todo>();
// Create an untyped node type with defaults
const TTodo = nodeType<Todo>()
.defaults({
done: () => false
});

// Create a todo node
const myTodo = TTodo({ done: false, text: 'Buy milk' });
// Create a todo node, omitting the 'done' property since it has a default
const myTodo = TTodo({ text: 'Buy milk' });
// myTodo.done will be false
```

#### Typed Node Types
Expand Down Expand Up @@ -244,6 +248,42 @@ TTypedTodo.onInit(todo => {
});
```

#### `.defaults(defaultGenerators)`

Define default values for properties when they are undefined or omitted:

```ts
const TTodo = nodeType<Todo>()
.defaults({
done: () => false,
});

// You can omit properties with defaults when creating nodes
const todo = TTodo({ text: "Buy milk" });
console.log(todo.done); // false

// Explicit values override defaults
const importantTodo = TTodo({
text: "Call mom",
done: true,
});
console.log(importantTodo.true); // true

// Defaults are also applied when using snapshot
const todoSnapshot = TTodo.snapshot({ text: "Clean house" });
// todoSnapshot.done will be false
```

Default generators are only called when a property is missing. The return type of each generator must match the property type in your node's interface.

Note that defaults are applied when:
- Creating nodes with the nodeType factory (e.g., `TTodo({ text: "example" })`)
- Creating snapshots with the nodeType's snapshot function (e.g., `TTodo.snapshot({ text: "example" })`)

But not when:
- Using the generic `node()` function
- Updating properties on an existing node

### Using Node Types with Arrays

Node types work with arrays too:
Expand Down
41 changes: 34 additions & 7 deletions packages/mobx-bonsai/src/node/nodeTypeKey/BaseNodeType.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { MarkOptional } from "ts-essentials"
import { IComputedValueOptions } from "mobx"
import { PrependArgument } from "../../utils/PrependArgument"

/**
* Base node type definition with core functionality
*
* @template TNode - Node structure that adheres to this type
* @template TKey - Key field in the node structure (if any)
* @template TOptional - Optional keys in the node structure
* @template TOther - Additional properties and methods
*/
export type BaseNodeType<TNode extends object, TKey extends keyof TNode | never, TOther> = {
export type BaseNodeType<TNode extends object, TOptional extends keyof TNode, TOther> = {
/**
* Node constructor.
* Requires all keys from TNode except those in TOptional (which may be omitted).
*/
(data: MarkOptional<TNode, TOptional>): TNode

/**
* Returns a snapshot based on the provided data.
*/
snapshot(data: MarkOptional<TNode, TOptional>): TNode

/**
* Adds volatile state properties to nodes of this type
*
Expand All @@ -20,7 +32,7 @@ export type BaseNodeType<TNode extends object, TKey extends keyof TNode | never,
*/
volatile<TVolatiles extends Record<string, () => any>>(
volatile: TVolatiles
): BaseNodeType<TNode, TKey, TOther & VolatileAccessors<TVolatiles, TNode>>
): BaseNodeType<TNode, TOptional, TOther & VolatileAccessors<TVolatiles, TNode>>

/**
* Registers action methods for nodes of this type
Expand All @@ -36,7 +48,7 @@ export type BaseNodeType<TNode extends object, TKey extends keyof TNode | never,
actions: (n: TNode) => TActions
): BaseNodeType<
TNode,
TKey,
TOptional,
TOther & {
[k in keyof TActions]: PrependArgument<TActions[k], TNode>
}
Expand All @@ -55,7 +67,7 @@ export type BaseNodeType<TNode extends object, TKey extends keyof TNode | never,
getters: (n: TNode) => TGetters
): BaseNodeType<
TNode,
TKey,
TOptional,
TOther & {
[k in keyof TGetters]: PrependArgument<TGetters[k], TNode>
}
Expand All @@ -75,7 +87,7 @@ export type BaseNodeType<TNode extends object, TKey extends keyof TNode | never,
computeds: (n: TNode) => TComputeds
): BaseNodeType<
TNode,
TKey,
TOptional,
TOther & {
[k in keyof TComputeds]: TComputeds[k] extends () => any
? PrependArgument<TComputeds[k], TNode>
Expand All @@ -95,11 +107,26 @@ export type BaseNodeType<TNode extends object, TKey extends keyof TNode | never,
...properties: readonly K[]
): BaseNodeType<
TNode,
TKey,
TOptional,
TOther & {
[P in K as `set${Capitalize<P>}`]: (node: TNode, value: Readonly<TNode[P]>) => void
}
>

/**
* Define default values for keys in TOptional.
* When omitted, those properties are filled with the results of these generators.
*
* @template TGen - Record of default value generators
*/
defaults<TGen extends { [K in keyof TNode]?: () => TNode[K] }>(
defaultGenerators: TGen
): BaseNodeType<TNode, TOptional | (keyof TGen & keyof TNode), TOther>

/**
* Default generators defined so far.
*/
defaultGenerators?: { [K in keyof TNode]?: () => TNode[K] }
} & TOther

/**
Expand Down
20 changes: 2 additions & 18 deletions packages/mobx-bonsai/src/node/nodeTypeKey/KeyedNodeType.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { MarkOptional } from "ts-essentials"
import { BaseTypedNodeType } from "./BaseTypedNodeType"
import { NodeWithAnyType, NodeTypeKey, NodeKeyValue } from "./nodeTypeKey"
import { NodeKeyValue, NodeWithAnyType } from "./nodeTypeKey"

/**
* Node type that uses a specific property as a unique key for each node
*
* @template TNode - Node structure that adheres to this type
* @template TKey - Key field in the node structure
* @template TOptional - Optional keys in the node structure
* @template TOther - Additional properties and methods
*/

Expand All @@ -17,22 +17,6 @@ export type KeyedNodeType<
TNode,
TKey,
{
/**
* Creates a node of this type with the type property automatically set
*
* @param data - Initial node data
* @returns A node instance of this type
*/
(data: MarkOptional<TNode, NodeTypeKey | TKey>): TNode

/**
* Creates snapshot data of this type without instantiating a node
*
* @param data - Initial data
* @returns A data snapshot with the type property set
*/
snapshot(data: MarkOptional<TNode, NodeTypeKey | TKey>): TNode

/**
* Property name containing the node's unique key
*/
Expand Down
23 changes: 2 additions & 21 deletions packages/mobx-bonsai/src/node/nodeTypeKey/TypedNodeType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { MarkOptional } from "ts-essentials"
import { BaseTypedNodeType } from "./BaseTypedNodeType"
import { NodeWithAnyType, NodeTypeKey } from "./nodeTypeKey"
import { KeyedNodeType } from "./KeyedNodeType"
Expand All @@ -7,36 +6,18 @@ import { KeyedNodeType } from "./KeyedNodeType"
* Represents a node type with associated lifecycle and behavior
*
* @template TNode - Node structure that adheres to this type
* @template TKey - Key field in the node structure (if any)
* @template TOther - Additional properties and methods
*/
export type TypedNodeType<TNode extends NodeWithAnyType> = BaseTypedNodeType<
TNode,
never,
NodeTypeKey,
{
/**
* Creates a node of this type with the type property automatically set
*
* @param data - Initial node data
* @returns A node instance of this type
*/
(data: MarkOptional<TNode, NodeTypeKey>): TNode

/**
* Creates snapshot data of this type without instantiating a node
*
* @param data - Initial data
* @returns A data snapshot with the type property set
*/
snapshot(data: MarkOptional<TNode, NodeTypeKey>): TNode

/**
* Configures this type to use a specific property as the node key
*
* @template TKey - Property key in the node type
* @param key - Property name to use as the node key
* @returns A keyed node type using the specified property as key
*/
withKey<TKey extends keyof TNode>(key: TKey): KeyedNodeType<TNode, TKey>
withKey<TKey extends keyof TNode>(key: TKey): KeyedNodeType<TNode, NodeTypeKey | TKey>
}
>
22 changes: 1 addition & 21 deletions packages/mobx-bonsai/src/node/nodeTypeKey/UntypedNodeType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,4 @@ import { BaseNodeType } from "./BaseNodeType"
*
* @template TNode - The object type that represents the node's data structure.
*/
export type UntypedNodeType<TNode extends object> = BaseNodeType<
TNode,
never,
{
/**
* Creates a node of this type with the type property automatically set
*
* @param data - Initial node data
* @returns A node instance of this type
*/
(data: TNode): TNode

/**
* Creates snapshot data of this type without instantiating a node
*
* @param data - Initial data
* @returns A data snapshot with the type property set
*/
snapshot(data: TNode): TNode
}
>
export type UntypedNodeType<TNode extends object> = BaseNodeType<TNode, never, unknown>
Loading

0 comments on commit 62e4fb4

Please sign in to comment.