Skip to content

Commit

Permalink
feat(form): add lazy value definition feature to data
Browse files Browse the repository at this point in the history
  • Loading branch information
kiaking committed Oct 27, 2022
1 parent dbf096d commit 77a09e6
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 10 deletions.
108 changes: 108 additions & 0 deletions lib/composables/Data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { watchOnce } from '@vueuse/core'
import cloneDeep from 'lodash-es/cloneDeep'
import { WatchSource, reactive } from 'vue'
import { isObject } from '../support/Utils'

export interface Data<T extends Record<string, any>> {
state: T
init(): void
}

export type DataWithDef = Record<string, any>

export interface Def<T = any> {
__isDef: true
value: any
source: WatchSource<T>
cb: (value: Exclude<T, undefined>) => void
}

export type UseDataInput<
T extends Record<string, any>
> = T | ((utils: UseDataInputUtils) => DataWithDef)

export interface UseDataInputUtils {
def<T>(
value: any,
source: WatchSource<T>,
cb: (value: Exclude<T, undefined>) => void
): Def<T>
}

export function useData<T extends Record<string, any>>(
data: UseDataInput<T>
): Data<T> {
const { state, defs } = createState(data)

const initialState = cloneDeep(state)
const reactiveState = reactive(state)

handleDefs(defs, reactiveState)

function init(): void {
Object.assign(reactiveState, initialState)
}

return {
state: reactiveState,
init
}
}

function createState<T extends Record<string, any>>(
data: UseDataInput<T>
): { state: T; defs: [string, Def][] } {
if (typeof data !== 'function') {
return { state: data, defs: [] }
}

const dataWithDef = data({ def })

const state = {} as T
const defs = [] as [string, Def][]

for (const key in dataWithDef) {
const maybeDef = dataWithDef[key]

if (!isDef(maybeDef)) {
(state as any)[key] = maybeDef
continue
}

(state as any)[key] = maybeDef.value

defs.push([key, maybeDef])
}

return {
state,
defs
}
}

function handleDefs<T extends Record<string, any>>(
defs: [string, Def][], state: T
): void {
defs.forEach(([key, def]) => {
watchOnce(def.source, (value: any) => {
(state as any)[key] = def.cb(value)
})
})
}

function def<T>(
value: any,
source: WatchSource<T>,
cb: (value: Exclude<T, undefined>) => void
): Def {
return {
__isDef: true,
value,
source,
cb
}
}

function isDef(value: any): boolean {
return isObject(value) ? !!value.__isDef : false
}
18 changes: 8 additions & 10 deletions lib/composables/Form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cloneDeep from 'lodash-es/cloneDeep'
import { Ref, computed, reactive } from 'vue'
import { Ref, computed } from 'vue'
import { useSnackbars } from '../stores/Snackbars'
import { UseDataInput, useData } from './Data'
import { Validation, useValidation } from './Validation'

export interface Form<T extends Record<string, any>> {
Expand All @@ -13,7 +13,7 @@ export interface Form<T extends Record<string, any>> {
}

export interface UseFormOptions<T extends Record<string, any>> {
data: T
data: UseDataInput<T>
rules?: Record<string, any> | ((state: T) => Record<string, any>)
}

Expand All @@ -22,20 +22,18 @@ export function useForm<
>(options: UseFormOptions<T>): Form<T> {
const snackbars = useSnackbars()

const initialData = cloneDeep(options.data)

const data = reactive(options.data)
const data = useData(options.data)

const rules = computed(() => {
return options.rules
? typeof options.rules === 'function' ? options.rules(data) : options.rules
? typeof options.rules === 'function' ? options.rules(data.state) : options.rules
: {}
})

const validation = useValidation(data, rules)
const validation = useValidation(data.state, rules)

function init(): void {
Object.assign(data, initialData)
data.init()
reset()
}

Expand All @@ -61,7 +59,7 @@ export function useForm<
}

return {
data,
data: data.state,
init,
reset,
validation,
Expand Down
4 changes: 4 additions & 0 deletions lib/support/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export function isNumber(value: unknown): value is number {
export function isArray(value: unknown): value is unknown[] {
return Array.isArray(value)
}

export function isObject(value: unknown): value is Record<string, any> {
return typeof value === 'object' && value !== null && !isArray(value)
}
12 changes: 12 additions & 0 deletions tests/support/Utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,16 @@ describe('support/Utils', () => {
expect(Utils.isArray([])).toEqual(true)
})
})

describe('isObject', () => {
test('check if the given value is object', () => {
expect(Utils.isObject(undefined)).toEqual(false)
expect(Utils.isObject(null)).toEqual(false)
expect(Utils.isObject(1)).toEqual(false)
expect(Utils.isObject('a')).toEqual(false)
expect(Utils.isObject(true)).toEqual(false)
expect(Utils.isObject([])).toEqual(false)
expect(Utils.isObject({})).toEqual(true)
})
})
})

0 comments on commit 77a09e6

Please sign in to comment.