@violice/rmu 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -36
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +13 -28
- package/dist/index.mjs +2 -204
- package/dist/index.mjs.map +1 -0
- package/package.json +27 -19
- package/src/constants.ts +8 -0
- package/src/emitter.test.ts +85 -0
- package/src/emitter.ts +24 -16
- package/src/events.test.ts +59 -0
- package/src/events.ts +14 -13
- package/src/integration.test.tsx +266 -0
- package/src/rmu-outlet.test.tsx +213 -0
- package/src/rmu-outlet.tsx +3 -2
- package/src/rmu-provider.test.tsx +15 -0
- package/src/types.ts +20 -16
- package/src/use-rmu-events.test.ts +117 -0
- package/src/use-rmu-events.ts +5 -5
- package/src/use-rmu-state.test.ts +129 -0
- package/dist/index.d.ts +0 -49
- package/dist/index.js +0 -231
package/dist/index.mjs
CHANGED
|
@@ -1,204 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
//#region src/rmu-context.ts
|
|
4
|
-
const RMUContext = createContext(null);
|
|
5
|
-
|
|
6
|
-
//#endregion
|
|
7
|
-
//#region src/emitter.ts
|
|
8
|
-
const listenersByType = {};
|
|
9
|
-
const emitter = {
|
|
10
|
-
subscribe(type, handler) {
|
|
11
|
-
if (!listenersByType[type]) listenersByType[type] = /* @__PURE__ */ new Set();
|
|
12
|
-
listenersByType[type].add(handler);
|
|
13
|
-
},
|
|
14
|
-
unsubscribe(type, handler) {
|
|
15
|
-
const listeners = listenersByType[type];
|
|
16
|
-
if (listeners) {
|
|
17
|
-
listeners.delete(handler);
|
|
18
|
-
if (listeners.size === 0) delete listenersByType[type];
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
emit(type, payload) {
|
|
22
|
-
const listeners = listenersByType[type];
|
|
23
|
-
if (!listeners) return;
|
|
24
|
-
[...listeners].forEach((handler) => {
|
|
25
|
-
try {
|
|
26
|
-
handler(payload);
|
|
27
|
-
} catch {}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
//#endregion
|
|
33
|
-
//#region src/events.ts
|
|
34
|
-
const RMU_EVENTS = {
|
|
35
|
-
open: "rmu:open-modal",
|
|
36
|
-
close: "rmu:close-modal"
|
|
37
|
-
};
|
|
38
|
-
const openModal = (modalComponent, config = {}) => {
|
|
39
|
-
const modalId = `rmu-modal-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
|
|
40
|
-
const { outletId = "rmu-default-outlet" } = config;
|
|
41
|
-
emitter.emit(RMU_EVENTS.open, {
|
|
42
|
-
modalId,
|
|
43
|
-
modalComponent,
|
|
44
|
-
outletId
|
|
45
|
-
});
|
|
46
|
-
return {
|
|
47
|
-
modalId,
|
|
48
|
-
outletId
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
const closeModal = ({ modalId, outletId }) => {
|
|
52
|
-
emitter.emit(RMU_EVENTS.close, {
|
|
53
|
-
modalId,
|
|
54
|
-
outletId
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
//#endregion
|
|
59
|
-
//#region src/use-rmu-events.ts
|
|
60
|
-
const useRMUEvents = (ctx) => {
|
|
61
|
-
const events = {
|
|
62
|
-
open: (payload) => ctx.openModal(payload),
|
|
63
|
-
close: (payload) => ctx.closeModal(payload)
|
|
64
|
-
};
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
Object.keys(events).forEach((event) => {
|
|
67
|
-
emitter.subscribe(RMU_EVENTS[event], events[event]);
|
|
68
|
-
});
|
|
69
|
-
return () => {
|
|
70
|
-
Object.keys(events).forEach((event) => {
|
|
71
|
-
emitter.unsubscribe(RMU_EVENTS[event], events[event]);
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
}, []);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/use-rmu-state.ts
|
|
79
|
-
const ACTIONS = {
|
|
80
|
-
openModal: "rmu:open-modal",
|
|
81
|
-
closeModal: "rmu:close-modal",
|
|
82
|
-
addOutlet: "rmu:add-modal",
|
|
83
|
-
removeOutlet: "rmu:remove-outlet"
|
|
84
|
-
};
|
|
85
|
-
const reducer = (state, action) => {
|
|
86
|
-
switch (action.type) {
|
|
87
|
-
case ACTIONS.openModal: {
|
|
88
|
-
const { modalId, modalComponent, outletId } = action.payload;
|
|
89
|
-
const modalOutlet = state.outlets[outletId];
|
|
90
|
-
if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
|
|
91
|
-
return {
|
|
92
|
-
...state,
|
|
93
|
-
outlets: {
|
|
94
|
-
...state.outlets,
|
|
95
|
-
[outletId]: {
|
|
96
|
-
...modalOutlet,
|
|
97
|
-
[modalId]: modalComponent
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
case ACTIONS.closeModal: {
|
|
103
|
-
const { modalId, outletId } = action.payload;
|
|
104
|
-
const modalOutlet = state.outlets[outletId];
|
|
105
|
-
if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
|
|
106
|
-
const { [modalId]: _removed,...restModals } = modalOutlet;
|
|
107
|
-
return {
|
|
108
|
-
...state,
|
|
109
|
-
outlets: {
|
|
110
|
-
...state.outlets,
|
|
111
|
-
[outletId]: restModals
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
case ACTIONS.addOutlet: {
|
|
116
|
-
const { outletId } = action.payload;
|
|
117
|
-
if (!!state.outlets[outletId]) throw new Error(`Outlet with id ${outletId} already exists`);
|
|
118
|
-
return {
|
|
119
|
-
...state,
|
|
120
|
-
outlets: {
|
|
121
|
-
...state.outlets,
|
|
122
|
-
[outletId]: {}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
case ACTIONS.removeOutlet: {
|
|
127
|
-
const { outletId } = action.payload;
|
|
128
|
-
const { [outletId]: _removed,...restOutlets } = state.outlets;
|
|
129
|
-
return {
|
|
130
|
-
...state,
|
|
131
|
-
outlets: restOutlets
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
default: return state;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
const useRMUState = () => {
|
|
138
|
-
const [state, dispatch] = useReducer(reducer, { outlets: {} });
|
|
139
|
-
const openModal$1 = ({ modalId, modalComponent, outletId }) => {
|
|
140
|
-
dispatch({
|
|
141
|
-
type: ACTIONS.openModal,
|
|
142
|
-
payload: {
|
|
143
|
-
modalId,
|
|
144
|
-
modalComponent,
|
|
145
|
-
outletId
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
};
|
|
149
|
-
const closeModal$1 = ({ modalId, outletId }) => {
|
|
150
|
-
dispatch({
|
|
151
|
-
type: ACTIONS.closeModal,
|
|
152
|
-
payload: {
|
|
153
|
-
modalId,
|
|
154
|
-
outletId
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
};
|
|
158
|
-
const addOutlet = (outletId) => {
|
|
159
|
-
dispatch({
|
|
160
|
-
type: ACTIONS.addOutlet,
|
|
161
|
-
payload: { outletId }
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
const removeOutlet = (outletId) => {
|
|
165
|
-
dispatch({
|
|
166
|
-
type: ACTIONS.removeOutlet,
|
|
167
|
-
payload: { outletId }
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
return {
|
|
171
|
-
...state,
|
|
172
|
-
openModal: openModal$1,
|
|
173
|
-
closeModal: closeModal$1,
|
|
174
|
-
addOutlet,
|
|
175
|
-
removeOutlet
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
//#endregion
|
|
180
|
-
//#region src/rmu-provider.tsx
|
|
181
|
-
const RMUProvider = ({ children }) => {
|
|
182
|
-
const state = useRMUState();
|
|
183
|
-
useRMUEvents(state);
|
|
184
|
-
return /* @__PURE__ */ React.createElement(RMUContext.Provider, { value: state }, children);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
//#endregion
|
|
188
|
-
//#region src/rmu-outlet.tsx
|
|
189
|
-
const RMUOutlet = ({ outletId = "rmu-default-outlet" }) => {
|
|
190
|
-
const ctx = useContext(RMUContext);
|
|
191
|
-
if (!ctx) throw new Error("RMUProvider not found in component tree");
|
|
192
|
-
const { outlets, addOutlet, removeOutlet } = ctx;
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
addOutlet(outletId);
|
|
195
|
-
return () => {
|
|
196
|
-
removeOutlet(outletId);
|
|
197
|
-
};
|
|
198
|
-
}, []);
|
|
199
|
-
const modals = outlets[outletId] ?? {};
|
|
200
|
-
return /* @__PURE__ */ React.createElement(React.Fragment, null, Object.entries(modals).map(([modalId, modalComponent]) => /* @__PURE__ */ React.createElement(Fragment, { key: modalId }, modalComponent)));
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
//#endregion
|
|
204
|
-
export { RMUOutlet, RMUProvider, closeModal, openModal };
|
|
1
|
+
import{Fragment as e,createContext as t,useContext as n,useEffect as r,useReducer as i}from"react";import{Fragment as a,jsx as o}from"react/jsx-runtime";const s=t(null);let c=function(e){return e.OpenModal=`RMU:OPEN_MODAL`,e.CloseModal=`RMU:CLOSE_MODAL`,e}({});const l=`RMU:DEFAULT_OUTLET`,u={open:c.OpenModal,close:c.CloseModal},d={},f={subscribe(e,t){d[e]||(d[e]=new Set),d[e].add(t)},unsubscribe(e,t){let n=d[e];n&&(n.delete(t),n.size===0&&delete d[e])},emit(e,t){let n=d[e];n&&[...n].forEach(e=>{try{e(t)}catch{}})},_clear(){Object.keys(d).forEach(e=>{delete d[e]})}},p=e=>{let t={open:t=>e.openModal(t),close:t=>e.closeModal(t)};r(()=>(Object.keys(t).forEach(e=>{f.subscribe(u[e],t[e])}),()=>{Object.keys(t).forEach(e=>{f.unsubscribe(u[e],t[e])})}),[])},m={openModal:`rmu:open-modal`,closeModal:`rmu:close-modal`,addOutlet:`rmu:add-modal`,removeOutlet:`rmu:remove-outlet`},h=(e,t)=>{switch(t.type){case m.openModal:{let{modalId:n,modalComponent:r,outletId:i}=t.payload,a=e.outlets[i];if(!a)throw Error(`Outlet with id ${i} not found`);return{...e,outlets:{...e.outlets,[i]:{...a,[n]:r}}}}case m.closeModal:{let{modalId:n,outletId:r}=t.payload,i=e.outlets[r];if(!i)throw Error(`Outlet with id ${r} not found`);let{[n]:a,...o}=i;return{...e,outlets:{...e.outlets,[r]:o}}}case m.addOutlet:{let{outletId:n}=t.payload;if(e.outlets[n])throw Error(`Outlet with id ${n} already exists`);return{...e,outlets:{...e.outlets,[n]:{}}}}case m.removeOutlet:{let{outletId:n}=t.payload,{[n]:r,...i}=e.outlets;return{...e,outlets:i}}default:return e}},g=()=>{let[e,t]=i(h,{outlets:{}}),n=({modalId:e,modalComponent:n,outletId:r})=>{t({type:m.openModal,payload:{modalId:e,modalComponent:n,outletId:r}})},r=({modalId:e,outletId:n})=>{t({type:m.closeModal,payload:{modalId:e,outletId:n}})},a=e=>{t({type:m.addOutlet,payload:{outletId:e}})},o=e=>{t({type:m.removeOutlet,payload:{outletId:e}})};return{...e,openModal:n,closeModal:r,addOutlet:a,removeOutlet:o}},_=({children:e})=>{let t=g();return p(t),o(s.Provider,{value:t,children:e})},v=({outletId:t=l})=>{let i=n(s);if(!i)throw Error(`RMUProvider not found in component tree`);let{outlets:c,addOutlet:u,removeOutlet:d}=i;r(()=>(u(t),()=>{d(t)}),[]);let f=c[t]??{};return o(a,{children:Object.entries(f).map(([t,n])=>o(e,{children:n},t))})},y=(e,t={})=>{let n=`rmu-modal-${new Date().getTime().toString()}`,{outletId:r=l}=t,i={modalId:n,modalComponent:e,outletId:r};return f.emit(c.OpenModal,i),{modalId:n,outletId:r}},b=({modalId:e,outletId:t})=>{let n={modalId:e,outletId:t};f.emit(c.CloseModal,n)};export{v as RMUOutlet,_ as RMUProvider,b as closeModal,y as openModal};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/rmu-context.ts","../src/types.ts","../src/constants.ts","../src/emitter.ts","../src/use-rmu-events.ts","../src/use-rmu-state.ts","../src/rmu-provider.tsx","../src/rmu-outlet.tsx","../src/events.ts"],"sourcesContent":["import { createContext } from 'react';\nimport { RMUContextState } from './types';\n\nexport const RMUContext = createContext<RMUContextState | null>(null);\n","import { ReactNode } from 'react';\n\nexport enum RMUEventType {\n OpenModal = 'RMU:OPEN_MODAL',\n CloseModal = 'RMU:CLOSE_MODAL',\n}\n\nexport type RMUContextState = {\n outlets: Record<string, Record<string, ReactNode>>;\n openModal: (payload: OpenModalPayload) => void;\n closeModal: (payload: CloseModalPayload) => void;\n addOutlet: (outletId: string) => void;\n removeOutlet: (outletId: string) => void;\n};\n\nexport type OpenModalPayload = {\n modalId: string;\n modalComponent: ReactNode;\n outletId: string;\n};\n\nexport type CloseModalPayload = {\n modalId: string;\n outletId: string;\n};\n\nexport type RMUEventPayload = OpenModalPayload | CloseModalPayload;\n","import { RMUEventType } from \"./types\";\n\nexport const RMU_DEFAULT_OUTLET_ID = 'RMU:DEFAULT_OUTLET' as const;\n\nexport const RMU_EVENTS = {\n open: RMUEventType.OpenModal,\n close: RMUEventType.CloseModal,\n} as const;\n","export type EventHandler<T = unknown> = (payload: T) => void;\n\ntype EventListenersMap = Record<string, Set<EventHandler<unknown>>>;\n\nconst listenersByType: EventListenersMap = {};\n\nexport const emitter = {\n subscribe<T>(type: string, handler: EventHandler<T>) {\n if (!listenersByType[type]) {\n listenersByType[type] = new Set<EventHandler<unknown>>();\n }\n listenersByType[type].add(handler as EventHandler<unknown>);\n },\n\n unsubscribe<T>(type: string, handler: EventHandler<T>) {\n const listeners = listenersByType[type];\n if (listeners) {\n listeners.delete(handler as EventHandler<unknown>);\n if (listeners.size === 0) {\n delete listenersByType[type];\n }\n }\n },\n\n emit<T>(type: string, payload: T) {\n const listeners = listenersByType[type];\n if (listeners) {\n // Clone to avoid issues if handlers mutate subscription during emit\n [...listeners].forEach((handler) => {\n try {\n handler(payload);\n } catch {\n // Swallow to ensure one handler error does not break others\n }\n });\n }\n },\n\n // For testing purposes only\n _clear() {\n Object.keys(listenersByType).forEach((key) => {\n delete listenersByType[key];\n });\n },\n};\n","import { useEffect } from 'react';\nimport { RMUContextState, OpenModalPayload, CloseModalPayload } from './types';\nimport { RMU_EVENTS } from './constants';\nimport { emitter } from './emitter';\n\nexport const useRMUEvents = (ctx: RMUContextState) => {\n const events = {\n open: (payload: OpenModalPayload) => ctx.openModal(payload),\n close: (payload: CloseModalPayload) => ctx.closeModal(payload),\n };\n\n useEffect(() => {\n (Object.keys(events) as (keyof typeof events)[]).forEach(event => {\n emitter.subscribe(RMU_EVENTS[event], events[event]);\n });\n\n return () => {\n (Object.keys(events) as (keyof typeof events)[]).forEach(event => {\n emitter.unsubscribe(RMU_EVENTS[event], events[event]);\n });\n };\n }, []);\n};\n","import { useReducer } from 'react';\nimport { RMUContextState } from './types';\n\nconst ACTIONS = {\n openModal: 'rmu:open-modal',\n closeModal: 'rmu:close-modal',\n addOutlet: 'rmu:add-modal',\n removeOutlet: 'rmu:remove-outlet',\n} as const;\n\nconst reducer = (\n state: {\n outlets: RMUContextState['outlets'];\n },\n action: {\n type: string;\n payload: Record<string, any>;\n }\n) => {\n switch (action.type) {\n case ACTIONS.openModal: {\n const { modalId, modalComponent, outletId } = action.payload;\n const modalOutlet = state.outlets[outletId];\n\n if (!modalOutlet) {\n throw new Error(`Outlet with id ${outletId} not found`);\n }\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: {\n ...modalOutlet,\n [modalId]: modalComponent,\n },\n },\n };\n }\n case ACTIONS.closeModal: {\n const { modalId, outletId } = action.payload;\n\n const modalOutlet = state.outlets[outletId];\n if (!modalOutlet) {\n throw new Error(`Outlet with id ${outletId} not found`);\n }\n\n const { [modalId]: _removed, ...restModals } = modalOutlet;\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: restModals,\n },\n };\n }\n case ACTIONS.addOutlet: {\n const { outletId } = action.payload;\n\n if (!!state.outlets[outletId]) {\n throw new Error(`Outlet with id ${outletId} already exists`);\n }\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: {},\n },\n };\n }\n case ACTIONS.removeOutlet: {\n const { outletId } = action.payload;\n\n const { [outletId]: _removed, ...restOutlets } = state.outlets;\n\n return {\n ...state,\n outlets: restOutlets,\n };\n }\n default:\n return state;\n }\n};\n\nexport const useRMUState = () => {\n const [state, dispatch] = useReducer(reducer, {\n outlets: {},\n });\n\n const openModal: RMUContextState['openModal'] = ({\n modalId,\n modalComponent,\n outletId,\n }) => {\n dispatch({\n type: ACTIONS.openModal,\n payload: { modalId, modalComponent, outletId },\n });\n };\n\n const closeModal: RMUContextState['closeModal'] = ({ modalId, outletId }) => {\n dispatch({ type: ACTIONS.closeModal, payload: { modalId, outletId } });\n };\n\n const addOutlet: RMUContextState['addOutlet'] = outletId => {\n dispatch({ type: ACTIONS.addOutlet, payload: { outletId } });\n };\n\n const removeOutlet: RMUContextState['removeOutlet'] = outletId => {\n dispatch({ type: ACTIONS.removeOutlet, payload: { outletId } });\n };\n\n return {\n ...state,\n openModal,\n closeModal,\n addOutlet,\n removeOutlet,\n };\n};\n","import React from 'react';\nimport { RMUContext } from './rmu-context';\nimport { useRMUEvents } from './use-rmu-events';\nimport { useRMUState } from './use-rmu-state';\n\nexport const RMUProvider = ({ children }: { children: React.ReactNode }) => {\n const state = useRMUState();\n\n useRMUEvents(state);\n\n return <RMUContext.Provider value={state}>{children}</RMUContext.Provider>;\n};\n","import React, { Fragment, useContext, useEffect } from 'react';\nimport { RMUContext } from './rmu-context';\nimport { RMU_DEFAULT_OUTLET_ID } from './constants';\n\nexport const RMUOutlet = ({ outletId = RMU_DEFAULT_OUTLET_ID }) => {\n const ctx = useContext(RMUContext);\n\n if (!ctx) {\n throw new Error('RMUProvider not found in component tree');\n }\n\n const { outlets, addOutlet, removeOutlet } = ctx;\n\n useEffect(() => {\n addOutlet(outletId);\n return () => {\n removeOutlet(outletId);\n };\n }, []);\n\n const modals = outlets[outletId] ?? {};\n\n return (\n <>\n {Object.entries(modals).map(([modalId, modalComponent]) => (\n <Fragment key={modalId}>{modalComponent}</Fragment>\n ))}\n </>\n );\n};\n\nexport default RMUOutlet;\n","import { ReactNode } from 'react';\nimport { OpenModalPayload, CloseModalPayload, RMUEventType } from './types';\nimport { emitter } from './emitter';\nimport { RMU_DEFAULT_OUTLET_ID } from './constants';\n\nexport const openModal = (\n modalComponent: ReactNode,\n config: { outletId?: string } = {}\n): { modalId: string; outletId: string } => {\n const modalId = `rmu-modal-${new Date().getTime().toString()}`;\n const { outletId = RMU_DEFAULT_OUTLET_ID } = config;\n const payload: OpenModalPayload = {\n modalId,\n modalComponent,\n outletId,\n };\n emitter.emit<OpenModalPayload>(RMUEventType.OpenModal, payload);\n return { modalId, outletId };\n};\n\nexport const closeModal = ({\n modalId,\n outletId,\n}: {\n modalId: string;\n outletId: string;\n}): void => {\n const payload: CloseModalPayload = { modalId, outletId };\n emitter.emit<CloseModalPayload>(RMUEventType.CloseModal, payload);\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@violice/rmu",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"author": "Sergey Ivashko",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"files": [
|
|
@@ -8,18 +9,20 @@
|
|
|
8
9
|
"src"
|
|
9
10
|
],
|
|
10
11
|
"engines": {
|
|
11
|
-
"node": ">=
|
|
12
|
+
"node": ">=24"
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
|
-
"
|
|
15
|
+
"dev": "tsdown --watch",
|
|
15
16
|
"build": "tsdown",
|
|
16
|
-
"prepare": "tsdown",
|
|
17
17
|
"size": "size-limit",
|
|
18
|
-
"
|
|
18
|
+
"size:why": "size-limit --why",
|
|
19
|
+
"test": "vitest --watch=false",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:coverage": "vitest run --coverage"
|
|
19
22
|
},
|
|
20
23
|
"peerDependencies": {
|
|
21
|
-
"react": ">=
|
|
22
|
-
"react-native": ">=0.
|
|
24
|
+
"react": ">=18",
|
|
25
|
+
"react-native": ">=0.69"
|
|
23
26
|
},
|
|
24
27
|
"peerDependenciesMeta": {
|
|
25
28
|
"react-native": {
|
|
@@ -32,21 +35,21 @@
|
|
|
32
35
|
"singleQuote": true,
|
|
33
36
|
"trailingComma": "es5"
|
|
34
37
|
},
|
|
35
|
-
"main": "dist/index.
|
|
38
|
+
"main": "dist/index.cjs",
|
|
36
39
|
"module": "dist/index.mjs",
|
|
37
40
|
"types": "dist/index.d.ts",
|
|
38
|
-
"react-native": "dist/index.
|
|
41
|
+
"react-native": "dist/index.cjs",
|
|
39
42
|
"exports": {
|
|
40
43
|
".": {
|
|
41
44
|
"types": "./dist/index.d.ts",
|
|
42
45
|
"import": "./dist/index.mjs",
|
|
43
|
-
"require": "./dist/index.
|
|
46
|
+
"require": "./dist/index.cjs"
|
|
44
47
|
}
|
|
45
48
|
},
|
|
46
49
|
"sideEffects": false,
|
|
47
50
|
"size-limit": [
|
|
48
51
|
{
|
|
49
|
-
"path": "dist/index.
|
|
52
|
+
"path": "dist/index.cjs",
|
|
50
53
|
"limit": "2 KB"
|
|
51
54
|
},
|
|
52
55
|
{
|
|
@@ -55,14 +58,19 @@
|
|
|
55
58
|
}
|
|
56
59
|
],
|
|
57
60
|
"devDependencies": {
|
|
58
|
-
"@size-limit/preset-small-lib": "^
|
|
59
|
-
"@
|
|
60
|
-
"@
|
|
61
|
-
"react": "^
|
|
62
|
-
"react-dom": "^
|
|
63
|
-
"
|
|
64
|
-
"
|
|
61
|
+
"@size-limit/preset-small-lib": "^12.0.1",
|
|
62
|
+
"@testing-library/dom": "^10.4.1",
|
|
63
|
+
"@testing-library/react": "^16.3.2",
|
|
64
|
+
"@types/react": "^19.2.14",
|
|
65
|
+
"@types/react-dom": "^19.2.3",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
67
|
+
"jsdom": "^26.0.0",
|
|
68
|
+
"react": "^19.2.5",
|
|
69
|
+
"react-dom": "^19.2.5",
|
|
70
|
+
"size-limit": "^12.0.1",
|
|
71
|
+
"tsdown": "^0.21.8",
|
|
65
72
|
"tslib": "^2.8.1",
|
|
66
|
-
"typescript": "^
|
|
73
|
+
"typescript": "^6.0.2",
|
|
74
|
+
"vitest": "^4.1.4"
|
|
67
75
|
}
|
|
68
76
|
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { emitter } from './emitter';
|
|
3
|
+
|
|
4
|
+
describe('emitter', () => {
|
|
5
|
+
it('should subscribe and emit events', () => {
|
|
6
|
+
const handler = vi.fn();
|
|
7
|
+
emitter.subscribe('test', handler);
|
|
8
|
+
emitter.emit('test', 'payload');
|
|
9
|
+
expect(handler).toHaveBeenCalledWith('payload');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should unsubscribe from events', () => {
|
|
13
|
+
const handler = vi.fn();
|
|
14
|
+
emitter.subscribe('test', handler);
|
|
15
|
+
emitter.unsubscribe('test', handler);
|
|
16
|
+
emitter.emit('test', 'payload');
|
|
17
|
+
expect(handler).not.toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should handle multiple handlers', () => {
|
|
21
|
+
const handler1 = vi.fn();
|
|
22
|
+
const handler2 = vi.fn();
|
|
23
|
+
emitter.subscribe('test', handler1);
|
|
24
|
+
emitter.subscribe('test', handler2);
|
|
25
|
+
emitter.emit('test', 'data');
|
|
26
|
+
expect(handler1).toHaveBeenCalled();
|
|
27
|
+
expect(handler2).toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should not break when emitting to non-existent event', () => {
|
|
31
|
+
expect(() => emitter.emit('non-existent', 'data')).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should pass payload to handlers', () => {
|
|
35
|
+
const handler = vi.fn();
|
|
36
|
+
const payload = { id: 1, name: 'test' };
|
|
37
|
+
emitter.subscribe('test', handler);
|
|
38
|
+
emitter.emit('test', payload);
|
|
39
|
+
expect(handler).toHaveBeenCalledWith(payload);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should delete event type when all handlers are unsubscribed', () => {
|
|
43
|
+
const handler1 = vi.fn();
|
|
44
|
+
const handler2 = vi.fn();
|
|
45
|
+
|
|
46
|
+
// Subscribe multiple handlers
|
|
47
|
+
emitter.subscribe('cleanup-test', handler1);
|
|
48
|
+
emitter.subscribe('cleanup-test', handler2);
|
|
49
|
+
|
|
50
|
+
// Emit should work
|
|
51
|
+
emitter.emit('cleanup-test', 'data');
|
|
52
|
+
expect(handler1).toHaveBeenCalled();
|
|
53
|
+
expect(handler2).toHaveBeenCalled();
|
|
54
|
+
|
|
55
|
+
// Unsubscribe first handler - event type should still exist
|
|
56
|
+
emitter.unsubscribe('cleanup-test', handler1);
|
|
57
|
+
emitter.emit('cleanup-test', 'data2');
|
|
58
|
+
expect(handler2).toHaveBeenCalledTimes(2);
|
|
59
|
+
|
|
60
|
+
// Unsubscribe second handler - event type should be deleted
|
|
61
|
+
emitter.unsubscribe('cleanup-test', handler2);
|
|
62
|
+
|
|
63
|
+
// Emit should not throw and handlers should not be called
|
|
64
|
+
expect(() => emitter.emit('cleanup-test', 'data3')).not.toThrow();
|
|
65
|
+
expect(handler1).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(handler2).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle errors in handlers without breaking other handlers', () => {
|
|
70
|
+
const errorHandler = vi.fn(() => {
|
|
71
|
+
throw new Error('Handler error');
|
|
72
|
+
});
|
|
73
|
+
const normalHandler = vi.fn();
|
|
74
|
+
|
|
75
|
+
emitter.subscribe('error-test', errorHandler);
|
|
76
|
+
emitter.subscribe('error-test', normalHandler);
|
|
77
|
+
|
|
78
|
+
// Emit should not throw even if one handler throws
|
|
79
|
+
expect(() => emitter.emit('error-test', 'data')).not.toThrow();
|
|
80
|
+
|
|
81
|
+
// Both handlers should have been called
|
|
82
|
+
expect(errorHandler).toHaveBeenCalled();
|
|
83
|
+
expect(normalHandler).toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/emitter.ts
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
|
-
type EventHandler = (payload:
|
|
1
|
+
export type EventHandler<T = unknown> = (payload: T) => void;
|
|
2
2
|
|
|
3
|
-
type EventListenersMap = Record<string, Set<EventHandler
|
|
3
|
+
type EventListenersMap = Record<string, Set<EventHandler<unknown>>>;
|
|
4
4
|
|
|
5
5
|
const listenersByType: EventListenersMap = {};
|
|
6
6
|
|
|
7
7
|
export const emitter = {
|
|
8
|
-
subscribe(type: string, handler: EventHandler) {
|
|
8
|
+
subscribe<T>(type: string, handler: EventHandler<T>) {
|
|
9
9
|
if (!listenersByType[type]) {
|
|
10
|
-
listenersByType[type] = new Set<EventHandler
|
|
10
|
+
listenersByType[type] = new Set<EventHandler<unknown>>();
|
|
11
11
|
}
|
|
12
|
-
listenersByType[type].add(handler);
|
|
12
|
+
listenersByType[type].add(handler as EventHandler<unknown>);
|
|
13
13
|
},
|
|
14
14
|
|
|
15
|
-
unsubscribe(type: string, handler: EventHandler) {
|
|
15
|
+
unsubscribe<T>(type: string, handler: EventHandler<T>) {
|
|
16
16
|
const listeners = listenersByType[type];
|
|
17
17
|
if (listeners) {
|
|
18
|
-
listeners.delete(handler);
|
|
18
|
+
listeners.delete(handler as EventHandler<unknown>);
|
|
19
19
|
if (listeners.size === 0) {
|
|
20
20
|
delete listenersByType[type];
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
|
|
25
|
-
emit(type: string, payload:
|
|
25
|
+
emit<T>(type: string, payload: T) {
|
|
26
26
|
const listeners = listenersByType[type];
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
if (listeners) {
|
|
28
|
+
// Clone to avoid issues if handlers mutate subscription during emit
|
|
29
|
+
[...listeners].forEach((handler) => {
|
|
30
|
+
try {
|
|
31
|
+
handler(payload);
|
|
32
|
+
} catch {
|
|
33
|
+
// Swallow to ensure one handler error does not break others
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// For testing purposes only
|
|
40
|
+
_clear() {
|
|
41
|
+
Object.keys(listenersByType).forEach((key) => {
|
|
42
|
+
delete listenersByType[key];
|
|
35
43
|
});
|
|
36
44
|
},
|
|
37
45
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { openModal, closeModal } from './events';
|
|
4
|
+
import { RMU_EVENTS, RMU_DEFAULT_OUTLET_ID } from './constants';
|
|
5
|
+
import { emitter } from './emitter';
|
|
6
|
+
|
|
7
|
+
describe('events', () => {
|
|
8
|
+
const mockComponent = React.createElement('div', null, 'Test Modal');
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('openModal', () => {
|
|
15
|
+
it('should emit open event with default outlet', () => {
|
|
16
|
+
const emitSpy = vi.spyOn(emitter, 'emit');
|
|
17
|
+
const result = openModal(mockComponent);
|
|
18
|
+
|
|
19
|
+
expect(emitSpy).toHaveBeenCalledWith(RMU_EVENTS.open, {
|
|
20
|
+
modalId: expect.stringMatching(/^rmu-modal-\d+$/),
|
|
21
|
+
modalComponent: mockComponent,
|
|
22
|
+
outletId: RMU_DEFAULT_OUTLET_ID,
|
|
23
|
+
});
|
|
24
|
+
expect(result).toHaveProperty('modalId');
|
|
25
|
+
expect(result).toHaveProperty('outletId', RMU_DEFAULT_OUTLET_ID);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should emit open event with custom outlet', () => {
|
|
29
|
+
const emitSpy = vi.spyOn(emitter, 'emit');
|
|
30
|
+
const result = openModal(mockComponent, { outletId: 'custom-outlet' });
|
|
31
|
+
|
|
32
|
+
expect(emitSpy).toHaveBeenCalledWith(RMU_EVENTS.open, {
|
|
33
|
+
modalId: expect.stringMatching(/^rmu-modal-\d+$/),
|
|
34
|
+
modalComponent: mockComponent,
|
|
35
|
+
outletId: 'custom-outlet',
|
|
36
|
+
});
|
|
37
|
+
expect(result.outletId).toBe('custom-outlet');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should generate unique modal IDs', async () => {
|
|
41
|
+
const result1 = openModal(mockComponent);
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
43
|
+
const result2 = openModal(mockComponent);
|
|
44
|
+
expect(result1.modalId).not.toBe(result2.modalId);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('closeModal', () => {
|
|
49
|
+
it('should emit close event', () => {
|
|
50
|
+
const emitSpy = vi.spyOn(emitter, 'emit');
|
|
51
|
+
closeModal({ modalId: 'test-modal', outletId: 'test-outlet' });
|
|
52
|
+
|
|
53
|
+
expect(emitSpy).toHaveBeenCalledWith(RMU_EVENTS.close, {
|
|
54
|
+
modalId: 'test-modal',
|
|
55
|
+
outletId: 'test-outlet',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/events.ts
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { OpenModalPayload, CloseModalPayload, RMUEventType } from './types';
|
|
3
3
|
import { emitter } from './emitter';
|
|
4
|
-
|
|
5
|
-
export const RMU_EVENTS = {
|
|
6
|
-
open: 'rmu:open-modal',
|
|
7
|
-
close: 'rmu:close-modal',
|
|
8
|
-
} as const;
|
|
4
|
+
import { RMU_DEFAULT_OUTLET_ID } from './constants';
|
|
9
5
|
|
|
10
6
|
export const openModal = (
|
|
11
7
|
modalComponent: ReactNode,
|
|
12
8
|
config: { outletId?: string } = {}
|
|
13
|
-
) => {
|
|
9
|
+
): { modalId: string; outletId: string } => {
|
|
14
10
|
const modalId = `rmu-modal-${new Date().getTime().toString()}`;
|
|
15
|
-
const { outletId =
|
|
16
|
-
|
|
11
|
+
const { outletId = RMU_DEFAULT_OUTLET_ID } = config;
|
|
12
|
+
const payload: OpenModalPayload = {
|
|
17
13
|
modalId,
|
|
18
14
|
modalComponent,
|
|
19
15
|
outletId,
|
|
20
|
-
}
|
|
16
|
+
};
|
|
17
|
+
emitter.emit<OpenModalPayload>(RMUEventType.OpenModal, payload);
|
|
21
18
|
return { modalId, outletId };
|
|
22
19
|
};
|
|
23
20
|
|
|
24
|
-
export const closeModal
|
|
21
|
+
export const closeModal = ({
|
|
25
22
|
modalId,
|
|
26
23
|
outletId,
|
|
27
|
-
}
|
|
28
|
-
|
|
24
|
+
}: {
|
|
25
|
+
modalId: string;
|
|
26
|
+
outletId: string;
|
|
27
|
+
}): void => {
|
|
28
|
+
const payload: CloseModalPayload = { modalId, outletId };
|
|
29
|
+
emitter.emit<CloseModalPayload>(RMUEventType.CloseModal, payload);
|
|
29
30
|
};
|