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 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
- - [ ] Poor Intellisense support? Could be this issue.
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.)
@@ -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
- * Set button variant/style
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('myButton').label('Click Me').click(() => console.log('hi')).render();"
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": "(stateObj)",
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('username')"
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-21T22:44:40.980Z"
3545
+ "lastUpdated": "2026-01-22T21:54:27.911Z"
3522
3546
  }
@@ -49,29 +49,7 @@ type InputState = {
49
49
  };
50
50
 
51
51
  /**
52
- * Input component - text input and textarea with validation
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
- private _boundState?: State<string>;
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
- * Two-way binding to state
197
+ * Bind event handler (stores for wiring in render)
237
198
  */
238
- bind(stateObj: State<string>): this {
239
- this._boundState = stateObj;
240
-
241
- // Subscribe to state changes
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
  }