@capillarytech/cap-ui-utils 3.0.15 → 3.0.17-alpha.0
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/index.js +2 -0
- package/package.json +1 -1
- package/utils/mfeEventBus.js +47 -0
- package/utils/useMFEModuleHeader.js +133 -0
package/index.js
CHANGED
|
@@ -12,3 +12,5 @@ export { default as sanitizeTemplateWithRegexp } from "./utils/contentSanitizati
|
|
|
12
12
|
export { default as decompressJsonObject } from "./utils/zlibDataDecompress";
|
|
13
13
|
export { default as getEnvVariable } from "./utils/getEnvVariables";
|
|
14
14
|
export { formatDateWithTimezone, getTimezoneTooltip, hasTimezoneFeatureAccess } from "./utils/timezone";
|
|
15
|
+
export { default as MFEEventBus } from "./utils/mfeEventBus";
|
|
16
|
+
export { default as useMFEModuleHeader } from "./utils/useMFEModuleHeader";
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFEEventBus
|
|
3
|
+
*
|
|
4
|
+
* Lightweight event bus for cross-boundary communication between the MFE
|
|
5
|
+
* host shell and remote apps. Uses window CustomEvent — no build-time
|
|
6
|
+
* coupling between apps.
|
|
7
|
+
*
|
|
8
|
+
* All events are namespaced with the MFE_EVENT_PREFIX to avoid collisions
|
|
9
|
+
* with other window events (e.g. analytics, third-party libraries).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const MFE_EVENT_PREFIX = 'mfe:';
|
|
13
|
+
|
|
14
|
+
const MFEEventBus = {
|
|
15
|
+
/**
|
|
16
|
+
* Emit an event to all listeners (host or any remote).
|
|
17
|
+
*
|
|
18
|
+
* @param {string} eventName - Event name without prefix (e.g. 'loyalty:navigate')
|
|
19
|
+
* @param {object} payload - Arbitrary serialisable data
|
|
20
|
+
*/
|
|
21
|
+
emit(eventName, payload = {}) {
|
|
22
|
+
window.dispatchEvent(
|
|
23
|
+
new CustomEvent(`${MFE_EVENT_PREFIX}${eventName}`, {
|
|
24
|
+
detail: payload,
|
|
25
|
+
bubbles: false,
|
|
26
|
+
cancelable: false,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to an event. Returns an unsubscribe function — call it in
|
|
33
|
+
* useEffect cleanup to prevent memory leaks.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} eventName - Event name without prefix
|
|
36
|
+
* @param {function} handler - Called with the event payload object
|
|
37
|
+
* @returns {function} - Unsubscribe / cleanup function
|
|
38
|
+
*/
|
|
39
|
+
on(eventName, handler) {
|
|
40
|
+
const listener = event => handler(event.detail);
|
|
41
|
+
window.addEventListener(`${MFE_EVENT_PREFIX}${eventName}`, listener);
|
|
42
|
+
return () =>
|
|
43
|
+
window.removeEventListener(`${MFE_EVENT_PREFIX}${eventName}`, listener);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default MFEEventBus;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import MFEEventBus from './mfeEventBus';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Emit the module header for a given path. For route-based settings, the
|
|
6
|
+
* settings route shows the settings header (with a back button); everything
|
|
7
|
+
* else shows the main header. This runs on every navigation so both the MFE
|
|
8
|
+
* back button and the browser back button land on the correct header.
|
|
9
|
+
* No-op when settings is not route-based (header is registered once on mount).
|
|
10
|
+
*/
|
|
11
|
+
const emitHeaderForPath = (cfg, pathname) => {
|
|
12
|
+
const { header, settingsRoute, settingsHeader } = cfg;
|
|
13
|
+
if (!settingsRoute) return;
|
|
14
|
+
if (pathname.startsWith(settingsRoute)) {
|
|
15
|
+
MFEEventBus.emit('module:header-register', {
|
|
16
|
+
...header,
|
|
17
|
+
...settingsHeader,
|
|
18
|
+
showSettings: false,
|
|
19
|
+
showBackButton: true,
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
MFEEventBus.emit('module:header-register', header);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const emitRouteChange = pathname =>
|
|
27
|
+
MFEEventBus.emit('module:route-change', { pathname });
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* useMFEModuleHeader
|
|
31
|
+
*
|
|
32
|
+
* Common hook wrapping the MFEEventBus module-header wiring duplicated across
|
|
33
|
+
* every remote app. Registers the module header, keeps it in sync on
|
|
34
|
+
* navigation (including browser back), and handles the settings button — for
|
|
35
|
+
* both modal-style settings (callback) and route-based settings (own URL).
|
|
36
|
+
*
|
|
37
|
+
* Router-version agnostic: it never calls useLocation/useNavigate itself
|
|
38
|
+
* (those throw without the matching Router context and break the rules of
|
|
39
|
+
* hooks when called conditionally). The caller passes the router primitives:
|
|
40
|
+
* - React Router v5: pass `history` (history.location/.listen/.push/.goBack)
|
|
41
|
+
* - React Router v6: pass `location` (from useLocation) and `navigate`
|
|
42
|
+
* (from useNavigate)
|
|
43
|
+
* If both are passed, `history` (v5) takes precedence.
|
|
44
|
+
*
|
|
45
|
+
* Mount the hook in a component that stays rendered while the module is active
|
|
46
|
+
* AND is an ancestor of the header listener, so the listener subscribes before
|
|
47
|
+
* these emits fire — the bus has no replay for late subscribers.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} config
|
|
50
|
+
* @param {object} [config.history] React Router v5 history object
|
|
51
|
+
* @param {object} [config.location] React Router v6 location ({ pathname })
|
|
52
|
+
* @param {function} [config.navigate] React Router v6 navigate function
|
|
53
|
+
* @param {object} config.header { name, description, showBorder, showSettings }
|
|
54
|
+
* @param {string[]} [config.landingRoutes] Exact paths where the header is shown
|
|
55
|
+
* @param {function} [config.onSettingsClick] Modal settings handler (no route change)
|
|
56
|
+
* @param {string} [config.settingsRoute] Route-based settings URL (enables push/back)
|
|
57
|
+
* @param {object} [config.settingsHeader] { name, description } shown on settingsRoute
|
|
58
|
+
*/
|
|
59
|
+
export default function useMFEModuleHeader(config) {
|
|
60
|
+
const {
|
|
61
|
+
history,
|
|
62
|
+
location,
|
|
63
|
+
navigate,
|
|
64
|
+
header,
|
|
65
|
+
landingRoutes,
|
|
66
|
+
settingsRoute,
|
|
67
|
+
} = config;
|
|
68
|
+
|
|
69
|
+
const isRouteBased = Boolean(settingsRoute);
|
|
70
|
+
const showSettings = Boolean(header && header.showSettings);
|
|
71
|
+
|
|
72
|
+
// Latest config in a ref so callers can pass inline objects/handlers without
|
|
73
|
+
// memoizing: the long-lived listeners always read fresh values and never
|
|
74
|
+
// re-subscribe on identity changes (no stale closure, no churn).
|
|
75
|
+
const cfgRef = useRef(config);
|
|
76
|
+
cfgRef.current = config;
|
|
77
|
+
|
|
78
|
+
// Header register (non-route-based) + routes, once on mount.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isRouteBased) {
|
|
81
|
+
MFEEventBus.emit('module:header-register', header);
|
|
82
|
+
}
|
|
83
|
+
if (landingRoutes && landingRoutes.length) {
|
|
84
|
+
MFEEventBus.emit('module:header-routes', {
|
|
85
|
+
landingRoutes,
|
|
86
|
+
...(isRouteBased ? { settingsRoute } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return () => MFEEventBus.emit('module:header-clear', {});
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// v5 navigation: single history.listen drives both header + route-change.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!history) return undefined;
|
|
95
|
+
const { pathname } = history.location;
|
|
96
|
+
emitHeaderForPath(cfgRef.current, pathname);
|
|
97
|
+
emitRouteChange(pathname);
|
|
98
|
+
return history.listen(loc => {
|
|
99
|
+
emitHeaderForPath(cfgRef.current, loc.pathname);
|
|
100
|
+
emitRouteChange(loc.pathname);
|
|
101
|
+
});
|
|
102
|
+
}, [history]);
|
|
103
|
+
|
|
104
|
+
// v6 navigation: re-run on pathname change (skipped when v5 history is used).
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (history || !location) return undefined;
|
|
107
|
+
emitHeaderForPath(cfgRef.current, location.pathname);
|
|
108
|
+
emitRouteChange(location.pathname);
|
|
109
|
+
return undefined;
|
|
110
|
+
}, [history, location && location.pathname]);
|
|
111
|
+
|
|
112
|
+
// Settings button.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!showSettings) return undefined;
|
|
115
|
+
return MFEEventBus.on('module:settings-click', () => {
|
|
116
|
+
if (isRouteBased) {
|
|
117
|
+
if (history) history.push(settingsRoute);
|
|
118
|
+
else if (navigate) navigate(settingsRoute);
|
|
119
|
+
} else if (cfgRef.current.onSettingsClick) {
|
|
120
|
+
cfgRef.current.onSettingsClick();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}, [history, navigate, settingsRoute, showSettings]);
|
|
124
|
+
|
|
125
|
+
// Back button (route-based settings only).
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!isRouteBased) return undefined;
|
|
128
|
+
return MFEEventBus.on('module:back-click', () => {
|
|
129
|
+
if (history) history.goBack();
|
|
130
|
+
else if (navigate) navigate(-1);
|
|
131
|
+
});
|
|
132
|
+
}, [history, navigate, settingsRoute]);
|
|
133
|
+
}
|