main
js 123 lines 3.5 KB
Raw
1 // Track all created stores
2 const stores = new Map();
3
4 /**
5 * Creates a store that can be used to share state between components.
6 * Uses initial state object and returns a proxy to it that uses Alpine when initialized
7 * @template T
8 * @param {string} name
9 * @param {T} initialState
10 * @returns {T}
11 */
12 export function createStore(name, initialState) {
13 const proxy = new Proxy(initialState, {
14 set(target, prop, value) {
15 const store = globalThis.Alpine?.store(name);
16 if (store) store[prop] = value;
17 else target[prop] = value;
18 return true;
19 },
20 get(target, prop) {
21 const store = globalThis.Alpine?.store(name);
22 if (store) return store[prop];
23 return target[prop];
24 }
25 });
26
27 if (globalThis.Alpine) {
28 globalThis.Alpine.store(name, initialState);
29 } else {
30 document.addEventListener("alpine:init", () => Alpine.store(name, initialState));
31 }
32
33 // Store the proxy
34 stores.set(name, proxy);
35
36 return /** @type {T} */ (proxy); // explicitly cast for linter support
37 }
38
39 /**
40 * Get an existing store by name
41 * @template T
42 * @param {string} name
43 * @returns {T | undefined}
44 */
45 export function getStore(name) {
46 return /** @type {T | undefined} */ (stores.get(name));
47 }
48
49 /**
50 * Save current state of a store into a plain object, with optional include/exclude filters.
51 * If exclude (blacklist) is provided and non-empty, everything except excluded keys is saved.
52 * Otherwise, if include (whitelist) is provided and non-empty, only included keys are saved.
53 * If both are empty, all own enumerable properties are saved.
54 * @param {object} store
55 * @param {string[]} [include]
56 * @param {string[]} [exclude]
57 * @returns {object}
58 */
59 export function saveState(store, include = [], exclude = []) {
60 const hasExclude = Array.isArray(exclude) && exclude.length > 0;
61 const hasInclude = !hasExclude && Array.isArray(include) && include.length > 0;
62
63 /** @type {Record<string, any>} */
64 const snapshot = {};
65
66 for (const key of Object.keys(store)) {
67 if (hasExclude) {
68 if (exclude.includes(key)) continue;
69 } else if (hasInclude) {
70 if (!include.includes(key)) continue;
71 }
72
73 const value = store[key];
74 if (typeof value === "function") continue;
75
76 if (Array.isArray(value)) {
77 snapshot[key] = value.map((item) =>
78 typeof item === "object" && item !== null ? { ...item } : item
79 );
80 } else if (typeof value === "object" && value !== null) {
81 snapshot[key] = { ...value };
82 } else {
83 snapshot[key] = value;
84 }
85 }
86
87 return snapshot;
88 }
89
90 /**
91 * Load a previously saved state object back into a store, honoring include/exclude filters.
92 * Filtering rules are the same as in saveState.
93 * @param {object} store
94 * @param {object} state
95 * @param {string[]} [include]
96 * @param {string[]} [exclude]
97 */
98 export function loadState(store, state, include = [], exclude = []) {
99 if (!state) return;
100
101 const hasExclude = Array.isArray(exclude) && exclude.length > 0;
102 const hasInclude = !hasExclude && Array.isArray(include) && include.length > 0;
103
104 for (const key of Object.keys(state)) {
105 if (hasExclude) {
106 if (exclude.includes(key)) continue;
107 } else if (hasInclude) {
108 if (!include.includes(key)) continue;
109 }
110
111 const value = state[key];
112
113 if (Array.isArray(value)) {
114 store[key] = value.map((item) =>
115 typeof item === "object" && item !== null ? { ...item } : item
116 );
117 } else if (typeof value === "object" && value !== null) {
118 store[key] = { ...value };
119 } else {
120 store[key] = value;
121 }
122 }
123 }