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
23 changes: 3 additions & 20 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,7 @@ jobs:
cache: pnpm
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Build project
run: pnpm build
- name: Run unit tests
run: pnpm bnt
benchmark:
name: Benchmark
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Run benchmark
run: node ./test/benchmark.js
run: pnpm test:coverage
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
node_modules/
node_modules/
TEST/
listen-keys/
React/
coverage/
dist/
5 changes: 3 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
**/types.ts
**/errors.ts
**/*.test.*
**/*.test.ts
tsconfig.json
test/

React/
img/

98 changes: 68 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Nano Stores DeepMap

\<img align="right" width="92" height="92" title="Nano Stores logo"
src="[https://nanostores.github.io/nanostores/logo.svg](https://nanostores.github.io/nanostores/logo.svg)"\>
<img align="right" width="64" height="64" title="Nano Stores logo"
src="https://nanostores.github.io/nanostores/logo.svg">

helper for [Nanostores](https://github.com/nanostores/nanostores) to create deep maps.
Deep maps extension for [Nano Stores](https://github.com/nanostores/nanostores) state manager.

## Install

Expand All @@ -13,51 +13,89 @@ npm install @nanostores/deep-map

## Usage

### Basic Usage

Import `deepMap` from this package instead of `nanostores` (which no longer has it).

Use `setKey` to create, replace, or delete any value at a specific path.

```ts
import { deepMap } from '@nanostores/deep-map'

const $store = deepMap({
count: 0,
type StoreProps = {
user?: {
name: string
age: number
}
count?: number
}

const $store = deepMap<StoreProps>({
user: {
name: 'Luke',
age: 19,
image_url: 'hhttps://example.com/default.png'
}
}
count: 0,
})

$store.setKey('count', 1)
// Replaces the value at 'count'
$store.setKey('count', 1) // -> { ...restValues, count: 1 }

```
Use `updateKey` to merge new data into an existing object. If the target isn't an object, it will be replaced.

```ts
// 'updateKey' merges, keeping 'name' and only changing 'age'
$store.updateKey('user', { age: 42 })
// -> { user: { name: 'Luke', age: 42 }, ... }
```
To delete a property from an object or an item from an array, just set its path to `undefined`.

```ts
// Deletes 'count' from the store
$store.setKey('count', undefined)
// -> { user: { name: 'Luke', age: 42 } }
```
### Working with Arrays
DeepMap fully supports arrays as the root value or nested within your state. You can use standard array syntax [index] in your paths.

```ts
//Before
const $store = deepMap<storeProps[]>([{}]) // Type Error

//After
const $store = deepMap<storeProps[]>([{}]) // OK

```

### TypeScript Support

The package is written in TypeScript and provides type inference:
The package is written in TypeScript and provides autocomplete for all methods and properties.

```ts
interface User {
id: number
name: string
settings: {
theme: 'light' | 'dark'
notifications: boolean
}
// Define your type with 'type' keyword instead of 'interface'
// for better autocomplete
type UserType = {
id?: string
}

const $user = deepMap<User>({
id: 1,
name: 'John',
settings: {
theme: 'light',
notifications: true
}
})
const $userT = deepMap<UserType>({})

$userT.setKey('id', 'uuidString') // Suggestion autocomplete

```
Typescript automatically infers the type of the store value.

```ts
type StoreProps = {
count?: number
}

const $store = deepMap<StoreProps>({})

$store.setKey('count', 'randomString')
// Type Error -> 'string' is not assignable to 'number'

// TypeScript (with a proper path utility type) can validate paths
$user.setKey('settings.theme', 'dark') // ✓ OK
$user.s_etKey('settings.theme', 'blue') // ✗ Type Error
// IMPORTANT: an empty path ('') is treated as the root object
$someStore.setKey('', 'randomString') // Replaces the entire store
```

## License
Expand All @@ -66,4 +104,4 @@ MIT

## Credits

* [Nanostores](https://github.com/nanostores/nanostores) - The original state manager.
* [Nano stores](https://github.com/nanostores/nanostores), the original state manager.
159 changes: 159 additions & 0 deletions deep-map/deepmap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { describe, it, expect } from 'vitest'
import { deepMap, getKey } from './deepmap.js'
import { setPath, getPath, normalizePath } from './path.js'

type Obj1 = {
a?: {
b?: string
d?: boolean
}
c?: string
arr?: string[]
}
type Arr1 = Array<Obj1>

describe('Test initial values', () => {
it('Should initialize with object value', () => {
const $storeObj1 = deepMap<Obj1>({ c: 'initial' })
expect($storeObj1.value).toEqual({ c: 'initial' })
})

it('Should initialize with array value', () => {
const $storeArr1 = deepMap<Arr1>([{ c: 'initial' }])
expect($storeArr1.value).toEqual([{ c: 'initial' }])
})
})

describe('Test setKey', () => {
it('Should set key in object', () => {
const $storeObj1 = deepMap<Obj1>({ c: 'initial' })
$storeObj1.setKey('a.b', 'value')
expect($storeObj1.value).toEqual({ a: { b: 'value' }, c: 'initial' })
})

it('Should set key in array', () => {
const $storeArr1 = deepMap<Arr1>([{ c: 'initial' }])
$storeArr1.setKey('[0].a.b', 'value')
expect($storeArr1.value).toEqual([{ a: { b: 'value' }, c: 'initial' }])
})

it('Should delete object property when set to undefined', () => {
const $storeObj1 = deepMap<Obj1>({ c: 'initial' })
$storeObj1.setKey('c', undefined)
expect($storeObj1.value).toEqual({})
})

it('Should delete array item when set to undefined', () => {
const $storeObj1 = deepMap<Obj1>({ arr: ['initial'] })
$storeObj1.setKey('arr[0]', undefined)
expect($storeObj1.value).toEqual({ arr: [] })
})

it('Should replace root object', () => {
const $storeObj1 = deepMap<Obj1>({ c: 'initial' })
$storeObj1.setKey('', { a: { b: 'value' } })
expect($storeObj1.value).toEqual({ a: { b: 'value' } })
})

it('Should replace root array', () => {
const $storeArr1 = deepMap<Arr1>([{ c: 'initial' }])
$storeArr1.setKey('[0].c', undefined)
expect($storeArr1.value).toEqual([{}])
})

it('Should add to array at new index', () => {
const $storeArr1 = deepMap<Obj1>({ arr: ['initial'] })
$storeArr1.setKey('arr[1]', 'value')
expect($storeArr1.value).toEqual({ arr: ['initial', 'value'] })
})
})

describe('Test updateKey', () => {
it('Should update/merge key in object', () => {
const $storeObj1 = deepMap<Obj1>({ a: { b: 'value' } })
$storeObj1.updateKey('a', { d: true })
expect($storeObj1.value).toEqual({ a: { b: 'value', d: true } })
})

it('Should update/merge key in array', () => {
const $storeArr1 = deepMap<Obj1[]>([{ c: 'initial' }])
$storeArr1.updateKey('', [{c: 'updated'}])
expect($storeArr1.value).toEqual([{c: 'initial'}, {c: 'updated'}])
})
})


describe('Test path functions', () => {
it('Should throw error when path is undefined', () => {
const badCall = () => {
normalizePath(undefined as any)
}

expect(badCall).toThrow()
})
})


describe('getPath', () => {
const state = {
user: {
name: 'John',
tags: ['a', 'b'],
},
c: 2,
}

it('Should get a root value', () => {
expect(getPath('c', state)).toBe(2)
})

it('Should get a nested object value', () => {
expect(getPath('user.name', state)).toBe('John')
})

it('Should get a nested array value', () => {
expect(getPath('user.tags[0]', state)).toBe('a')
})
})

describe('setPath', () => {
const originalState = {
a: { b: 1 },
c: 2,
}

it('Should set a nested value immutably', () => {
const newState = setPath('a.b', 99, originalState)

expect(newState).toEqual({ a: { b: 99 }, c: 2 })

expect(originalState).toEqual({ a: { b: 1 }, c: 2 })

expect(newState).not.toBe(originalState)
expect(newState.a).not.toBe(originalState.a)
})

it('Should delete a key when value is undefined', () => {
const state = { a: 1, b: 2 }
const newState = setPath('b', undefined, state)

expect(newState).toEqual({ a: 1 })
expect(state).toEqual({ a: 1, b: 2 })
})

it('Should add items to an array immutably', () => {
const state = { items: ['a', 'b'] }
const newState = setPath('items[2]', 'c', state)

expect(newState).toEqual({ items: ['a', 'b', 'c'] })
expect(state).toEqual({ items: ['a', 'b'] })
expect(newState.items).not.toBe(state.items)
})
})

describe('getKey', () => {
it('Should get a value from store', () => {
const $storeObj1 = deepMap<Obj1>({ c: 'initial' })
expect(getKey($storeObj1, 'c')).toBe('initial')
})
})
Loading