Skip to content

Commit

Permalink
adds onAction function
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcisbee committed Jun 21, 2021
1 parent 92d0e7d commit af25bde
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.13.0

### Feature
* Adds new `onAction` method that triggers callback whenever specific action is called.

## 0.12.4

### Bugfixes
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,45 @@ function App() {
```
[__Open in Codesandbox__](https://codesandbox.io/s/exome-counter-96qfq)

### `onAction`
Function that calls callback whenever specific action on Exome is called.

```ts
function onAction(store: typeof Exome): Unsubscribe
```

__Arguments__
1. `store` _([Exome](#exome) constructor)_: Store that has desired action to listen to.
2. `action` _(string)_: method (action) name on store instance.
3. `callback` _(Function)_: Callback that will be triggered before or after action.<br>
__Arguments__
- `instance` _([Exome](#exome))_: Instance where action is taking place.
- `action` _(String)_: Action name.
- `payload` _(any[])_: Array of arguments passed in action.<br>
4. `type` _("before" | "after")_: when to run callback - before or after action, default is `"after"`.

__Returns__

- _Function_: Unsubscribes this action listener

__Example__

```tsx
import { onAction } from "exome"
const unsubscribe = onAction(
Person,
'rename',
(instance, action, payload) => {
console.log(`Person ${instance} was renamed to ${payload[0]}`);

// Unsubscribe is no longer needed
unsubscribe();
},
'before'
)
```

### `saveState`
Function that saves snapshot of current state for any Exome and returns string.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "exome",
"version": "0.12.4",
"version": "0.13.0",
"description": "Proxy based store manager for deeply nested states",
"main": "exome.js",
"module": "exome.esm.js",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { updateView } from './utils/update-view'
export { saveState } from './utils/save-state'
export { loadState, registerLoadable } from './utils/load-state'
export { addMiddleware, Middleware } from './middleware'
export { onAction } from './utils/on-action'
52 changes: 52 additions & 0 deletions src/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,56 @@ test('runs middleware unsubscribe method', () => {
assert.equal(unsubscribe.args[0], [])
})

test('removes middleware correctly', () => {
const instance = new Exome()
const middleware = fake()

const unsubscribe = addMiddleware(middleware)

runMiddleware(instance, 'actionName', [])

assert.equal(middleware.callCount, 1)
assert.equal(middleware.args[0], [instance, 'actionName', []])

runMiddleware(instance, 'actionName', [])

assert.equal(middleware.callCount, 2)
assert.equal(middleware.args[1], [instance, 'actionName', []])

unsubscribe()
runMiddleware(instance, 'actionName', [])

assert.equal(middleware.callCount, 2)
})

test('removes multiple middleware correctly', () => {
const instance = new Exome()
const middleware1 = fake()
const middleware2 = fake()

const unsubscribe1 = addMiddleware(middleware1)
const unsubscribe2 = addMiddleware(middleware2)

runMiddleware(instance, 'actionName', [])

assert.equal(middleware1.callCount, 1)
assert.equal(middleware1.args[0], [instance, 'actionName', []])
assert.equal(middleware2.callCount, 1)
assert.equal(middleware2.args[0], [instance, 'actionName', []])

runMiddleware(instance, 'actionName', [])

assert.equal(middleware1.callCount, 2)
assert.equal(middleware1.args[1], [instance, 'actionName', []])
assert.equal(middleware2.callCount, 2)
assert.equal(middleware2.args[1], [instance, 'actionName', []])

unsubscribe1()
unsubscribe2()
runMiddleware(instance, 'actionName', [])

assert.equal(middleware1.callCount, 2)
assert.equal(middleware2.callCount, 2)
})

test.run()
7 changes: 6 additions & 1 deletion src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { Exome } from './exome'
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
export type Middleware = (instance: Exome, action: string, payload: any[]) => (void | (() => void))

const middleware: Middleware[] = []
export const middleware: Middleware[] = []

export function addMiddleware(fn: Middleware) {
middleware.push(fn)

return () => {
const index = middleware.indexOf(fn)
middleware.splice(index, 1)
}
}

export function runMiddleware(...params: Parameters<Middleware>) {
Expand Down
203 changes: 203 additions & 0 deletions src/utils/on-action.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { fake } from 'sinon'
import { test } from 'uvu'
import assert from 'uvu/assert'

import { Exome } from '../exome'
import { middleware, runMiddleware } from '../middleware'

import { onAction } from './on-action'

test.before.each(() => {
middleware.splice(0, 100)
})

test('exports `onAction`', () => {
assert.ok(onAction)
})

test('that `onAction` is function', () => {
assert.instance(onAction, Function)
})

test('adds before middleware without errors', () => {
class Person extends Exome {
constructor(
public name?: string
) {
super()
}

public rename(name: string) {
this.name = name
}
}

const person = new Person('John')
const handler = fake()

onAction(Person, 'rename', handler, 'before')

const after = runMiddleware(person, 'rename', [1])

assert.equal(handler.callCount, 1)
assert.equal(handler.args[0], [person, 'rename', [1]])

after()

assert.equal(handler.callCount, 1)
})

test('adds after middleware without errors', () => {
class Person extends Exome {
constructor(
public name?: string
) {
super()
}

public rename(name: string) {
this.name = name
}
}

const person = new Person('John')
const handler = fake()

onAction(Person, 'rename', handler, 'after')

const after = runMiddleware(person, 'rename', [1])

assert.equal(handler.callCount, 0)

after()

assert.equal(handler.callCount, 1)
assert.equal(handler.args[0], [person, 'rename', [1]])
})

test('calls NEW action correctly', async() => {
class Person extends Exome {
constructor(
public name?: string
) {
super()
}

public rename(name: string) {
this.name = name
}
}

const handler = fake()
onAction(Person, 'NEW', handler)

assert.equal(handler.callCount, 0)

// eslint-disable-next-line no-new
new Person('John')

await new Promise((resolve) => setTimeout(resolve, 10))

assert.equal(handler.callCount, 1)
assert.instance(handler.args[0][0], Person)
assert.equal(handler.args[0][1], 'NEW')
assert.equal(handler.args[0][2].length, 0)
})

test('calls any action correctly', async() => {
class Person extends Exome {
constructor(
public name?: string
) {
super()
}

public rename(name: string) {
this.name = name
}
}

const handler = fake()
onAction(Person, null, handler)

assert.equal(handler.callCount, 0)

const person = new Person('John')

await new Promise((resolve) => setTimeout(resolve, 10))

assert.equal(handler.callCount, 1)
assert.instance(handler.args[0][0], Person)
assert.equal(handler.args[0][1], 'NEW')
assert.equal(handler.args[0][2].length, 0)

person.rename('Jane')

assert.equal(handler.callCount, 2)
assert.equal(handler.args[1], [person, 'rename', ['Jane']])
})

test('calls custom action correctly', () => {
class Person extends Exome {
constructor(
public name?: string
) {
super()
}

public rename(name: string) {
this.name = name
}
}

const handler = fake()
onAction(Person, 'rename', handler)

assert.equal(handler.callCount, 0)

const person = new Person('John')

assert.equal(handler.callCount, 0)

person.rename('Jane')

assert.equal(handler.callCount, 1)
assert.equal(handler.args[0], [person, 'rename', ['Jane']])
})

test('unsubscribes correctly', () => {
class Person extends Exome {
constructor(
public name?: string
) {
super()
}

public rename(name: string) {
this.name = name
}
}

const handler = fake()
const unsubscribe = onAction(Person, 'rename', handler)

assert.equal(handler.callCount, 0)

const person = new Person('John')

assert.equal(handler.callCount, 0)

person.rename('Jane')

assert.equal(handler.callCount, 1)

person.rename('Janine')

assert.equal(handler.callCount, 2)

unsubscribe()

assert.equal(handler.callCount, 2)
})

test.run()
26 changes: 26 additions & 0 deletions src/utils/on-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Exome } from '../exome'
import { addMiddleware } from '../middleware'

type Unsubscribe = () => void

export function onAction<T extends Exome>(
Parent: new (...args: any[]) => T,
action: null | 'NEW' | keyof T,
callback: (instance: T, action: 'NEW' | keyof T, payload: any[]) => void,
type: 'before' | 'after' = 'after'
): Unsubscribe {
return addMiddleware((instance, targetAction, payload) => {
if (!(instance instanceof Parent && (targetAction === action || action === null))) {
return
}

if (targetAction === 'NEW' || type === 'before') {
callback(instance, targetAction as any, payload)
return
}

return () => {
callback(instance, targetAction as any, payload)
}
})
}

0 comments on commit af25bde

Please sign in to comment.