juxscript 1.0.17 → 1.0.19
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 +8 -1
- package/lib/components/button.ts +16 -50
- package/lib/components/docs-data.json +28 -4
- package/lib/components/input.ts +80 -83
- package/lib/components/paragraph.ts +87 -0
- package/lib/components/req.ts +3 -1
- package/lib/components/token-calculator.ts +1 -68
- package/lib/reactivity/state.ts +13 -299
- package/package.json +1 -1
- package/presets/notion.css +1567 -843
package/README.md
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
- [ ] Distributable Bundle (Static Sites)
|
|
13
13
|
- [ ] Tree Shake/Efficiencies.
|
|
14
14
|
|
|
15
|
+
- [ ] Data
|
|
16
|
+
- [ ] Drivers: File, S3, Database.
|
|
17
|
+
- [ ] const d = jux.data('id',{});
|
|
18
|
+
- [ ] d.driver(file|s3|database)
|
|
19
|
+
- [ ] d.items([] | juxitem)
|
|
20
|
+
- [ ] d.store(callback)
|
|
21
|
+
|
|
15
22
|
- [X] Layouts (100% done.)
|
|
16
23
|
- [ ] *Authoring Layout Pages* - `docs`
|
|
17
24
|
- [ ] *Authoring Application Pages* - `docs`
|
|
@@ -22,7 +29,7 @@
|
|
|
22
29
|
- [X] Reactivity (90% done.)
|
|
23
30
|
- [ ] Client Components (99% of what would be needed.)
|
|
24
31
|
- [X] Charts
|
|
25
|
-
- [
|
|
32
|
+
- [X] Poor Intellisense support? Could be this issue. - fixed.
|
|
26
33
|
- [ ] Api Wrapper
|
|
27
34
|
- [X] Params/Active State for Menu/Nav matching - built in.
|
|
28
35
|
- [ ] CDN Bundle (import CDN/'state', 'jux' from cdn.)
|
package/lib/components/button.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Button component options
|
|
@@ -38,12 +39,6 @@ type ButtonState = {
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Button component
|
|
41
|
-
*
|
|
42
|
-
* Usage:
|
|
43
|
-
* jux.button('myButton').label('Click Me').click(() => console.log('hi')).render();
|
|
44
|
-
*
|
|
45
|
-
* // Or with options
|
|
46
|
-
* jux.button('myButton', { label: 'Click Me', click: () => console.log('hi') }).render();
|
|
47
42
|
*/
|
|
48
43
|
export class Button {
|
|
49
44
|
state: ButtonState;
|
|
@@ -51,6 +46,9 @@ export class Button {
|
|
|
51
46
|
_id: string;
|
|
52
47
|
id: string;
|
|
53
48
|
|
|
49
|
+
// Store bind() instructions
|
|
50
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
51
|
+
|
|
54
52
|
constructor(id: string, options?: ButtonOptions) {
|
|
55
53
|
this._id = id;
|
|
56
54
|
this.id = id;
|
|
@@ -77,97 +75,69 @@ export class Button {
|
|
|
77
75
|
* Fluent API
|
|
78
76
|
* ------------------------- */
|
|
79
77
|
|
|
80
|
-
/**
|
|
81
|
-
* Set button label text
|
|
82
|
-
*/
|
|
83
78
|
label(value: string): this {
|
|
84
79
|
this.state.label = value;
|
|
85
80
|
return this;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
/**
|
|
89
|
-
* Set button icon (emoji or text)
|
|
90
|
-
*/
|
|
91
83
|
icon(value: string): this {
|
|
92
84
|
this.state.icon = value;
|
|
93
85
|
return this;
|
|
94
86
|
}
|
|
95
87
|
|
|
96
|
-
/**
|
|
97
|
-
* Set click handler
|
|
98
|
-
*/
|
|
99
88
|
click(callback: (e: Event) => void): this {
|
|
100
89
|
this.state.click = callback;
|
|
101
90
|
return this;
|
|
102
91
|
}
|
|
103
92
|
|
|
104
93
|
/**
|
|
105
|
-
*
|
|
94
|
+
* Bind event handler (stores for wiring in render)
|
|
106
95
|
*/
|
|
96
|
+
bind(event: string, handler: Function): this {
|
|
97
|
+
this._bindings.push({ event, handler });
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
107
101
|
variant(value: string): this {
|
|
108
102
|
this.state.variant = value;
|
|
109
103
|
return this;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
/**
|
|
113
|
-
* Set button size
|
|
114
|
-
*/
|
|
115
106
|
size(value: 'small' | 'medium' | 'large'): this {
|
|
116
107
|
this.state.size = value;
|
|
117
108
|
return this;
|
|
118
109
|
}
|
|
119
110
|
|
|
120
|
-
/**
|
|
121
|
-
* Set disabled state
|
|
122
|
-
*/
|
|
123
111
|
disabled(value: boolean): this {
|
|
124
112
|
this.state.disabled = value;
|
|
125
113
|
return this;
|
|
126
114
|
}
|
|
127
115
|
|
|
128
|
-
/**
|
|
129
|
-
* Set loading state
|
|
130
|
-
*/
|
|
131
116
|
loading(value: boolean): this {
|
|
132
117
|
this.state.loading = value;
|
|
133
118
|
return this;
|
|
134
119
|
}
|
|
135
120
|
|
|
136
|
-
/**
|
|
137
|
-
* Set icon position (left or right)
|
|
138
|
-
*/
|
|
139
121
|
iconPosition(value: 'left' | 'right'): this {
|
|
140
122
|
this.state.iconPosition = value;
|
|
141
123
|
return this;
|
|
142
124
|
}
|
|
143
125
|
|
|
144
|
-
/**
|
|
145
|
-
* Set full width
|
|
146
|
-
*/
|
|
147
126
|
fullWidth(value: boolean): this {
|
|
148
127
|
this.state.fullWidth = value;
|
|
149
128
|
return this;
|
|
150
129
|
}
|
|
151
130
|
|
|
152
|
-
/**
|
|
153
|
-
* Set button type attribute
|
|
154
|
-
*/
|
|
155
131
|
type(value: 'button' | 'submit' | 'reset'): this {
|
|
156
132
|
this.state.type = value;
|
|
157
133
|
return this;
|
|
158
134
|
}
|
|
159
135
|
|
|
160
|
-
/**
|
|
161
|
-
* Set inline style
|
|
162
|
-
*/
|
|
163
136
|
style(value: string): this {
|
|
164
137
|
this.state.style = value;
|
|
165
138
|
return this;
|
|
166
139
|
}
|
|
167
140
|
|
|
168
|
-
/**
|
|
169
|
-
* Set additional CSS classes
|
|
170
|
-
*/
|
|
171
141
|
class(value: string): this {
|
|
172
142
|
this.state.class = value;
|
|
173
143
|
return this;
|
|
@@ -177,9 +147,6 @@ export class Button {
|
|
|
177
147
|
* Render
|
|
178
148
|
* ------------------------- */
|
|
179
149
|
|
|
180
|
-
/**
|
|
181
|
-
* Render button to target element
|
|
182
|
-
*/
|
|
183
150
|
render(targetId?: string): this {
|
|
184
151
|
let container: HTMLElement;
|
|
185
152
|
|
|
@@ -238,18 +205,20 @@ export class Button {
|
|
|
238
205
|
button.appendChild(iconEl);
|
|
239
206
|
}
|
|
240
207
|
|
|
241
|
-
// Event binding - click handler
|
|
208
|
+
// Event binding - legacy click handler from state
|
|
242
209
|
if (click) {
|
|
243
210
|
button.addEventListener('click', click);
|
|
244
211
|
}
|
|
245
212
|
|
|
213
|
+
// Event binding - bind() method handlers
|
|
214
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
215
|
+
button.addEventListener(event, handler as EventListener);
|
|
216
|
+
});
|
|
217
|
+
|
|
246
218
|
container.appendChild(button);
|
|
247
219
|
return this;
|
|
248
220
|
}
|
|
249
221
|
|
|
250
|
-
/**
|
|
251
|
-
* Render to another Jux component's container
|
|
252
|
-
*/
|
|
253
222
|
renderTo(juxComponent: this): this {
|
|
254
223
|
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
255
224
|
throw new Error('Button.renderTo: Invalid component - not an object');
|
|
@@ -263,9 +232,6 @@ export class Button {
|
|
|
263
232
|
}
|
|
264
233
|
}
|
|
265
234
|
|
|
266
|
-
/**
|
|
267
|
-
* Factory helper
|
|
268
|
-
*/
|
|
269
235
|
export function button(id: string, options?: ButtonOptions): Button {
|
|
270
236
|
return new Button(id, options);
|
|
271
237
|
}
|
|
@@ -1100,6 +1100,12 @@
|
|
|
1100
1100
|
"returns": "this",
|
|
1101
1101
|
"description": "Set icon"
|
|
1102
1102
|
},
|
|
1103
|
+
{
|
|
1104
|
+
"name": "bind",
|
|
1105
|
+
"params": "(event, handler)",
|
|
1106
|
+
"returns": "this",
|
|
1107
|
+
"description": "Set bind"
|
|
1108
|
+
},
|
|
1103
1109
|
{
|
|
1104
1110
|
"name": "variant",
|
|
1105
1111
|
"params": "(value)",
|
|
@@ -1167,7 +1173,7 @@
|
|
|
1167
1173
|
"description": "Set renderTo"
|
|
1168
1174
|
}
|
|
1169
1175
|
],
|
|
1170
|
-
"example": "jux.button('
|
|
1176
|
+
"example": "jux.button('id').render()"
|
|
1171
1177
|
},
|
|
1172
1178
|
{
|
|
1173
1179
|
"name": "Card",
|
|
@@ -2391,10 +2397,16 @@
|
|
|
2391
2397
|
},
|
|
2392
2398
|
{
|
|
2393
2399
|
"name": "bind",
|
|
2394
|
-
"params": "(
|
|
2400
|
+
"params": "(event, handler)",
|
|
2395
2401
|
"returns": "this",
|
|
2396
2402
|
"description": "Set bind"
|
|
2397
2403
|
},
|
|
2404
|
+
{
|
|
2405
|
+
"name": "sync",
|
|
2406
|
+
"params": "(property, stateObj, toState?, toComponent?)",
|
|
2407
|
+
"returns": "this",
|
|
2408
|
+
"description": "Set sync"
|
|
2409
|
+
},
|
|
2398
2410
|
{
|
|
2399
2411
|
"name": "render",
|
|
2400
2412
|
"params": "(targetId?)",
|
|
@@ -2408,7 +2420,7 @@
|
|
|
2408
2420
|
"description": "Set renderTo"
|
|
2409
2421
|
}
|
|
2410
2422
|
],
|
|
2411
|
-
"example": "jux.input('
|
|
2423
|
+
"example": "jux.input('id').render()"
|
|
2412
2424
|
},
|
|
2413
2425
|
{
|
|
2414
2426
|
"name": "Kpicard",
|
|
@@ -2818,6 +2830,18 @@
|
|
|
2818
2830
|
"returns": "this",
|
|
2819
2831
|
"description": "Set style"
|
|
2820
2832
|
},
|
|
2833
|
+
{
|
|
2834
|
+
"name": "bind",
|
|
2835
|
+
"params": "(property, source, transform?)",
|
|
2836
|
+
"returns": "this",
|
|
2837
|
+
"description": "Set bind"
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
"name": "sync",
|
|
2841
|
+
"params": "(property, stateObj, transform?)",
|
|
2842
|
+
"returns": "this",
|
|
2843
|
+
"description": "Set sync"
|
|
2844
|
+
},
|
|
2821
2845
|
{
|
|
2822
2846
|
"name": "render",
|
|
2823
2847
|
"params": "(targetId?)",
|
|
@@ -3518,5 +3542,5 @@
|
|
|
3518
3542
|
}
|
|
3519
3543
|
],
|
|
3520
3544
|
"version": "1.0.0",
|
|
3521
|
-
"lastUpdated": "2026-01-
|
|
3545
|
+
"lastUpdated": "2026-01-22T21:54:27.911Z"
|
|
3522
3546
|
}
|
package/lib/components/input.ts
CHANGED
|
@@ -49,29 +49,7 @@ type InputState = {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Input component
|
|
53
|
-
*
|
|
54
|
-
* Usage:
|
|
55
|
-
* jux.input('username')
|
|
56
|
-
* .label('Username')
|
|
57
|
-
* .placeholder('Enter username')
|
|
58
|
-
* .required(true)
|
|
59
|
-
* .minLength(3)
|
|
60
|
-
* .maxLength(20)
|
|
61
|
-
* .render('#form');
|
|
62
|
-
*
|
|
63
|
-
* jux.input('age')
|
|
64
|
-
* .type('number')
|
|
65
|
-
* .min(0)
|
|
66
|
-
* .max(120)
|
|
67
|
-
* .step(1)
|
|
68
|
-
* .render('#form');
|
|
69
|
-
*
|
|
70
|
-
* jux.input('bio')
|
|
71
|
-
* .type('textarea')
|
|
72
|
-
* .rows(5)
|
|
73
|
-
* .maxLength(500)
|
|
74
|
-
* .render('#form');
|
|
52
|
+
* Input component
|
|
75
53
|
*/
|
|
76
54
|
export class Input {
|
|
77
55
|
state: InputState;
|
|
@@ -80,7 +58,12 @@ export class Input {
|
|
|
80
58
|
id: string;
|
|
81
59
|
private _onChange?: (value: string) => void;
|
|
82
60
|
private _onValidate?: (value: string) => boolean | string;
|
|
83
|
-
|
|
61
|
+
|
|
62
|
+
// Store bind() instructions
|
|
63
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
64
|
+
|
|
65
|
+
// Store sync() instructions
|
|
66
|
+
private _syncBindings: Array<{ property: string, stateObj: State<any>, toState?: Function, toComponent?: Function }> = [];
|
|
84
67
|
|
|
85
68
|
constructor(id: string, options: InputOptions = {}) {
|
|
86
69
|
this._id = id;
|
|
@@ -154,54 +137,36 @@ export class Input {
|
|
|
154
137
|
return this;
|
|
155
138
|
}
|
|
156
139
|
|
|
157
|
-
/**
|
|
158
|
-
* Minimum value for number inputs
|
|
159
|
-
*/
|
|
160
140
|
min(value: number): this {
|
|
161
141
|
this.state.min = value;
|
|
162
142
|
this._updateElement();
|
|
163
143
|
return this;
|
|
164
144
|
}
|
|
165
145
|
|
|
166
|
-
/**
|
|
167
|
-
* Maximum value for number inputs
|
|
168
|
-
*/
|
|
169
146
|
max(value: number): this {
|
|
170
147
|
this.state.max = value;
|
|
171
148
|
this._updateElement();
|
|
172
149
|
return this;
|
|
173
150
|
}
|
|
174
151
|
|
|
175
|
-
/**
|
|
176
|
-
* Step value for number inputs
|
|
177
|
-
*/
|
|
178
152
|
step(value: number): this {
|
|
179
153
|
this.state.step = value;
|
|
180
154
|
this._updateElement();
|
|
181
155
|
return this;
|
|
182
156
|
}
|
|
183
157
|
|
|
184
|
-
/**
|
|
185
|
-
* Minimum length for text inputs
|
|
186
|
-
*/
|
|
187
158
|
minLength(value: number): this {
|
|
188
159
|
this.state.minLength = value;
|
|
189
160
|
this._updateElement();
|
|
190
161
|
return this;
|
|
191
162
|
}
|
|
192
163
|
|
|
193
|
-
/**
|
|
194
|
-
* Maximum length for text inputs
|
|
195
|
-
*/
|
|
196
164
|
maxLength(value: number): this {
|
|
197
165
|
this.state.maxLength = value;
|
|
198
166
|
this._updateElement();
|
|
199
167
|
return this;
|
|
200
168
|
}
|
|
201
169
|
|
|
202
|
-
/**
|
|
203
|
-
* Pattern validation for text inputs (regex)
|
|
204
|
-
*/
|
|
205
170
|
pattern(value: string): this {
|
|
206
171
|
this.state.pattern = value;
|
|
207
172
|
this._updateElement();
|
|
@@ -223,30 +188,29 @@ export class Input {
|
|
|
223
188
|
return this;
|
|
224
189
|
}
|
|
225
190
|
|
|
226
|
-
/**
|
|
227
|
-
* Custom validation function
|
|
228
|
-
* Should return true if valid, or an error message string if invalid
|
|
229
|
-
*/
|
|
230
191
|
onValidate(handler: (value: string) => boolean | string): this {
|
|
231
192
|
this._onValidate = handler;
|
|
232
193
|
return this;
|
|
233
194
|
}
|
|
234
195
|
|
|
235
196
|
/**
|
|
236
|
-
*
|
|
197
|
+
* Bind event handler (stores for wiring in render)
|
|
237
198
|
*/
|
|
238
|
-
bind(
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
stateObj.subscribe((val) => {
|
|
243
|
-
this.state.value = val;
|
|
244
|
-
this._updateElement();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Update state on input change
|
|
248
|
-
this.onChange((value) => stateObj.set(value));
|
|
199
|
+
bind(event: string, handler: Function): this {
|
|
200
|
+
this._bindings.push({ event, handler });
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
249
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Two-way sync with state (stores for wiring in render)
|
|
206
|
+
*
|
|
207
|
+
* @param property - Component property to sync ('value', 'label', etc)
|
|
208
|
+
* @param stateObj - State object to sync with
|
|
209
|
+
* @param toState - Optional transform function when going from component to state
|
|
210
|
+
* @param toComponent - Optional transform function when going from state to component
|
|
211
|
+
*/
|
|
212
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
213
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
250
214
|
return this;
|
|
251
215
|
}
|
|
252
216
|
|
|
@@ -257,12 +221,10 @@ export class Input {
|
|
|
257
221
|
private _validate(value: string): boolean | string {
|
|
258
222
|
const { required, type, min, max, minLength, maxLength, pattern } = this.state;
|
|
259
223
|
|
|
260
|
-
// Required check
|
|
261
224
|
if (required && !value.trim()) {
|
|
262
225
|
return 'This field is required';
|
|
263
226
|
}
|
|
264
227
|
|
|
265
|
-
// Number validation
|
|
266
228
|
if (type === 'number' && value) {
|
|
267
229
|
const numValue = Number(value);
|
|
268
230
|
|
|
@@ -279,7 +241,6 @@ export class Input {
|
|
|
279
241
|
}
|
|
280
242
|
}
|
|
281
243
|
|
|
282
|
-
// Text length validation
|
|
283
244
|
if ((type === 'text' || type === 'textarea') && value) {
|
|
284
245
|
if (minLength !== undefined && value.length < minLength) {
|
|
285
246
|
return `Must be at least ${minLength} characters`;
|
|
@@ -290,7 +251,6 @@ export class Input {
|
|
|
290
251
|
}
|
|
291
252
|
}
|
|
292
253
|
|
|
293
|
-
// Pattern validation
|
|
294
254
|
if (pattern && value) {
|
|
295
255
|
const regex = new RegExp(pattern);
|
|
296
256
|
if (!regex.test(value)) {
|
|
@@ -298,7 +258,6 @@ export class Input {
|
|
|
298
258
|
}
|
|
299
259
|
}
|
|
300
260
|
|
|
301
|
-
// Custom validation
|
|
302
261
|
if (this._onValidate) {
|
|
303
262
|
const result = this._onValidate(value);
|
|
304
263
|
if (result !== true) {
|
|
@@ -341,9 +300,6 @@ export class Input {
|
|
|
341
300
|
this.state.errorMessage = undefined;
|
|
342
301
|
}
|
|
343
302
|
|
|
344
|
-
/**
|
|
345
|
-
* Manually validate the current value
|
|
346
|
-
*/
|
|
347
303
|
validate(): boolean {
|
|
348
304
|
const result = this._validate(this.state.value);
|
|
349
305
|
|
|
@@ -428,17 +384,17 @@ export class Input {
|
|
|
428
384
|
}
|
|
429
385
|
|
|
430
386
|
// Label
|
|
387
|
+
const labelEl = document.createElement('label');
|
|
388
|
+
labelEl.className = 'jux-input-label';
|
|
389
|
+
labelEl.htmlFor = `${this._id}-input`;
|
|
390
|
+
labelEl.textContent = label;
|
|
391
|
+
if (required) {
|
|
392
|
+
const requiredSpan = document.createElement('span');
|
|
393
|
+
requiredSpan.className = 'jux-input-required';
|
|
394
|
+
requiredSpan.textContent = ' *';
|
|
395
|
+
labelEl.appendChild(requiredSpan);
|
|
396
|
+
}
|
|
431
397
|
if (label) {
|
|
432
|
-
const labelEl = document.createElement('label');
|
|
433
|
-
labelEl.className = 'jux-input-label';
|
|
434
|
-
labelEl.htmlFor = `${this._id}-input`;
|
|
435
|
-
labelEl.textContent = label;
|
|
436
|
-
if (required) {
|
|
437
|
-
const requiredSpan = document.createElement('span');
|
|
438
|
-
requiredSpan.className = 'jux-input-required';
|
|
439
|
-
requiredSpan.textContent = ' *';
|
|
440
|
-
labelEl.appendChild(requiredSpan);
|
|
441
|
-
}
|
|
442
398
|
wrapper.appendChild(labelEl);
|
|
443
399
|
}
|
|
444
400
|
|
|
@@ -454,14 +410,12 @@ export class Input {
|
|
|
454
410
|
inputEl = document.createElement('input');
|
|
455
411
|
inputEl.type = type;
|
|
456
412
|
|
|
457
|
-
// Number-specific attributes
|
|
458
413
|
if (type === 'number') {
|
|
459
414
|
if (min !== undefined) inputEl.min = String(min);
|
|
460
415
|
if (max !== undefined) inputEl.max = String(max);
|
|
461
416
|
if (step !== undefined) inputEl.step = String(step);
|
|
462
417
|
}
|
|
463
418
|
|
|
464
|
-
// Text-specific attributes
|
|
465
419
|
if (type === 'text' || type === 'email' || type === 'tel' || type === 'url') {
|
|
466
420
|
if (minLength !== undefined) inputEl.minLength = minLength;
|
|
467
421
|
if (maxLength !== undefined) inputEl.maxLength = maxLength;
|
|
@@ -477,12 +431,11 @@ export class Input {
|
|
|
477
431
|
inputEl.required = required;
|
|
478
432
|
inputEl.disabled = disabled;
|
|
479
433
|
|
|
480
|
-
// Input event handler
|
|
434
|
+
// Input event handler - from onChange option
|
|
481
435
|
inputEl.addEventListener('input', (e) => {
|
|
482
436
|
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
|
483
437
|
this.state.value = target.value;
|
|
484
438
|
|
|
485
|
-
// Clear error on input
|
|
486
439
|
this._clearError();
|
|
487
440
|
|
|
488
441
|
if (this._onChange) {
|
|
@@ -490,7 +443,6 @@ export class Input {
|
|
|
490
443
|
}
|
|
491
444
|
});
|
|
492
445
|
|
|
493
|
-
// Blur event for validation
|
|
494
446
|
inputEl.addEventListener('blur', () => {
|
|
495
447
|
this.validate();
|
|
496
448
|
});
|
|
@@ -519,10 +471,55 @@ export class Input {
|
|
|
519
471
|
}
|
|
520
472
|
|
|
521
473
|
container.appendChild(wrapper);
|
|
522
|
-
|
|
523
|
-
// Add default styles if not already present
|
|
524
474
|
this._injectDefaultStyles();
|
|
525
475
|
|
|
476
|
+
// === Wire up event bindings ===
|
|
477
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
478
|
+
wrapper.addEventListener(event, handler as EventListener);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// === Wire up sync bindings (TWO-WAY) ===
|
|
482
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
483
|
+
if (property === 'value') {
|
|
484
|
+
// Default transforms
|
|
485
|
+
const transformToState = toState || ((v: string) => {
|
|
486
|
+
return type === 'number' ? (parseInt(v) || 0) : v;
|
|
487
|
+
});
|
|
488
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
489
|
+
|
|
490
|
+
let isUpdating = false;
|
|
491
|
+
|
|
492
|
+
// State → Input (when state changes, update input)
|
|
493
|
+
stateObj.subscribe((val: any) => {
|
|
494
|
+
if (isUpdating) return;
|
|
495
|
+
const transformed = transformToComponent(val);
|
|
496
|
+
if (inputEl.value !== transformed) {
|
|
497
|
+
inputEl.value = transformed;
|
|
498
|
+
this.state.value = transformed;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Input → State (when input changes, update state)
|
|
503
|
+
inputEl.addEventListener('input', () => {
|
|
504
|
+
if (isUpdating) return;
|
|
505
|
+
isUpdating = true;
|
|
506
|
+
const transformed = transformToState(inputEl.value);
|
|
507
|
+
stateObj.set(transformed);
|
|
508
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
else if (property === 'label') {
|
|
512
|
+
// Sync label
|
|
513
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
514
|
+
|
|
515
|
+
stateObj.subscribe((val: any) => {
|
|
516
|
+
const transformed = transformToComponent(val);
|
|
517
|
+
labelEl.textContent = transformed;
|
|
518
|
+
this.state.label = transformed;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
526
523
|
return this;
|
|
527
524
|
}
|
|
528
525
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Paragraph options
|
|
@@ -24,6 +25,17 @@ type ParagraphState = {
|
|
|
24
25
|
* Usage:
|
|
25
26
|
* jux.paragraph('intro', { text: 'Welcome to JUX' }).render('#app');
|
|
26
27
|
* jux.paragraph('description').text('A simple framework').render('#app');
|
|
28
|
+
*
|
|
29
|
+
* // With state binding
|
|
30
|
+
* jux.paragraph('counter')
|
|
31
|
+
* .text('Count: 0')
|
|
32
|
+
* .bind('text', count, (val) => `Count: ${val}`)
|
|
33
|
+
* .render('#app');
|
|
34
|
+
*
|
|
35
|
+
* // With sync (one-way for paragraph)
|
|
36
|
+
* jux.paragraph('display')
|
|
37
|
+
* .sync('text', count, (val) => `Count: ${val}`)
|
|
38
|
+
* .render('#app');
|
|
27
39
|
*/
|
|
28
40
|
export class Paragraph {
|
|
29
41
|
state: ParagraphState;
|
|
@@ -31,6 +43,12 @@ export class Paragraph {
|
|
|
31
43
|
_id: string;
|
|
32
44
|
id: string;
|
|
33
45
|
|
|
46
|
+
// Store bind() instructions
|
|
47
|
+
private _bindings: Array<{ event: string, handler: Function, stateObj?: State<any>, transform?: Function }> = [];
|
|
48
|
+
|
|
49
|
+
// Store sync() instructions
|
|
50
|
+
private _syncBindings: Array<{ property: string, stateObj: State<any>, transform?: Function }> = [];
|
|
51
|
+
|
|
34
52
|
constructor(id: string, options: ParagraphOptions = {}) {
|
|
35
53
|
this._id = id;
|
|
36
54
|
this.id = id;
|
|
@@ -61,6 +79,40 @@ export class Paragraph {
|
|
|
61
79
|
return this;
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Bind event or state (stores for wiring in render)
|
|
84
|
+
*/
|
|
85
|
+
bind(property: string, source: State<any> | Function, transform?: Function): this {
|
|
86
|
+
if (typeof source === 'function') {
|
|
87
|
+
// Event binding
|
|
88
|
+
this._bindings.push({ event: property, handler: source });
|
|
89
|
+
} else {
|
|
90
|
+
// Validate it's a State object
|
|
91
|
+
if (!source || typeof source.subscribe !== 'function') {
|
|
92
|
+
throw new Error(`Paragraph.bind: Expected a State object, got ${typeof source}. Did you pass 'state.value' instead of 'state'?`);
|
|
93
|
+
}
|
|
94
|
+
// State binding
|
|
95
|
+
this._bindings.push({ event: property, handler: () => { }, stateObj: source, transform });
|
|
96
|
+
}
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sync with state (one-way for paragraph: State → Component)
|
|
102
|
+
*
|
|
103
|
+
* @param property - Component property to sync ('text', 'class', 'style')
|
|
104
|
+
* @param stateObj - State object to sync with
|
|
105
|
+
* @param transform - Optional transform function when going from state to component
|
|
106
|
+
*/
|
|
107
|
+
sync(property: string, stateObj: State<any>, transform?: Function): this {
|
|
108
|
+
// Validate it's a State object
|
|
109
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
110
|
+
throw new Error(`Paragraph.sync: Expected a State object, got ${typeof stateObj}. Did you pass 'state.value' instead of 'state'?`);
|
|
111
|
+
}
|
|
112
|
+
this._syncBindings.push({ property, stateObj, transform });
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
64
116
|
/* -------------------------
|
|
65
117
|
* Render
|
|
66
118
|
* ------------------------- */
|
|
@@ -99,6 +151,41 @@ export class Paragraph {
|
|
|
99
151
|
|
|
100
152
|
container.appendChild(p);
|
|
101
153
|
|
|
154
|
+
// Wire up bind() bindings after DOM element is created
|
|
155
|
+
this._bindings.forEach(({ event, handler, stateObj, transform }) => {
|
|
156
|
+
if (stateObj) {
|
|
157
|
+
// State binding - subscribe to state changes
|
|
158
|
+
stateObj.subscribe((val: any) => {
|
|
159
|
+
const transformed = transform ? transform(val) : val;
|
|
160
|
+
if (event === 'text') {
|
|
161
|
+
p.textContent = transformed;
|
|
162
|
+
this.state.text = transformed;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
// Event binding
|
|
167
|
+
p.addEventListener(event, handler as EventListener);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Wire up sync() bindings (State → Component)
|
|
172
|
+
this._syncBindings.forEach(({ property, stateObj, transform }) => {
|
|
173
|
+
stateObj.subscribe((val: any) => {
|
|
174
|
+
const transformed = transform ? transform(val) : val;
|
|
175
|
+
|
|
176
|
+
if (property === 'text') {
|
|
177
|
+
p.textContent = transformed;
|
|
178
|
+
this.state.text = transformed;
|
|
179
|
+
} else if (property === 'class') {
|
|
180
|
+
p.className = transformed;
|
|
181
|
+
this.state.class = transformed;
|
|
182
|
+
} else if (property === 'style') {
|
|
183
|
+
p.setAttribute('style', transformed);
|
|
184
|
+
this.state.style = transformed;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
102
189
|
return this;
|
|
103
190
|
}
|
|
104
191
|
}
|