Skip to content

Commit 25de020

Browse files
committed
feat: adds the composable and event system for inertia integration
1 parent d4799a6 commit 25de020

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

src/event.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { VisitOptions, GlobalEventsMap } from '@inertiajs/core';
2+
3+
export type Events<A extends [...args: any]> = {
4+
[K in keyof Omit<GlobalEventsMap, 'navigate' | 'invalid' | 'exception'>]
5+
: (...args: [...GlobalEventsMap[K]['parameters'], ...A]) => GlobalEventsMap[K]['result'];
6+
} & {
7+
cancelToken: (...args: [{ cancel: () => void }, ...A]) => void;
8+
};
9+
10+
export type EventsList<A extends [...args: any]> = {
11+
[K in keyof Events<A>]: Events<A>[K][];
12+
};
13+
14+
export const useEventsSystem = <E extends [...args: any]>() => {
15+
const eventList: Partial<EventsList<E>> = {};
16+
17+
const on = <T extends keyof Events<E>>(eventName: T, callback: Events<E>[T]) => {
18+
if (typeof eventList[eventName] === 'undefined') eventList[eventName] = [];
19+
20+
eventList[eventName]?.push(callback);
21+
};
22+
23+
const combine = (combineCb: (cb: typeof on) => void | ((cb: typeof on) => void)[]) => {
24+
if (Array.isArray(combineCb)) combineCb.forEach((cb) => cb(on));
25+
combineCb(on);
26+
};
27+
28+
const execute = <T extends keyof Events<E>>(eventName: T, ...params: Parameters<Events<E>[T]>): ReturnType<Events<E>[T]> | undefined => {
29+
const events = eventList[eventName];
30+
if (!events) return;
31+
32+
if (eventName === 'before') {
33+
for (const event of events) {
34+
const res = event(...params);
35+
36+
if (typeof res === 'boolean') return res as ReturnType<Events<E>[T]>;
37+
}
38+
} else {
39+
for (const event of events) {
40+
event(...params);
41+
}
42+
}
43+
};
44+
45+
const toVisitOptions = (...params: E): VisitOptions => ((Object.keys(eventList) as (keyof typeof eventList)[]).map((name) => ({
46+
[`on${(name.charAt(0).toUpperCase() + name.slice(1)) as Capitalize<keyof typeof eventList>}`]: (arg: any) => {
47+
return execute(name, ...[arg, ...params]);
48+
}
49+
})).reduce((p, c) => ({
50+
...p,
51+
...c
52+
}), {}));
53+
54+
return {
55+
on,
56+
combine,
57+
execute,
58+
toVisitOptions
59+
};
60+
};

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type { Events, EventsList } from './event';
2+
export { useEventsSystem } from './event';
3+
4+
export { useForm } from './inertia';

src/inertia.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import type { FormKitNode } from '@formkit/core';
2+
import type { Method, VisitOptions, RequestPayload } from '@inertiajs/core';
3+
4+
import { createMessage } from '@formkit/core';
5+
import { router } from '@inertiajs/core';
6+
import { reactive, toRefs, watchEffect } from 'vue';
7+
import { useEventsSystem } from './event';
8+
9+
export const useForm = <F extends RequestPayload>(initialFields?: F) => {
10+
const event = useEventsSystem<[node: FormKitNode]>();
11+
12+
let _recentlySuccessfulTimeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
13+
let _cancelToken: {
14+
cancel: () => void;
15+
} | undefined = undefined;
16+
17+
const state = reactive<{
18+
node: FormKitNode | null,
19+
dirty: boolean | null;
20+
errors: boolean | null;
21+
processing: boolean;
22+
progress: number;
23+
recentlySuccessful: boolean;
24+
valid: boolean | null;
25+
wasSuccessful: boolean;
26+
}>({
27+
node: null,
28+
dirty: null,
29+
errors: null,
30+
processing: false,
31+
progress: 0,
32+
recentlySuccessful: false,
33+
valid: null,
34+
wasSuccessful: false
35+
});
36+
37+
event.combine((on) => {
38+
on('cancelToken', (token) => {
39+
_cancelToken = token;
40+
});
41+
42+
on('before', () => {
43+
state.progress = 0;
44+
state.recentlySuccessful = false;
45+
state.wasSuccessful = false;
46+
47+
clearInterval(_recentlySuccessfulTimeoutId);
48+
});
49+
50+
on('start', (_, node) => {
51+
node.store.set(createMessage({
52+
key: 'loading',
53+
visible: false,
54+
value: true
55+
}));
56+
57+
if (node.props.submitBehavior !== 'live') node.props.disabled = true;
58+
59+
state.processing = true;
60+
});
61+
62+
on('progress', (axiosProgress) => {
63+
state.progress = axiosProgress?.percentage || 0;
64+
});
65+
66+
on('success', () => {
67+
state.recentlySuccessful = true;
68+
state.wasSuccessful = true;
69+
70+
_recentlySuccessfulTimeoutId = setTimeout(() => {
71+
state.recentlySuccessful = false;
72+
}, 2000);
73+
});
74+
75+
on('error', (errors, node) => {
76+
node.setErrors(node.name in errors ? errors[node.name] : [], errors);
77+
});
78+
79+
on('finish', (_, node) => {
80+
_cancelToken = undefined;
81+
82+
node.store.remove('loading');
83+
84+
if (node.props.submitBehavior !== 'live') node.props.disabled = false;
85+
86+
state.processing = false;
87+
state.progress = 0;
88+
});
89+
});
90+
91+
const plugin = (node: FormKitNode) => {
92+
if (node.props.type !== 'form') return;
93+
94+
state.node = node;
95+
node.input(initialFields);
96+
97+
node.on('created', () => {
98+
if (!node.context) return;
99+
100+
watchEffect(() => {
101+
if (!node.context) return;
102+
103+
state.dirty = node.context.state.dirty;
104+
state.valid = node.context.state.valid;
105+
state.errors = node.context.state.errors;
106+
});
107+
});
108+
109+
return false;
110+
};
111+
112+
const _createVisitHandler = (method: Method) => (url: URL | string, options?: Exclude<VisitOptions, 'method' | 'data'>) => (data: F, node: FormKitNode) => {
113+
if (method === 'delete') {
114+
router.delete(url, {
115+
...event.toVisitOptions(node),
116+
...options,
117+
data
118+
});
119+
} else {
120+
router[method](url, data, {
121+
...event.toVisitOptions(node),
122+
...options,
123+
});
124+
}
125+
};
126+
127+
return {
128+
get: _createVisitHandler('get'),
129+
post: _createVisitHandler('post'),
130+
put: _createVisitHandler('put'),
131+
patch: _createVisitHandler('patch'),
132+
delete: _createVisitHandler('delete'),
133+
cancel: () => {
134+
if (_cancelToken) _cancelToken.cancel();
135+
},
136+
137+
...toRefs(state),
138+
139+
on: event.on,
140+
combine: event.combine,
141+
142+
plugin,
143+
}
144+
};

0 commit comments

Comments
 (0)