Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2026-02-09

### Added

- Per-field dirty tracking via `isDirtyByField` — know exactly which fields have been modified, with automatic parent path marking (e.g., changing `address.street` also marks `address` as dirty)
- New `DirtyFields` type export for typing per-field dirty state

### Changed

- `isDirty` is now derived from `isDirtyByField`, so both stay perfectly in sync
- `reset()`, `rollback()`, and successful actions clear all per-field dirty state

## [1.3.0] - 2026-01-31

### Added
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Note: The demo has its own `node_modules` and uses Zod for some validation examp

### Core Files

- `src/index.ts` - Public exports: `createSvState`, validator builders, types (`Snapshot`, `EffectContext`, `SnapshotFunction`, `SvStateOptions`, `Validator`, `AsyncValidator`, `AsyncValidatorFunction`, `AsyncErrors`)
- `src/index.ts` - Public exports: `createSvState`, validator builders, types (`Snapshot`, `EffectContext`, `SnapshotFunction`, `SvStateOptions`, `Validator`, `AsyncValidator`, `AsyncValidatorFunction`, `AsyncErrors`, `DirtyFields`)
- `src/state.svelte.ts` - Main `createSvState<T, V, P>()` function with snapshot/undo system and async validation
- `src/proxy.ts` - `ChangeProxy` deep reactive proxy implementation
- `src/validators.ts` - Fluent validator builders (string, number, array, date)
Expand All @@ -99,7 +99,8 @@ const { data, execute, state, rollback, reset } = createSvState(init, actuators?
- `state` - Object containing reactive stores:
- `errors: Readable<V | undefined>` - Validation errors (sync)
- `hasErrors: Readable<boolean>` - Whether any sync validation errors exist
- `isDirty: Readable<boolean>` - Whether state has been modified
- `isDirty: Readable<boolean>` - Whether state has been modified (derived from `isDirtyByField`)
- `isDirtyByField: Readable<DirtyFields>` - Per-field dirty tracking; keys are dot-notation property paths. When a nested field changes, all parent paths are also marked dirty (e.g., changing `customer.address.street` marks `customer.address` and `customer` as dirty). Cleared on `reset()`, `rollback()`, and successful action (respecting `resetDirtyOnAction`).
- `actionInProgress: Readable<boolean>` - Action execution status
- `actionError: Readable<Error | undefined>` - Last action error
- `snapshots: Readable<Snapshot<T>[]>` - Snapshot history for undo
Expand Down
71 changes: 63 additions & 8 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,47 @@ const { data, state: { errors, hasErrors } } = createSvState(

---

### How does per-field dirty tracking work?

svstate tracks which specific fields have been modified via the `isDirtyByField` store. It returns a `DirtyFields` object where keys are dot-notation property paths and values are `true`:

```typescript
const {
data,
state: { isDirty, isDirtyByField }
} = createSvState({
name: '',
address: { street: '', city: '' }
});

data.address.street = '123 Main St';

// $isDirtyByField → { "address.street": true, "address": true }
// $isDirty → true (derived from isDirtyByField)
```

**Key behaviors:**

- When a nested field changes, all parent paths are also marked dirty (e.g., changing `address.street` also marks `address` as dirty)
- `isDirty` is derived from `isDirtyByField` — it's `true` when any field is dirty
- Cleared on `reset()`, `rollback()`, and successful action (respecting `resetDirtyOnAction`)
- Useful for highlighting changed fields in the UI or showing "unsaved changes" per section

```svelte
<!-- Highlight changed fields -->
<input
bind:value={data.name}
class:modified={$isDirtyByField['name']}
/>

<!-- Show section-level dirty indicator -->
{#if $isDirtyByField['address']}
<span class="badge">Modified</span>
{/if}
```

---

## Validation

### How does validation work and when does it run?
Expand Down Expand Up @@ -424,16 +465,30 @@ execute(); // Default save
svstate exports these types for building type-safe external functions:

```typescript
import type { Validator, EffectContext, Snapshot, SnapshotFunction, SvStateOptions } from 'svstate';
import type {
Validator,
EffectContext,
Snapshot,
SnapshotFunction,
SvStateOptions,
AsyncValidator,
AsyncValidatorFunction,
AsyncErrors,
DirtyFields
} from 'svstate';
```

| Type | Use Case |
| ------------------ | --------------------------------------------- |
| `Validator` | Type for validation error objects |
| `EffectContext<T>` | Type effect callbacks when defined externally |
| `SnapshotFunction` | Type for the `snapshot` function parameter |
| `Snapshot<T>` | Type for snapshot history entries |
| `SvStateOptions` | Type for configuration options |
| Type | Use Case |
| --------------------------- | ------------------------------------------------------------ |
| `Validator` | Type for validation error objects |
| `EffectContext<T>` | Type effect callbacks when defined externally |
| `SnapshotFunction` | Type for the `snapshot` function parameter |
| `Snapshot<T>` | Type for snapshot history entries |
| `SvStateOptions` | Type for configuration options |
| `AsyncValidator<T>` | Object mapping property paths to async validator functions |
| `AsyncValidatorFunction<T>` | Async function: `(value, source, signal) => Promise<string>` |
| `AsyncErrors` | Object mapping property paths to error strings |
| `DirtyFields` | Object mapping dot-notation paths to dirty status |

**Example:**

Expand Down
31 changes: 17 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const customer = $state({
- 🌐 **Async validation** for server-side checks (username availability, email verification)
- ⚡ **Fires effects** when any property changes (with full context)
- ⏪ **Snapshots & undo** for complex editing workflows
- 🎯 **Tracks dirty state** automatically
- 🎯 **Tracks dirty state** automatically (per-field and aggregate)
- 🔧 **Supports methods** on state objects for computed values and formatting

```typescript
Expand Down Expand Up @@ -855,7 +855,8 @@ Creates a supercharged state object.
| `reset()` | `() => void` | Return to initial state |
| `state.errors` | `Readable<V>` | Sync validation errors store |
| `state.hasErrors` | `Readable<boolean>` | Has sync errors? |
| `state.isDirty` | `Readable<boolean>` | Has state changed? |
| `state.isDirty` | `Readable<boolean>` | Has state changed? (derived from `isDirtyByField`) |
| `state.isDirtyByField` | `Readable<DirtyFields>` | Per-field dirty tracking (dot-notation paths) |
| `state.actionInProgress` | `Readable<boolean>` | Is action running? |
| `state.actionError` | `Readable<Error>` | Last action error |
| `state.snapshots` | `Readable<Snapshot[]>` | Undo history |
Expand Down Expand Up @@ -890,7 +891,8 @@ import type {
SvStateOptions,
AsyncValidator,
AsyncValidatorFunction,
AsyncErrors
AsyncErrors,
DirtyFields
} from 'svstate';
```

Expand All @@ -904,6 +906,7 @@ import type {
| `AsyncValidator<T>` | Object mapping property paths to async validator functions |
| `AsyncValidatorFunction<T>` | Async function: `(value, source, signal) => Promise<string>` |
| `AsyncErrors` | Object mapping property paths to error strings |
| `DirtyFields` | Object mapping dot-notation property paths to `boolean` dirty status |

**Example: External validator and effect functions**

Expand Down Expand Up @@ -944,17 +947,17 @@ const { data, state } = createSvState<UserData, UserErrors, object>(

## 🎨 Why svstate?

| Feature | Native Svelte 5 | svstate |
| ---------------------- | ------------------ | --------------- |
| Simple flat objects | ✅ Great | ✅ Great |
| Deep nested objects | ⚠️ Manual tracking | ✅ Automatic |
| Property change events | ❌ Not available | ✅ Full context |
| Structured validation | ❌ DIY | ✅ Mirrors data |
| Async validation | ❌ DIY | ✅ Built-in |
| Undo/Redo | ❌ DIY | ✅ Built-in |
| Dirty tracking | ❌ DIY | ✅ Automatic |
| Action loading states | ❌ DIY | ✅ Built-in |
| State with methods | ⚠️ Manual cloning | ✅ Automatic |
| Feature | Native Svelte 5 | svstate |
| ---------------------- | ------------------ | ------------------------ |
| Simple flat objects | ✅ Great | ✅ Great |
| Deep nested objects | ⚠️ Manual tracking | ✅ Automatic |
| Property change events | ❌ Not available | ✅ Full context |
| Structured validation | ❌ DIY | ✅ Mirrors data |
| Async validation | ❌ DIY | ✅ Built-in |
| Undo/Redo | ❌ DIY | ✅ Built-in |
| Dirty tracking | ❌ DIY | ✅ Automatic (per-field) |
| Action loading states | ❌ DIY | ✅ Built-in |
| State with methods | ⚠️ Manual cloning | ✅ Automatic |

**svstate is for:**

Expand Down
Loading