diff --git a/package.json b/package.json index 0da486b..a53b7d3 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ }, "peerDependencies": { "@formkit/vue": ">= 1.2.0", - "@inertiajs/core": ">= 1.0.0", + "@inertiajs/vue3": ">= 1.0.0", "vue": ">= 3.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5996c6..b30f939 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ dependencies: '@formkit/vue': specifier: '>= 1.2.0' version: 1.2.2(typescript@5.2.2) - '@inertiajs/core': + '@inertiajs/vue3': specifier: '>= 1.0.0' - version: 1.0.13 + version: 1.0.14(vue@3.3.7) vue: specifier: '>= 3.0.0' version: 3.3.7(typescript@5.2.2) @@ -608,8 +608,8 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} dev: true - /@inertiajs/core@1.0.13: - resolution: {integrity: sha512-xPvogbRgAXbogP16EBnGduEmVtImdncEBNQF9etRFF5Ne2nrJafeFgM0FZHqsSqCDvA4jjk8b8Ezt6gZRPK9hg==} + /@inertiajs/core@1.0.14: + resolution: {integrity: sha512-S33PU6mWEYbn/s2Op+CJ6MN7ON354vWw8Y+UvtQzPt0r7pVgOuIArrqqsoulf9oQz9sbP1+vp/tCvyBzm4XmpA==} dependencies: axios: 1.6.0 deepmerge: 4.3.1 @@ -619,6 +619,19 @@ packages: - debug dev: false + /@inertiajs/vue3@1.0.14(vue@3.3.7): + resolution: {integrity: sha512-lKL3Bm9k95Gw1GAq4RxgjfwSMfklkeMbvEfzwmsEBsZ4BbbWwfpC/+KS+4O4faTjjijczvkDPhMKv4duzFxtGw==} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@inertiajs/core': 1.0.14 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + vue: 3.3.7(typescript@5.2.2) + transitivePeerDependencies: + - debug + dev: false + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2220,6 +2233,14 @@ packages: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: true + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false + /lodash.isfunction@3.0.9: resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} dev: true diff --git a/src/addons/formkit.ts b/src/addons/formkit.ts new file mode 100644 index 0000000..f54705e --- /dev/null +++ b/src/addons/formkit.ts @@ -0,0 +1,57 @@ +import type { FormKitNode } from '@formkit/core'; +import type { RequestPayload } from '@inertiajs/core'; +import type { AddonExtension } from '../inertia'; + +import { reactive, watchEffect } from 'vue'; +import { createMessage } from '@formkit/core'; + +export default (initialFields?: F) => { + const state = reactive({ + node: null as null | FormKitNode, + dirty: false as boolean | null, + errors: false as boolean | null, + valid: false as boolean | null, + }); + + return { + state, + + addon: ((on) => { + on('start', (_, node) => { + node.store.set(createMessage({ + key: 'loading', + visible: false, + value: true + })); + + if (node.props.submitBehavior !== 'live') node.props.disabled = true; + }); + + on('error', (errors, node) => { + node.setErrors(node.name in errors ? errors[node.name] : [], errors); + }); + + on('finish', (_, node) => { + node.store.remove('loading'); + + if (node.props.submitBehavior !== 'live') node.props.disabled = false; + }); + }) as AddonExtension, + plugin: (node: FormKitNode) => { + if (node.props.type !== 'form') return; + + state.node = node; + node.input(initialFields); + + node.on('created', () => { + watchEffect(() => { + state.dirty = node.context!.state.dirty; + state.valid = node.context!.state.valid; + state.errors = node.context!.state.errors; + }); + }); + + return false; + } + } +}; diff --git a/src/addons/index.ts b/src/addons/index.ts new file mode 100644 index 0000000..1ecd599 --- /dev/null +++ b/src/addons/index.ts @@ -0,0 +1,2 @@ +export { default as createFormkitAddon } from './formkit'; +export { default as createStateAddon } from './state'; diff --git a/src/addons/state.ts b/src/addons/state.ts new file mode 100644 index 0000000..3440ba5 --- /dev/null +++ b/src/addons/state.ts @@ -0,0 +1,51 @@ +import type { AddonExtension } from '../inertia'; + +import { reactive } from 'vue'; + +export default (recentlySuccessfulTimeoutTime = 2000) => { + let _recentlySuccessfulTimeoutId: ReturnType | undefined = undefined; + + const state = reactive({ + processing: false, + progress: 0, + recentlySuccessful: false, + wasSuccessful: false, + }); + + return { + state, + + addon: ((on) => { + on('before', () => { + state.processing = false; + state.progress = 0; + state.recentlySuccessful = false; + state.wasSuccessful = false; + + clearInterval(_recentlySuccessfulTimeoutId); + }); + + on('start', () => { + state.processing = true; + }); + + on('progress', (progress) => { + state.progress = progress?.percentage || 0; + }); + + on('success', () => { + state.recentlySuccessful = true; + state.wasSuccessful = true; + + _recentlySuccessfulTimeoutId = setTimeout(() => { + state.recentlySuccessful = false; + }, recentlySuccessfulTimeoutTime); + }); + + on('finish', () => { + state.processing = false; + state.progress = 0; + }); + }) as AddonExtension + }; +} diff --git a/src/event.ts b/src/event.ts deleted file mode 100644 index 4496ed5..0000000 --- a/src/event.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { GlobalEventsMap } from '@inertiajs/core'; - -export type EventCallback = { - [K in keyof Omit] - : (...args: [...GlobalEventsMap[K]['parameters'], ...A]) - => K extends 'success' | 'error' - ? Promise | GlobalEventsMap[K]['result'] - : GlobalEventsMap[K]['result']; -} & { - cancelToken: (...args: [{ cancel: () => void }, ...A]) => void; -}; - -export type OnFunction = ReturnType>['on']; -export type CombineFunction = ReturnType>['combine']; -export type ExecuteFunction = ReturnType>['execute']; - -export const createEventCallbackManager = () => { - const events: Partial<{ - [K in keyof EventCallback]: EventCallback[K][]; - }> = {}; - - const on = >(eventName: T, callback: EventCallback[T]) => { - if (typeof events[eventName] === 'undefined') events[eventName] = []; - - events[eventName]?.push(callback); - }; - - const combine = (combineCb: (cb: typeof on) => void | ((cb: typeof on) => void)[]) => { - if (Array.isArray(combineCb)) combineCb.forEach((cb) => cb(on)); - combineCb(on); - }; - - const execute = >(eventName: T, ...params: Parameters[T]>): ReturnType[T]> | undefined => { - const eventList = events[eventName]; - if (!eventList) return; - - if (eventName === 'before') { - for (const event of eventList) { - const res = event(...params); - - if (typeof res === 'boolean') return res as ReturnType[T]>; - } - } else if (['success', 'error'].includes(eventName)) { - let promiseResolver = Promise.resolve(); - - for (const event of eventList) { - const res = event(...params); - - if (res instanceof Promise) { - promiseResolver = res; - } else { - promiseResolver = Promise.resolve(res as void); - } - } - - return promiseResolver as ReturnType[T]>; - } else { - for (const event of eventList) { - event(...params); - } - } - }; - - return { - events, - on, - combine, - execute - }; -}; diff --git a/src/eventManager.ts b/src/eventManager.ts new file mode 100644 index 0000000..f95ade3 --- /dev/null +++ b/src/eventManager.ts @@ -0,0 +1,49 @@ +import type { FormKitNode } from '@formkit/core'; +import type { GlobalEventsMap } from '@inertiajs/core'; + +export type EventCallback = { + [K in keyof Omit] + : (...args: [...GlobalEventsMap[K]['parameters'], ...[node: FormKitNode]]) + => K extends 'success' | 'error' + ? Promise | GlobalEventsMap[K]['result'] + : GlobalEventsMap[K]['result']; +} & { + cancelToken: (...args: [{ cancel: () => void }, ...[node: FormKitNode]]) => void; +}; + +export const createEventManager = () => { + const events: Partial<{ + [K in keyof EventCallback]: EventCallback[K][]; + }> = {}; + + const on = (name: T, cb: EventCallback[T]) => { + if (typeof events[name] === 'undefined') events[name] = []; + + events[name]?.push(cb); + }; + + const run = (name: keyof EventCallback, ...args: any): Promise | void | boolean => { + let promiseResolver = Promise.resolve(); + + for (const event of events[name] || []) { + const res = event(...args); + + if (name === 'before' && typeof res === 'boolean') return res; + else if (name === 'success' || name === 'finish') { + if (res instanceof Promise) { + promiseResolver = res; + } else { + promiseResolver = Promise.resolve(res); + } + } + } + + if (name === 'success' || name === 'finish') return promiseResolver; + }; + + return { + events, + on, + run + } +} diff --git a/src/index.ts b/src/index.ts index a96aa5a..2e3e55f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1 @@ -export type { EventCallback, OnFunction, CombineFunction, ExecuteFunction } from './event'; -export { createEventCallbackManager } from './event'; - -export { useForm } from './inertia'; +export { useForm, type AddonExtension } from './inertia'; diff --git a/src/inertia.ts b/src/inertia.ts index cfe70d9..ddfbac9 100644 --- a/src/inertia.ts +++ b/src/inertia.ts @@ -1,157 +1,81 @@ import type { FormKitNode } from '@formkit/core'; import type { Method, VisitOptions, RequestPayload } from '@inertiajs/core'; -import { createMessage } from '@formkit/core'; -import { router } from '@inertiajs/core'; -import { reactive, toRefs, watchEffect } from 'vue'; -import { createEventCallbackManager } from './event'; +import { router } from '@inertiajs/vue3'; +import { toRefs } from 'vue'; +import { createEventManager } from './eventManager'; +import { createFormkitAddon, createStateAddon } from './addons/index'; + +export type AddonExtension = (on: ReturnType['on']) => void; export const useForm = (initialFields?: F) => { - const eventManager = createEventCallbackManager<[node: FormKitNode]>(); + const eventManager = createEventManager(); + const { state: addonState, addon: stateAddon } = createStateAddon(); + const { state: formkitState, addon: formkitAddon, plugin } = createFormkitAddon(initialFields); + + const addon = (addons: AddonExtension | AddonExtension[]) => { + if (Array.isArray(addons)) addons.forEach((cb) => cb(eventManager.on)); + else addons(eventManager.on); + }; + + addon(stateAddon); + addon(formkitAddon); - let _recentlySuccessfulTimeoutId: ReturnType | undefined = undefined; let _cancelToken: { cancel: () => void; } | undefined = undefined; - const state = reactive<{ - node: FormKitNode | null, - dirty: boolean | null; - errors: boolean | null; - processing: boolean; - progress: number; - recentlySuccessful: boolean; - valid: boolean | null; - wasSuccessful: boolean; - }>({ - node: null, - dirty: null, - errors: null, - processing: false, - progress: 0, - recentlySuccessful: false, - valid: null, - wasSuccessful: false - }); - - eventManager.combine((on) => { + addon((on) => { on('cancelToken', (token) => { _cancelToken = token; }); - on('before', () => { - state.progress = 0; - state.recentlySuccessful = false; - state.wasSuccessful = false; - - clearInterval(_recentlySuccessfulTimeoutId); - }); - - on('start', (_, node) => { - node.store.set(createMessage({ - key: 'loading', - visible: false, - value: true - })); - - if (node.props.submitBehavior !== 'live') node.props.disabled = true; - - state.processing = true; - }); - - on('progress', (axiosProgress) => { - state.progress = axiosProgress?.percentage || 0; - }); - - on('success', () => { - state.recentlySuccessful = true; - state.wasSuccessful = true; - - _recentlySuccessfulTimeoutId = setTimeout(() => { - state.recentlySuccessful = false; - }, 2000); - }); - - on('error', (errors, node) => { - node.setErrors(node.name in errors ? errors[node.name] : [], errors); - }); - - on('finish', (_, node) => { + on('finish', () => { _cancelToken = undefined; - - node.store.remove('loading'); - - if (node.props.submitBehavior !== 'live') node.props.disabled = false; - - state.processing = false; - state.progress = 0; }); }); - const plugin = (node: FormKitNode) => { - if (node.props.type !== 'form') return; - - state.node = node; - node.input(initialFields); - - node.on('created', () => { - if (!node.context) return; - - watchEffect(() => { - if (!node.context) return; - - state.dirty = node.context.state.dirty; - state.valid = node.context.state.valid; - state.errors = node.context.state.errors; - }); - }); - - return false; - }; - - const _createVisitHandler = (method: Method) => (url: URL | string, options?: Exclude) => (data: F, node: FormKitNode) => { - const _optionEventCallbacks: { - [key: string]: any - } = {}; - - const names = Object.keys(eventManager.events) as (keyof typeof eventManager.events)[]; + const submit = (method: Method, url: URL | string, options?: Exclude) => (data: F, node: FormKitNode) => { + const callbackEvents = (Object.keys(eventManager.events) as (keyof typeof eventManager.events)[]).map((name) => ({ + [`on${name.charAt(0).toUpperCase() + name.slice(1)}`]: (arg: any) => { + if (name === 'cancel') return eventManager.run(name, arg); - for (const name of names) { - const _callbackName = `on${name.charAt(0).toUpperCase() + name.slice(1)}`; - - _optionEventCallbacks[_callbackName] = (arg: any) => { - return eventManager.execute(name, arg, node); - }; - } + return eventManager.run(name, arg, node); + } + })).reduce((p, c) => ({ ...p, ...c }), {}); if (method === 'delete') { router.delete(url, { - ..._optionEventCallbacks, + ...callbackEvents, ...options, data }); } else { router[method](url, data, { - ..._optionEventCallbacks, + ...callbackEvents, ...options, }); } }; return { - get: _createVisitHandler('get'), - post: _createVisitHandler('post'), - put: _createVisitHandler('put'), - patch: _createVisitHandler('patch'), - delete: _createVisitHandler('delete'), + submit, + + get: (url: URL | string, options?: Exclude) => submit('get', url, options), + post: (url: URL | string, options?: Exclude) => submit('post', url, options), + put: (url: URL | string, options?: Exclude) => submit('put', url, options), + patch: (url: URL | string, options?: Exclude) => submit('patch', url, options), + delete: (url: URL | string, options?: Exclude) => submit('delete', url, options), + cancel: () => { if (_cancelToken) _cancelToken.cancel(); }, - ...toRefs(state), + ...toRefs(addonState), + ...toRefs(formkitState), on: eventManager.on, - combine: eventManager.combine, + addon, plugin, }