Skip to content

Commit

Permalink
refact useTrackedInstance types
Browse files Browse the repository at this point in the history
  • Loading branch information
rudnik275 committed Feb 25, 2024
1 parent 5bb8be2 commit 57a8cb8
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 117 deletions.
14 changes: 7 additions & 7 deletions src/collection.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import {computed, shallowRef, triggerRef, ShallowRef, ComputedRef, ref, Ref} from 'vue'
import {computed, ComputedRef, ref, Ref, ShallowRef, shallowRef, triggerRef} from 'vue'
import {TrackedInstance, useTrackedInstance} from './tracked-instance'

export interface CollectionItem<Item extends Record<string, any>, Meta = Record<string, any>> {
export interface CollectionItem<Item, Meta> {
instance: TrackedInstance<Item>
meta: Meta
isRemoved: Ref<boolean>
isNew: Ref<boolean>
}

export interface Collection<Item extends Record<string, any>, Meta = Record<string, any>> {
export interface Collection<Item, Meta> {
items: ShallowRef<CollectionItem<Item, Meta>[]>
isDirty: ComputedRef<boolean>
add: (item: Partial<Item>, afterIndex?: number) => CollectionItem<Item, Meta>
add: (item: Item, afterIndex?: number) => CollectionItem<Item, Meta>
remove: (index: number, isHardRemove?: boolean) => void
loadData: (items: Item[]) => void
reset: () => void
}

export const useCollection = <Item extends Record<string, any>, Meta = Record<string, any>>(
createItemMeta: (instance: TrackedInstance<Item>) => Meta = () => ({}) as Meta
export const useCollection = <Item = any, Meta = any>(
createItemMeta: (instance: TrackedInstance<Item>) => Meta = () => undefined as Meta
): Collection<Item, Meta> => {
const items = shallowRef<CollectionItem<Item, Meta>[]>([])

const isDirty = computed(() =>
items.value.some(({instance, isRemoved, isNew}) => instance.isDirty.value || isNew.value || isRemoved.value)
)

const add = (item: Partial<Item>, index: number = items.value.length) => {
const add = (item: Item, index: number = items.value.length) => {
const instance = useTrackedInstance<Item>(item)
const newItem = {
isRemoved: ref(false),
Expand Down
123 changes: 13 additions & 110 deletions src/tracked-instance.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,15 @@
import {get, has, set, unset} from 'lodash-es'
import {computed, customRef, Ref} from 'vue'
import {computed, Ref} from 'vue'
import {createNestedRef, DeepPartial, isEmpty, isObject, iterateObject, NestedProxyPathItem} from './utils'

type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T

export interface TrackedInstance<Data extends Record<string, any>> {
export interface TrackedInstance<Data> {
data: Ref<Data>
isDirty: Ref<boolean>
changedData: Ref<DeepPartial<Data>>
loadData: (newData: DeepPartial<Data>) => void
loadData: (newData: Data) => void
reset: () => void
}

interface NestedProxyPathItem {
target: Record<string, any>
property: string
receiver?: Record<string, any>
}

const isObject = (value: unknown) =>
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Date) &&
!(value instanceof File) &&
!(value instanceof Map) &&
!(value instanceof Set)

const isEmpty = (value: object) => Object.keys(value).length === 0

const iterateObject = function* (
source: Record<string, any>,
params: {
// define condition when need to go deep
goDeepCondition?: (path: string[], value: any) => boolean
// include parent into separate step when we go deep
includeParent?: boolean
} = {}
) {
const {goDeepCondition = (_, value) => isObject(value), includeParent = false} = params
const iterateObjectDeep = function* (path: string[], obj: Record<string, any>): Generator<[string[], any]> {
for (const [key, value] of Object.entries(obj)) {
const currentPath = path.concat(key)
if (goDeepCondition(currentPath, value)) {
if (includeParent) {
yield [currentPath, value]
}
yield* iterateObjectDeep(currentPath, value)
} else {
yield [currentPath, value]
}
}
}

yield* iterateObjectDeep([], source)
}

const createNestedRef = <Source extends Record<string, any>>(
source: Source,
handler: (path: NestedProxyPathItem[]) => ProxyHandler<Source>
) =>
customRef<Source>((track, trigger) => {
// make nested objects and arrays is reactive
const createProxy = <InnerSource extends Record<string, any>>(
source: InnerSource,
path: NestedProxyPathItem[] = []
): InnerSource => {
const currentProxyHandler = handler(path) as unknown as ProxyHandler<InnerSource>
return new Proxy(source, {
...currentProxyHandler,
get(target, property: string, receiver) {
track()
const result = currentProxyHandler.get
? currentProxyHandler.get(target, property, receiver)
: Reflect.get(target, property, receiver)

if (isObject(result) || Array.isArray(result)) {
return createProxy(result, path.concat({target, property, receiver}))
}
return result
},
set(target, property, value, receiver) {
const result = currentProxyHandler.set
? currentProxyHandler.set(target, property, value, receiver)
: Reflect.set(target, property, value, receiver)
trigger()
return result
},
deleteProperty(target, property) {
const result = currentProxyHandler.deleteProperty
? currentProxyHandler.deleteProperty(target, property)
: Reflect.deleteProperty(target, property)
trigger()
return result
}
} as ProxyHandler<InnerSource>)
}

let value = createProxy(source)

return {
get() {
track()
return value
},
set(newValue: Source) {
value = createProxy(newValue)
trigger()
}
}
})

// array values in originalData should store in default object to avoid removing items on change length
class ArrayInOriginalData {
length: number
Expand Down Expand Up @@ -207,9 +107,12 @@ const snapshotValueToOriginalData = (
}
}

export const useTrackedInstance = <Data extends Record<string, any>>(
initialData: Partial<Data>
): TrackedInstance<Data> => {
export function useTrackedInstance<Data = any>(): TrackedInstance<Data | undefined>
export function useTrackedInstance<Data>(value: Data): TrackedInstance<Data>

export function useTrackedInstance<Data>(
initialData?: Data
): TrackedInstance<Data> {
type InternalData = { root: Data }
const _originalData = createNestedRef<DeepPartial<InternalData>>({}, (path) => ({
deleteProperty(target, property) {
Expand All @@ -236,7 +139,7 @@ export const useTrackedInstance = <Data extends Record<string, any>>(
path.map((i) => i.property)
) as ArrayInOriginalData | undefined

const {length: originalDataLength} = originalDataValue || oldValue
const {length: originalDataLength} = originalDataValue || oldValue as any[]

if (value < originalDataLength) {
// when removed new value
Expand All @@ -261,7 +164,7 @@ export const useTrackedInstance = <Data extends Record<string, any>>(

return Reflect.set(target, property, value, receiver)
},
deleteProperty(target, property: keyof typeof target) {
deleteProperty(target, property) {
setOriginalDataValue(_originalData.value, parentThree.concat({target, property} as NestedProxyPathItem))
return Reflect.deleteProperty(target, property)
}
Expand Down Expand Up @@ -299,7 +202,7 @@ export const useTrackedInstance = <Data extends Record<string, any>>(

const changedData = computed(() => _changedData.value.root as DeepPartial<Data>)

const loadData = (newData: DeepPartial<Data>) => {
const loadData = (newData: Data) => {
_data.value = {root: newData} as InternalData
_originalData.value = {}
}
Expand Down
102 changes: 102 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {customRef} from 'vue'

export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T

export interface NestedProxyPathItem {
target: Record<string, any>
property: string
receiver?: Record<string, any>
}

export const isObject = (value: unknown) =>
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Date) &&
!(value instanceof File) &&
!(value instanceof Map) &&
!(value instanceof Set)

export const isEmpty = (value: object) => Object.keys(value).length === 0

export const iterateObject = function* (
source: Record<string, any>,
params: {
// define condition when need to go deep
goDeepCondition?: (path: string[], value: any) => boolean
// include parent into separate step when we go deep
includeParent?: boolean
} = {}
) {
const {goDeepCondition = (_, value) => isObject(value), includeParent = false} = params
const iterateObjectDeep = function* (path: string[], obj: Record<string, any>): Generator<[string[], any]> {
for (const [key, value] of Object.entries(obj)) {
const currentPath = path.concat(key)
if (goDeepCondition(currentPath, value)) {
if (includeParent) {
yield [currentPath, value]
}
yield* iterateObjectDeep(currentPath, value)
} else {
yield [currentPath, value]
}
}
}

yield* iterateObjectDeep([], source)
}

export const createNestedRef = <Source extends Record<string, any>>(
source: Source,
handler: <InnerSource extends Record<string, any>>(path: NestedProxyPathItem[]) => ProxyHandler<InnerSource>
) =>
customRef<Source>((track, trigger) => {
// make nested objects and arrays is reactive
const createProxy = <InnerSource extends Record<string, any>>(
source: InnerSource,
path: NestedProxyPathItem[] = []
): InnerSource => {
const currentProxyHandler = handler(path) as unknown as ProxyHandler<InnerSource>
return new Proxy(source, {
...currentProxyHandler,
get(target, property: string, receiver) {
track()
const result = currentProxyHandler.get
? currentProxyHandler.get(target, property, receiver)
: Reflect.get(target, property, receiver)

if (isObject(result) || Array.isArray(result)) {
return createProxy(result, path.concat({target, property, receiver}))
}
return result
},
set(target, property, value, receiver) {
const result = currentProxyHandler.set
? currentProxyHandler.set(target, property, value, receiver)
: Reflect.set(target, property, value, receiver)
trigger()
return result
},
deleteProperty(target, property) {
const result = currentProxyHandler.deleteProperty
? currentProxyHandler.deleteProperty(target, property)
: Reflect.deleteProperty(target, property)
trigger()
return result
}
} as ProxyHandler<InnerSource>)
}

let value = createProxy(source)

return {
get() {
track()
return value
},
set(newValue: Source) {
value = createProxy(newValue)
trigger()
}
}
})

0 comments on commit 57a8cb8

Please sign in to comment.