mithril-mobs 0.0.1 → 0.0.3
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 +1 -0
- package/package.json +1 -1
- package/src/mobs/index.js +174 -63
package/README.md
CHANGED
package/package.json
CHANGED
package/src/mobs/index.js
CHANGED
@@ -1,66 +1,193 @@
|
|
1
1
|
import m from "mithril"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
};
|
3
|
+
/**
|
4
|
+
* Reactive Observable (obs)
|
5
|
+
*/
|
6
|
+
export function obs(initialValue) {
|
7
|
+
let _value = initialValue;
|
8
|
+
const listeners = new Set();
|
9
|
+
|
10
|
+
const api = {
|
11
|
+
get value() {
|
12
|
+
return _value;
|
13
|
+
},
|
14
|
+
set value(newValue) {
|
15
|
+
_value = newValue;
|
16
|
+
listeners.forEach(listener => listener(_value));
|
17
|
+
},
|
18
|
+
subscribe(listener) {
|
19
|
+
listeners.add(listener);
|
20
|
+
return () => listeners.delete(listener);
|
21
|
+
},
|
22
|
+
toJSON() {
|
23
|
+
return { __obs__: true, value: _value }; // Mark for serialization
|
24
|
+
},
|
25
|
+
_isObs: true
|
26
|
+
};
|
28
27
|
|
29
|
-
|
28
|
+
return api;
|
30
29
|
}
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
/**
|
32
|
+
* Helper to serialize state with obs() support
|
33
|
+
*/
|
34
|
+
function serializeState(state) {
|
35
|
+
const result = {};
|
36
|
+
for (const key in state) {
|
37
|
+
const val = state[key];
|
38
|
+
if (val && typeof val === 'object' && val._isObs) {
|
39
|
+
result[key] = val.toJSON();
|
40
|
+
} else {
|
41
|
+
result[key] = val;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
return JSON.stringify(result);
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Helper to deserialize and hydrate obs() in state
|
49
|
+
*/
|
50
|
+
function deserializeState(json, template = {}) {
|
51
|
+
const parsed = JSON.parse(json);
|
52
|
+
const result = {};
|
53
|
+
|
54
|
+
for (const key in parsed) {
|
55
|
+
const val = parsed[key];
|
56
|
+
|
57
|
+
if (val && typeof val === 'object' && val.__obs__) {
|
58
|
+
if (template[key] && template[key]._isObs) {
|
59
|
+
template[key].value = val.value;
|
60
|
+
result[key] = template[key];
|
61
|
+
} else {
|
62
|
+
result[key] = obs(val.value);
|
63
|
+
}
|
64
|
+
} else {
|
65
|
+
result[key] = val;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
return result;
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* defineState - reactive state with localStorage
|
74
|
+
*/
|
75
|
+
export function defineState(name, stateTemplate) {
|
76
|
+
const listeners = new Set();
|
77
|
+
let state = { ...stateTemplate };
|
78
|
+
|
79
|
+
try {
|
80
|
+
const saved = localStorage.getItem(name);
|
81
|
+
if (saved) {
|
82
|
+
state = deserializeState(saved, stateTemplate);
|
83
|
+
}
|
84
|
+
} catch (e) {
|
85
|
+
console.warn(`Failed to load state '${name}' from localStorage:`, e);
|
86
|
+
}
|
87
|
+
|
88
|
+
return {
|
89
|
+
...state,
|
90
|
+
set(setter, save) {
|
91
|
+
setter(state);
|
92
|
+
listeners.forEach(listener => listener(state));
|
93
|
+
if (save) this.save();
|
94
|
+
},
|
95
|
+
subscribe(listener) {
|
96
|
+
listeners.add(listener);
|
97
|
+
return () => listeners.delete(listener);
|
98
|
+
},
|
99
|
+
save() {
|
100
|
+
try {
|
101
|
+
localStorage.setItem(name, serializeState(state));
|
102
|
+
} catch (e) {
|
103
|
+
console.warn(`Failed to save state '${name}' to localStorage:`, e);
|
104
|
+
}
|
105
|
+
},
|
106
|
+
load() {
|
107
|
+
try {
|
108
|
+
const saved = localStorage.getItem(name);
|
109
|
+
if (saved) {
|
110
|
+
const newState = deserializeState(saved, state);
|
111
|
+
Object.assign(state, newState);
|
112
|
+
listeners.forEach(listener => listener(state));
|
113
|
+
}
|
114
|
+
} catch (e) {
|
115
|
+
console.warn(`Failed to reload state '${name}' from localStorage:`, e);
|
116
|
+
}
|
117
|
+
}
|
118
|
+
};
|
39
119
|
}
|
40
120
|
|
121
|
+
/**
|
122
|
+
* Class Binding Utility
|
123
|
+
*/
|
124
|
+
export function classes(classes_) {
|
125
|
+
const result = [];
|
126
|
+
for (let key in classes_) {
|
127
|
+
if (classes_[key]) result.push(key);
|
128
|
+
}
|
129
|
+
return result.join(" ");
|
130
|
+
}
|
41
131
|
|
132
|
+
/**
|
133
|
+
* Conditional Rendering Helper
|
134
|
+
*/
|
135
|
+
export function if_(firstCondition) {
|
136
|
+
const conditions = [firstCondition];
|
137
|
+
const vnodes = [];
|
42
138
|
|
139
|
+
const ctx = {
|
140
|
+
then(v) {
|
141
|
+
vnodes.push(v);
|
142
|
+
return prepare(ctx);
|
143
|
+
},
|
144
|
+
elseIf(newCondition) {
|
145
|
+
conditions.push(newCondition);
|
146
|
+
return prepare(ctx);
|
147
|
+
},
|
148
|
+
else(v) {
|
149
|
+
conditions.push(true);
|
150
|
+
vnodes.push(v);
|
151
|
+
return prepare(ctx);
|
152
|
+
},
|
153
|
+
view() {
|
154
|
+
for (let i = 0; i < conditions.length; i++) {
|
155
|
+
if (conditions[i]) return vnodes[i] || null;
|
156
|
+
}
|
157
|
+
return null;
|
158
|
+
}
|
159
|
+
};
|
43
160
|
|
161
|
+
return prepare(ctx);
|
162
|
+
}
|
44
163
|
|
164
|
+
function prepare(ctx) {
|
165
|
+
const n = m(ctx);
|
166
|
+
n.then = ctx.then;
|
167
|
+
n.elseIf = ctx.elseIf;
|
168
|
+
n.else = ctx.else;
|
169
|
+
n.view = ctx.view;
|
170
|
+
return n;
|
171
|
+
}
|
45
172
|
|
173
|
+
/**
|
174
|
+
* Styled Component Creator
|
175
|
+
*/
|
46
176
|
const Component = {
|
47
177
|
_counter: 0,
|
48
178
|
|
49
179
|
create(def) {
|
50
180
|
const compId = `comp-${Component._counter++}`;
|
51
181
|
|
52
|
-
// Convert SASS-like staticStyles object to CSS string
|
53
182
|
function stylesToCss(styles, prefix = `[data-comp-id="${compId}"]`) {
|
54
183
|
let css = "";
|
55
184
|
let props = [];
|
56
185
|
|
57
186
|
for (const key in styles) {
|
58
187
|
if (key.startsWith("/")) {
|
59
|
-
// Nested selector, replace "/" with space for nesting
|
60
188
|
const selector = prefix + key.replace(/\//g, " ");
|
61
189
|
css += stylesToCss(styles[key], selector);
|
62
190
|
} else {
|
63
|
-
// Style property
|
64
191
|
props.push(`${key}: ${styles[key]};`);
|
65
192
|
}
|
66
193
|
}
|
@@ -72,7 +199,6 @@ const Component = {
|
|
72
199
|
return css;
|
73
200
|
}
|
74
201
|
|
75
|
-
// Automatically add <style id="component-styles"> if missing
|
76
202
|
let styleTag = document.getElementById("component-styles");
|
77
203
|
if (!styleTag) {
|
78
204
|
styleTag = document.createElement("style");
|
@@ -80,7 +206,6 @@ const Component = {
|
|
80
206
|
document.head.appendChild(styleTag);
|
81
207
|
}
|
82
208
|
|
83
|
-
// Use a Set to track injected components
|
84
209
|
styleTag.__injectedComponents = styleTag.__injectedComponents || new Set();
|
85
210
|
|
86
211
|
if (!styleTag.__injectedComponents.has(compId)) {
|
@@ -112,33 +237,19 @@ const Component = {
|
|
112
237
|
export const createComponent = (def) => Component.create(def);
|
113
238
|
|
114
239
|
|
115
|
-
export function obs(initialValue) {
|
116
|
-
let value = initialValue;
|
117
|
-
const listeners = new Set();
|
118
240
|
|
119
|
-
return {
|
120
|
-
get() {
|
121
|
-
return value;
|
122
|
-
},
|
123
|
-
set(newValue) {
|
124
|
-
value = newValue
|
125
|
-
listeners.forEach((listener) => listener(value))
|
126
|
-
},
|
127
|
-
subscribe(listener) {
|
128
|
-
listeners.add(listener);
|
129
|
-
// Return an unsubscribe function
|
130
|
-
return () => listeners.delete(listener);
|
131
|
-
}
|
132
|
-
};
|
133
|
-
}
|
134
241
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
242
|
+
/**
|
243
|
+
* Repeat Helper
|
244
|
+
*/
|
245
|
+
export const repeat = (times, increment = 1) => {
|
246
|
+
return {
|
247
|
+
map(cb) {
|
248
|
+
const items = []
|
249
|
+
for(let i = 0; i < times; i+=increment) {
|
250
|
+
items.push(cb(i))
|
251
|
+
}
|
252
|
+
return items
|
141
253
|
}
|
142
254
|
}
|
143
|
-
return classes__.join(" ")
|
144
255
|
}
|