Skip to content

Commit

Permalink
Merge pull request #3 from lukasborawski/chore/issue-2-provide-destro…
Browse files Browse the repository at this point in the history
…y-destination

chore(#2): destroy destination
  • Loading branch information
lukasborawski authored Jun 9, 2022
2 parents 52ae1a2 + afc7a0b commit f499954
Show file tree
Hide file tree
Showing 18 changed files with 1,017 additions and 788 deletions.
98 changes: 83 additions & 15 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## Vue Use State Effect

<a href="https://badge.fury.io/js/vue-use-state-effect"><img src="https://img.shields.io/github/workflow/status/lukasborawski/vue-use-state-effect/CI" alt="npm version" height="18"></a>
<a href="https://badge.fury.io/js/vue-use-state-effect"><img src="https://d25lcipzij17d.cloudfront.net/badge.svg?id=js&r=r&type=6e&v=0.1.1&x2=0" alt="npm version" height="18"></a>
<a href="https://badge.fury.io/js/vue-use-state-effect"><img src="https://d25lcipzij17d.cloudfront.net/badge.svg?id=js&r=r&type=6e&v=0.1.2&x2=0" alt="npm version" height="18"></a>
<a href="https://badge.fury.io/js/vue-use-state-effect"><img src="https://img.shields.io/bundlephobia/min/vue-use-state-effect" alt="npm version" height="18"></a>

**CAUTION**: Built and tested for/with **Vue 3** and/or **Nuxt 3**.
**CAUTION**: Built and tested for/with **Vue 3** and/or **Nuxt 3** (RC-2).

Fast and small library (composable), built on top of the native `EffectScope` **Vue 3 API** that will provide safe and sharable (across the app) state for your local composables and functions. It might be a good replacement / alternative for **Vuex** or **Pinia** state management, if you need smaller and less extensive solution.

Expand Down Expand Up @@ -44,19 +44,26 @@ const composable = () => {
/* your composable logic here */
}

export const useSharedComposable = useStateEffect(composable)
export const useSharedComposable = useStateEffect(composable, { ...config })
```

Interface (**TypeScript**).

```typescript
interface UseStateEffectConfig {
readonly name?: string | null
readonly destroy?: boolean
readonly destroy?: boolean | 'custom'
readonly debug?: boolean
}
export type UseStateEffectOptions<T = any> = {
destroyLabels: string[]
props: ExtractPropTypes<{ stateEffectDestroyLabel: string } | T>
}

export function useStateEffect<T extends (...args: any[]) => any>(composable: T, config?: UseStateEffectConfig): () => {
export function useStateEffect<T extends (...args: any[]) => ReturnType<T>>(
composable: T,
config?: UseStateEffectConfig,
): (opts?: UseStateEffectOptions<opts.props>) => {
[keyof in string | 'state']: ReturnType<T>
}
```
Expand All @@ -73,7 +80,7 @@ You can use some options to define your usage preferences.

- **type:** `String | 'state'`

- **default**: `state`
- **default**: `'state'`

- **description**: name of composable state object that you'll be referring to inside your components, if not defined by default your state object will get `state` key, please note that it's not read automatically and that's because of application build mode functions name-spaces formatting

Expand All @@ -83,7 +90,7 @@ You can use some options to define your usage preferences.

- **default**: `false`

- **description**: if set to `true` it will turn on the debug mode, you will be able to see the shared composable body
- **description**: if set to `true` it will turn on the debug mode, you will be able to see the shared composable body / state

- **tip**: you can turn it on for the development mode

Expand All @@ -93,19 +100,78 @@ You can use some options to define your usage preferences.

### `destroy`

- **type**: `Boolean`
- **type**: `Boolean` | `'custom'`

- **default**: `false`

- **description**: if set to `true` composable state will be destroyed after component `onBeforeUnmount` hook
- **description**: if set to `true` composable state will be destroyed after component `onBeforeUnmount` hook, if set to `custom` it will be waiting for custom setup (described below) and destroyed `onBeforeMount` hook

#### Destroy Destination (Custom) ✨ `from 0.1.2`

You can decide where the state will be destroyed (re-initialized). You will achieve this by passing special property and corresponding label that will point place / component where it should be executed.

For this one you can pass inline options to the `useStateEffect` composable while invoking within the components.

Here is a simple example of how to use it (the whole config).
```typescript
export type UseStateEffectOptions<T = any> = {
readonly destroyLabels: string[]
readonly props: ExtractPropTypes<{ stateEffectDestroyLabel: string } | T>
}
```

Let's say that `SharedStateComponent.vue` component is reused across the application, and you're displaying here some shared data from the `useSharedState` composable. And it will always be updated and aligned with the state unless the passed property from the parent component will not meet the same custom label defined as a `destroyLabels` (you can use multiple) with your composable invocation. Like this.

```vue
<!-- SharedStateComponent.vue -->
<script setup lang="ts">
import { useSharedState } from '@composables/useSharedState'
const props = defineProps({
stateEffectDestroyLabel: { type: String, default: 'Label' },
})
const {
sharedState: { data },
} = useSharedState({ destroyLabels: ['CustomLabel'], props })
</script>
```

*please check the [example](#example) for better context

And this is how you can use it along with the real component or page.

```vue
<!-- New Page | New.vue -->
<template>
<shared-state-component state-effect-destroy-label="CustomLabel" />
</template>
```

So while this `New.vue` component will be initialized (just before mounting) the state that you've established in the `SharedStateComponent.vue` component will be destroyed (re-initialized).


> **WARNING**!
>
> Please don't try to destroy the state in the same component where you're updating (setting) the new data for it. It will be caught with the same lifecycle loop and destroyed after component termination. Finally, passed as empty.
>
> ![Diagram](https://share.getcloudapp.com/eDuXxk88/download/use-state-effect-destroy.svg)
>
> To destroy state just after component unmount event you can (and probably you should 😊) use [straight form](#destroy) of the destroy feature with `true` value.

Great! You can check it in action with the special Nuxt 3 [StackBlitz](https://stackblitz.com/edit/vue-use-state-effect-demo) demo.

---

In the end, here is a simple example of how to use the whole config.

```javascript
export const useComposable = useStateEffect(useSharedComposable, {
name: 'useSharedComposable',
debug: true,
destroy: true,
destroy: true, // or 'custom'
})
```

Expand Down Expand Up @@ -149,7 +215,7 @@ Now, import and use the `vue-use-state-effect` composable.

import { useStateEffect } from 'vue-use-state-effect'

/* you composable logic */
/* your composable logic */

export const useSharedState: any = useStateEffect(sharedState, {
name: 'sharedState',
Expand All @@ -161,7 +227,7 @@ export const useSharedState: any = useStateEffect(sharedState, {
OK, great. Let's use it along with some page / component. Create one e.g. `home.vue`.

```vue
<!-- Home Page | home.vue -->
<!-- Home Page | Home.vue -->
<template>
<div>{{ test }}</div>
Expand All @@ -180,10 +246,10 @@ const test = computed(() => state.value.test) // '🚀 Initial state value.',
</script>
```

Please note that we're using `<script setup>` notation here, you can find more about it in [this article](https://itnext.io/vue-3-script-setup-afb42a53462a). Right, what you can see here is that we're importing our newly shared composable with `state` data. With the `state` we have the `updateState` method, that it will update the state. Name of the parent object (`sharedState`) was defined within the configuration. Now you can create new page / component and read saved or updated state along with the different context. Like this.
Please note that we're using `<script setup>` notation here, you can find more about it in [this article](https://itnext.io/vue-3-script-setup-afb42a53462a). Right, what you can see here is that we're importing our newly shared composable with `state` data. With the `state` we have the `updateState` method, that will update the state - of course. Name of the parent object (`sharedState`) was defined within the configuration. Now you can create new page / component and read saved or updated state along with the different context. Like this.

```vue
<!-- New Page | new.vue -->
<!-- New Page | New.vue -->
<template>
<div>{{ test }}</div>
Expand All @@ -196,6 +262,8 @@ import { useSharedState } from '@composables/useSharedState'
const {
sharedState: { state },
} = useSharedState()
/* you can use watch here as well */
const test = ref(state.value.test) // '🌝 Updated state value.',
</script>
```
Expand Down
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Closing: #[issue number]
* [ ] issue connected
* [ ] summary
* [ ] self review
* [ ] issue todos

**Summary**:
---
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ sw.*
lib
.husky/_

package
vue-use-state-effect/
8 changes: 4 additions & 4 deletions BACKLOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
### Missing features, things to do:

- [ ] multi-state
- [ ] threads (on top of `id`)
- [ ] destroying destination
- [ ] state reset
- [ ] cached multi-state / threads
- [ ] docs on Nuxt Content
- [ ] Q&A section

### Done:

- [x] unit tests - delivered with `0.1.0`
- [x] destroying destination - `0.1.2`
- [x] unit tests - `0.1.0`
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 0.1.2 (09.06.2022)
* [**feature**] destroy destination ([#3](https://github.com/lukasborawski/vue-use-state-effect/pull/3))
* Nuxt demo update ([c38644d](https://github.com/lukasborawski/vue-use-state-effect/pull/3/commits/c38644d56babbf3c674f81742c94b74518d7a429))
* docs update

### 0.1.1 (24.05.2022)
* fixed typings ([96fb9ce](https://github.com/lukasborawski/vue-use-state-effect/commit/96fb9ce2bcf29bc8048ca2e99e5f0cd8493b4f43))
* Nuxt demo update ([2a078b8](https://github.com/lukasborawski/vue-use-state-effect/commit/2a078b898c36abbb439f180f1fd853cac704f847))
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Vue Use State Effect

**CAUTION**: Built and tested for/with **Vue 3** and/or **Nuxt 3**.
**CAUTION**: Built and tested for/with **Vue 3** and/or **Nuxt 3** (RC-2).

Fast and small library, built on top of the native `scopeEffect` **Vue 3 API** that will provide safe and sharable (across the app) state for your local composables and functions. It might be a good replacement for **Vuex** or **Pinia** state management, if you need smaller and less extensive solution.

Expand All @@ -12,6 +12,8 @@ Fast and small library, built on top of the native `scopeEffect` **Vue 3 API** t

You can read all about the technical background and all the details in this [article](https://itnext.io/vue-use-state-effect-14f81a6c8d62).

Configuration (docs) and examples can be found [here](https://github.com/lukasborawski/vue-use-state-effect).

### Install

---
Expand All @@ -37,19 +39,26 @@ const composable = () => {
/* your composable logic here */
}

export const useSharedComposable = useStateEffect(composable)
export const useSharedComposable = useStateEffect(composable, { ...config })
```

Interface (**TypeScript**).

```typescript
interface UseStateEffectConfig {
readonly name?: string | null
readonly destroy?: boolean
readonly destroy?: boolean | 'custom'
readonly debug?: boolean
}
export type UseStateEffectOptions<T = any> = {
destroyLabels: string[]
props: ExtractPropTypes<{ stateEffectDestroyLabel: string } | T>
}

export function useStateEffect<T extends (...args: any[]) => any>(composable: T, config?: UseStateEffectConfig): () => {
export function useStateEffect<T extends (...args: any[]) => ReturnType<T>>(
composable: T,
config?: UseStateEffectConfig,
): (opts?: UseStateEffectOptions<opts.props>) => {
[keyof in string | 'state']: ReturnType<T>
}
```
Expand Down
38 changes: 30 additions & 8 deletions __tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ const config = {
name: 'sharedState',
instance: true,
}
const instance = {
uid: 1,
type: {
props: {},
},
}

jest.mock('vue', () => ({
ref: jest.requireActual('vue').ref,
getCurrentInstance: config.instance
? jest.fn(() => {
return { instance: true }
})
: null,
getCurrentInstance: config.instance ? jest.fn(() => instance) : null,
getCurrentScope: jest.requireActual('vue').getCurrentScope,
effectScope: jest.requireActual('vue').effectScope,
// lint-disable-next-line
onBeforeUnmount: jest.fn((fn) => fn()),
onBeforeMount: jest.fn((fn) => fn()),
}))

describe('vue-use-state-effect', () => {
getCurrentInstance.mockImplementation(() => true)
global.console = { debug: jest.fn(), warn: jest.fn() }
getCurrentInstance.mockImplementation(() => instance)
global.console = { debug: jest.fn(), warn: jest.fn(), log: console.log }

const { name } = config
const stateMock = ref({ test: '🚀 Initial state value.' })
Expand All @@ -45,12 +48,17 @@ describe('vue-use-state-effect', () => {
class StateEffect {
constructor(state, signature) {
this._syg = signature || 'StateEffect'
this._uid = 1
Object.assign(this, state)
}
}

describe('was invoked without config and', () => {
const state = new StateEffect({ state: ref({ test: '🚀 Initial state value.' }) })

beforeEach(() => {
jest.clearAllMocks()
})
it('returns state', () => {
const receivedState = useStateEffect(composableMock)()
const expectState = { state }
Expand All @@ -71,6 +79,10 @@ describe('vue-use-state-effect', () => {

describe('was invoked with config and', () => {
const state = new StateEffect({ state: ref({ test: '🚀 Initial state value.' }) }, name)

beforeEach(() => {
jest.clearAllMocks()
})
it('has custom state object name', () => {
const receivedState = useStateEffect(composableMock, { name })()
const expectState = { sharedState: state }
Expand All @@ -85,9 +97,19 @@ describe('vue-use-state-effect', () => {
expect(receivedState).toEqual({ [name]: ref(null) })
expect(console.debug).toBeCalledTimes(2)
})
it('destroys state with custom settings', () => {
const receivedState = useStateEffect(composableMock, { name, destroy: 'custom', debug: true })
expect(receivedState({ destroyLabels: ['Test'], props: { stateEffectDestroyLabel: 'Test' } })).toEqual({
[name]: ref(null),
})
expect(console.debug).toBeCalledTimes(2)
})
})

describe('will fail when', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('composable is not a function', () => {
const receivedState = useStateEffect('test', { debug: true })
expect(receivedState).toStrictEqual(ref(null))
Expand All @@ -97,7 +119,7 @@ describe('vue-use-state-effect', () => {
getCurrentInstance.mockImplementation(() => false)
const receivedState = useStateEffect(composableMock, { debug: true })()
expect(receivedState).toStrictEqual(ref(null))
expect(console.warn).toBeCalledTimes(2)
expect(console.warn).toBeCalledTimes(1)
})
})
})
Loading

0 comments on commit f499954

Please sign in to comment.