Skip to content

Commit

Permalink
feat(reactify)!: enable support for reactive getter by default (#1860)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jul 12, 2022
1 parent a5192c4 commit d484f8f
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 69 deletions.
112 changes: 58 additions & 54 deletions packages/shared/reactify/index.test.ts
@@ -1,92 +1,96 @@
import { ref } from 'vue-demi'
import { useSetup } from '../../.test'
import { reactify } from '.'

describe('reactify', () => {
it('one arg', () => {
useSetup(() => {
const base = ref(1.5)
const floor = reactify(Math.floor)
const result = floor(base)
const base = ref(1.5)
const floor = reactify(Math.floor)
const result = floor(base)

expect(result.value).toBe(1)
expect(result.value).toBe(1)

base.value = 2.8
base.value = 2.8

expect(result.value).toBe(2)
})
expect(result.value).toBe(2)
})

it('two args', () => {
useSetup(() => {
const base = ref(0)
const exponent = ref(0)
const pow = reactify(Math.pow)
const result = pow(base, exponent)
const base = ref(0)
const exponent = ref(0)
const pow = reactify(Math.pow)
const result = pow(base, exponent)

expect(base.value).toBe(0)
expect(result.value).toBe(1)
expect(base.value).toBe(0)
expect(result.value).toBe(1)

base.value = 2
exponent.value = 2
base.value = 2
exponent.value = 2

expect(result.value).toBe(4)
expect(result.value).toBe(4)

base.value = 3
base.value = 3

expect(result.value).toBe(9)
expect(result.value).toBe(9)

exponent.value = 3
exponent.value = 3

expect(result.value).toBe(27)
})
expect(result.value).toBe(27)
})

it('mixed with literal', () => {
useSetup(() => {
const base = ref(0)
const pow = reactify(Math.pow)
const result = pow(base, 2)
const base = ref(0)
const pow = reactify(Math.pow)
const result = pow(base, 2)

expect(base.value).toBe(0)
expect(result.value).toBe(0)
expect(base.value).toBe(0)
expect(result.value).toBe(0)

base.value = 10
expect(result.value).toBe(100)
})
base.value = 10
expect(result.value).toBe(100)
})

it('JSON.stringify', () => {
useSetup(() => {
const base = ref<any>(0)
const stringify = reactify(JSON.stringify)
const result = stringify(base, null, 2)
const base = ref<any>(0)
const stringify = reactify(JSON.stringify)
const result = stringify(base, null, 2)

expect(base.value).toBe(0)
expect(result.value).toBe('0')
expect(base.value).toBe(0)
expect(result.value).toBe('0')

base.value = { foo: 'bar' }
expect(result.value).toBe('{\n "foo": "bar"\n}')
})
base.value = { foo: 'bar' }
expect(result.value).toBe('{\n "foo": "bar"\n}')
})

it('Pythagorean theorem', () => {
useSetup(() => {
const pow = reactify(Math.pow)
const sqrt = reactify(Math.sqrt)
const add = reactify((a: number, b: number) => a + b)
const pow = reactify(Math.pow)
const sqrt = reactify(Math.sqrt)
const add = reactify((a: number, b: number) => a + b)

const a = ref(3)
const b = ref(4)
const a = ref(3)
const b = ref(4)

const c = sqrt(add(pow(a, 2), pow(b, 2)))
const c = sqrt(add(pow(a, 2), pow(b, 2)))

expect(c.value).toBe(5)
expect(c.value).toBe(5)

a.value = 5
b.value = 12
a.value = 5
b.value = 12

expect(c.value).toBe(13)
})
expect(c.value).toBe(13)
})

it('computed getter', () => {
const add = reactify((a: number, b: number) => a + b)

const a = ref(3)
const b = ref(4)

const c = add(a, () => b.value)

expect(c.value).toBe(7)

b.value = 12

expect(c.value).toBe(15)
})
})
23 changes: 17 additions & 6 deletions packages/shared/reactify/index.ts
@@ -1,22 +1,33 @@
import type { ComputedRef } from 'vue-demi'
import { computed, unref } from 'vue-demi'
import type { MaybeRef } from '../utils'
import { resolveUnref } from '../resolveUnref'
import type { MaybeComputedRef, MaybeRef } from '../utils'

export type Reactify<T> = T extends (...args: infer A) => infer R
? (...args: { [K in keyof A]: MaybeRef<A[K]> }) => ComputedRef<R>
export type Reactified<T, Computed extends boolean> = T extends (...args: infer A) => infer R
? (...args: { [K in keyof A]: Computed extends true ? MaybeComputedRef<A[K]> : MaybeRef<A[K]> }) => ComputedRef<R>
: never

export interface ReactifyOptions<T extends boolean> {
/**
* Accept passing a function as a reactive getter
*
* @default true
*/
computedGetter?: T
}

/**
* Converts plain function into a reactive function.
* The converted function accepts refs as it's arguments
* and returns a ComputedRef, with proper typing.
*
* @param fn - Source function
*/
export function reactify<T extends Function>(fn: T): Reactify<T> {
export function reactify<T extends Function, K extends boolean = true>(fn: T, options?: ReactifyOptions<K>): Reactified<T, K> {
const unrefFn = options?.computedGetter === false ? unref : resolveUnref
return function (this: any, ...args: any[]) {
return computed(() => fn.apply(this, args.map(i => unref(i))))
} as Reactify<T>
return computed(() => fn.apply(this, args.map(i => unrefFn(i))))
} as any
}

// alias
Expand Down
22 changes: 13 additions & 9 deletions packages/shared/reactifyObject/index.ts
@@ -1,9 +1,9 @@
import type { Reactify } from '../reactify'
import type { Reactified, ReactifyOptions } from '../reactify'
import { reactify } from '../reactify'

export type ReactifyNested<T, Keys extends keyof T = keyof T> = { [K in Keys]: T[K] extends (...args: any[]) => any ? Reactify<T[K]> : T[K] }
export type ReactifyNested<T, Keys extends keyof T = keyof T, S extends boolean= true> = { [K in Keys]: T[K] extends (...args: any[]) => any ? Reactified<T[K], S> : T[K] }

export interface ReactifyObjectOptions {
export interface ReactifyObjectOptions<T extends boolean> extends ReactifyOptions<T> {
/**
* Includes names from Object.getOwnPropertyNames
*
Expand All @@ -15,13 +15,17 @@ export interface ReactifyObjectOptions {
/**
* Apply `reactify` to an object
*/
export function reactifyObject<T extends object, Keys extends keyof T>(obj: T, keys?: (keyof T)[]): ReactifyNested<T, Keys>
export function reactifyObject<T extends object>(obj: T, options?: ReactifyObjectOptions): ReactifyNested<T>
export function reactifyObject<T extends object, Keys extends keyof T>(obj: T, keys?: (keyof T)[]): ReactifyNested<T, Keys, true>
export function reactifyObject<T extends object, S extends boolean = true>(obj: T, options?: ReactifyObjectOptions<S>): ReactifyNested<T, keyof T, S>

export function reactifyObject<T extends object>(obj: T, optionsOrKeys: ReactifyObjectOptions | (keyof T)[] = {}): ReactifyNested<T> {
export function reactifyObject<T extends object, S extends boolean = true>(obj: T, optionsOrKeys: ReactifyObjectOptions<S> | (keyof T)[] = {}): ReactifyNested<T, keyof T, S> {
let keys: string[] = []
if (Array.isArray(optionsOrKeys)) { keys = optionsOrKeys as string[] }
let options: ReactifyOptions<S> | undefined
if (Array.isArray(optionsOrKeys)) {
keys = optionsOrKeys as string[]
}
else {
options = optionsOrKeys
const { includeOwnProperties = true } = optionsOrKeys

keys.push(...Object.keys(obj))
Expand All @@ -36,9 +40,9 @@ export function reactifyObject<T extends object>(obj: T, optionsOrKeys: Reactify
return [
key,
typeof value === 'function'
? reactify(value.bind(obj))
? reactify(value.bind(obj), options)
: value,
]
}),
) as ReactifyNested<T>
) as ReactifyNested<T, keyof T, S>
}

0 comments on commit d484f8f

Please sign in to comment.