-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compose): started a basic compose extension
- Loading branch information
1 parent
5a9ca86
commit aecf3e1
Showing
10 changed files
with
340 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# Harlem Storage Extension | ||
|
||
![npm](https://img.shields.io/npm/v/@harlem/extension-storage) | ||
|
||
This is the official storage extension for Harlem. The storage extension adds the ability to sync store state to/from `localStorage` or `sessionStorage`. | ||
|
||
## Getting Started | ||
|
||
Follow the steps below to get started using the storage extension. | ||
|
||
### Installation | ||
|
||
Before installing this extension make sure you have installed `@harlem/core`. | ||
|
||
```bash | ||
yarn add @harlem/extension-storage | ||
# or | ||
npm install @harlem/extension-storage | ||
``` | ||
|
||
### Registration | ||
|
||
To get started simply register this extension with the store you wish to extend. | ||
|
||
```typescript | ||
import storageExtension from '@harlem/extension-storage'; | ||
|
||
import { | ||
createStore | ||
} from '@harlem/core'; | ||
|
||
const STATE = { | ||
firstName: 'Jane', | ||
lastName: 'Smith' | ||
}; | ||
|
||
const { | ||
state, | ||
getter, | ||
mutation, | ||
startStorageSync, | ||
stopStorageSync, | ||
clearStorage | ||
} = createStore('example', STATE, { | ||
extensions: [ | ||
storageExtension({ | ||
type: 'local', | ||
prefix: 'harlem', | ||
sync: true, | ||
exclude: [], | ||
serialiser: state => JSON.stringify(state), | ||
parser: value => JSON.parse(value) | ||
}) | ||
] | ||
}); | ||
``` | ||
|
||
The storage extension adds several new methods to the store instance (highlighted above). | ||
|
||
|
||
## Usage | ||
|
||
### Options | ||
The storage extension method accepts an options object with the following properties: | ||
- **type**: `string` - The type of storage interface to use. Acceptable values are `local` or `session`. Default value is `local`. | ||
- **prefix**: `string` - The prefix to use on the storage key. The storage value will be in the form `${prefix}:${storeName}`. Default value is `harlem`. | ||
- **sync**: `boolean` - Whether to automatically sync changes from the storage interface back to the store. Default value is `true`. | ||
- **exclude**: `string[]` - A list of mutation names to exclude from triggering a storage sync event. | ||
- **serialiser**: `unknown => string` - A function to serialise the store to string. The default behaviour is `JSON.stringify`. | ||
- **parser**: `string => unknown` - A function to serialise the storage string to a state structure. The default behaviour is `JSON.parse`. | ||
|
||
### Manually starting/stopping sync | ||
The `startStorageSync` and `stopStorageSync` methods can be used to start or stop sync behaviour. | ||
|
||
|
||
### Clearing storage | ||
Use the `clearStorage` method to clear all stored data relating to this store. | ||
|
||
|
||
## Considerations | ||
Please keep the following points in mind when using this extension: | ||
|
||
- The default behaviour for serialising/parsing only supports JSON-compatible types. For non-JSON-compatible types please specify a custom serialiser/parser. See [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) for a list of JSON-compatible types. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"name": "@harlem/extension-compose", | ||
"amdName": "harlemCompose", | ||
"version": "2.1.0", | ||
"license": "MIT", | ||
"author": "Andrew Courtice <[email protected]>", | ||
"description": "The official compose extension for Harlem", | ||
"homepage": "https://harlemjs.com", | ||
"source": "src/index.ts", | ||
"main": "dist/index.js", | ||
"module": "dist/esm/index.js", | ||
"unpkg": "dist/iife/index.js", | ||
"jsdelivr": "dist/iife/index.js", | ||
"types": "dist/index.d.ts", | ||
"private": true, | ||
"exports": { | ||
".": { | ||
"import": "./dist/esm/index.js", | ||
"require": "./dist/index.js" | ||
} | ||
}, | ||
"keywords": [ | ||
"vue", | ||
"state", | ||
"harlem", | ||
"extension", | ||
"compose" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/andrewcourtice/harlem.git", | ||
"directory": "extensions/compose" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/andrewcourtice/harlem/issues" | ||
}, | ||
"scripts": { | ||
"dev": "tsup --watch src", | ||
"build": "tsup", | ||
"prepublish": "yarn build" | ||
}, | ||
"dependencies": { | ||
"@harlem/utilities": "^2.1.0" | ||
}, | ||
"peerDependencies": { | ||
"@harlem/core": "^2.0.0", | ||
"vue": "^3.2.0" | ||
}, | ||
"devDependencies": { | ||
"@harlem/core": "^2.1.0", | ||
"vue": "^3.2.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const SENDER = 'extension:compose'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { | ||
SENDER, | ||
} from './constants'; | ||
|
||
import { | ||
computed, DeepReadonly, | ||
} from 'vue'; | ||
|
||
import { | ||
BaseState, | ||
InternalStore, | ||
} from '@harlem/core'; | ||
|
||
import { | ||
fromPath, | ||
} from '@harlem/utilities'; | ||
|
||
import type { | ||
Accessor, | ||
Getter, | ||
Setter, | ||
} from './types'; | ||
|
||
export * from './types'; | ||
|
||
function traceObjectPath<TValue extends object>(onAccess: (key: PropertyKey) => void): TValue { | ||
return new Proxy({} as TValue, { | ||
get(target, key) { | ||
onAccess(key); | ||
return traceObjectPath(onAccess); | ||
}, | ||
}); | ||
} | ||
|
||
function getTraceObject<TValue extends object>() { | ||
const nodes = new Set<PropertyKey>(); | ||
const value = traceObjectPath<TValue>(key => nodes.add(key)); | ||
const getNodes = () => Array.from(nodes); | ||
const resetNodes = () => nodes.clear(); | ||
|
||
return { | ||
value, | ||
getNodes, | ||
resetNodes, | ||
}; | ||
} | ||
|
||
export default function composeExtension<TState extends BaseState>() { | ||
|
||
return (store: InternalStore<TState>) => { | ||
const { | ||
value, | ||
getNodes, | ||
resetNodes, | ||
} = getTraceObject<TState>(); | ||
|
||
function useState<TValue>(accessor: Accessor<TState, TValue>, mutationName?: string): [Getter<TValue>, Setter<TValue>] { | ||
accessor(value); | ||
|
||
const nodes = getNodes(); | ||
const key = nodes.pop(); | ||
const name = mutationName || `compose:${String(key)}`; | ||
const parent = (state: object) => fromPath(state, nodes) as Record<PropertyKey, unknown>; | ||
|
||
resetNodes(); | ||
|
||
if (!key) { | ||
throw new Error('A valid property must be used'); | ||
} | ||
|
||
const getter = () => parent(store.state)[key] as DeepReadonly<TValue>; | ||
const setter = (value: TValue) => store.write(name, SENDER, state => { | ||
parent(state)[key] = value; | ||
}); | ||
|
||
return [ | ||
getter, | ||
setter, | ||
]; | ||
} | ||
|
||
function computeState<TValue>(accessor: Accessor<TState, TValue>, mutationName?: string) { | ||
const [ | ||
getter, | ||
setter, | ||
] = useState(accessor, mutationName); | ||
|
||
return computed({ | ||
get: () => getter() as TValue, | ||
set: value => setter(value), | ||
}); | ||
} | ||
|
||
return { | ||
useState, | ||
computeState, | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { DeepReadonly } from '@vue/reactivity'; | ||
|
||
export type Accessor<TState, TValue> = (state: TState) => TValue; | ||
export type Getter<TValue> = () => DeepReadonly<TValue>; | ||
export type Setter<TValue> = (value: TValue) => void; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { | ||
bootstrap, | ||
getStore, | ||
} from '@harlem/testing'; | ||
|
||
import composeExtension from '../src'; | ||
|
||
describe('Compose Extension', () => { | ||
|
||
beforeAll(() => bootstrap()); | ||
|
||
test('Use state with simple value', () => { | ||
const { | ||
store, | ||
} = getStore({ | ||
extensions: [ | ||
composeExtension(), | ||
], | ||
}); | ||
|
||
const { | ||
useState, | ||
} = store; | ||
|
||
const [ | ||
getFirstName, | ||
setFirstName, | ||
] = useState(state => state.details.firstName); | ||
|
||
expect(getFirstName()).toBe(''); | ||
setFirstName('John'); | ||
expect(getFirstName()).toBe('John'); | ||
}); | ||
|
||
test('Use state with complex value', () => { | ||
const { | ||
store, | ||
} = getStore({ | ||
extensions: [ | ||
composeExtension(), | ||
], | ||
}); | ||
|
||
const { | ||
useState, | ||
} = store; | ||
|
||
const [ | ||
getDetails, | ||
setDetails, | ||
] = useState(state => state.details); | ||
|
||
let details = getDetails(); | ||
|
||
expect(details.age).toBe(0); | ||
|
||
setDetails({ | ||
firstName: 'Bill', | ||
lastName: 'Smith', | ||
age: 45, | ||
}); | ||
|
||
details = getDetails(); | ||
|
||
expect(details.age).toBe(45); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"include": [ | ||
"src/**/*.ts", | ||
"../../global.d.ts" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import base from '../../tsup.config'; | ||
|
||
import type { | ||
Options, | ||
} from 'tsup'; | ||
|
||
export default { | ||
...base, | ||
globalName: 'HarlemComposeExtension', | ||
} as Options; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import isArray from '../type/is-array'; | ||
|
||
export default function fromPath<TValue extends object>(value: TValue, path: string | PropertyKey[]): object | undefined { | ||
export default function fromPath<TValue extends object>(value: TValue, path: string | PropertyKey[]): unknown { | ||
const nodes = isArray(path) ? path : path.split('/'); | ||
return nodes.reduce((branch, node) => (branch as any)?.[node], value); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters