dzql 0.1.0-alpha.1

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.
@@ -0,0 +1,990 @@
1
+ /**
2
+ * DZQL Declarative UI Framework
3
+ *
4
+ * Renders adaptive UI components from JSON descriptions.
5
+ * Handles all DZQL operations declaratively with automatic state management.
6
+ */
7
+
8
+ // Component Registry - Maps component types to render functions
9
+ const componentRegistry = new Map();
10
+
11
+ // Global state management
12
+ class StateManager {
13
+ constructor() {
14
+ this.state = {};
15
+ this.listeners = new Map();
16
+ this.componentStates = new Map();
17
+ }
18
+
19
+ get(path) {
20
+ const keys = path.split('.');
21
+ let value = this.state;
22
+ for (const key of keys) {
23
+ value = value?.[key];
24
+ }
25
+ return value;
26
+ }
27
+
28
+ set(path, value) {
29
+ const keys = path.split('.');
30
+ const lastKey = keys.pop();
31
+ let target = this.state;
32
+
33
+ for (const key of keys) {
34
+ if (!target[key]) target[key] = {};
35
+ target = target[key];
36
+ }
37
+
38
+ target[lastKey] = value;
39
+ this.notify(path, value);
40
+ }
41
+
42
+ subscribe(path, callback) {
43
+ if (!this.listeners.has(path)) {
44
+ this.listeners.set(path, new Set());
45
+ }
46
+ this.listeners.get(path).add(callback);
47
+ return () => this.listeners.get(path)?.delete(callback);
48
+ }
49
+
50
+ notify(path, value) {
51
+ // Notify exact path listeners
52
+ this.listeners.get(path)?.forEach(cb => cb(value));
53
+
54
+ // Notify parent path listeners
55
+ const parts = path.split('.');
56
+ for (let i = parts.length - 1; i > 0; i--) {
57
+ const parentPath = parts.slice(0, i).join('.');
58
+ this.listeners.get(parentPath)?.forEach(cb => cb(this.get(parentPath)));
59
+ }
60
+ }
61
+
62
+ // Component-specific state
63
+ getComponentState(componentId) {
64
+ if (!this.componentStates.has(componentId)) {
65
+ this.componentStates.set(componentId, {});
66
+ }
67
+ return this.componentStates.get(componentId);
68
+ }
69
+
70
+ setComponentState(componentId, state) {
71
+ this.componentStates.set(componentId, state);
72
+ this.notify(`component.${componentId}`, state);
73
+ }
74
+ }
75
+
76
+ const state = new StateManager();
77
+
78
+ /**
79
+ * Base Component Class
80
+ */
81
+ class Component {
82
+ constructor(config, ws) {
83
+ this.config = config;
84
+ this.ws = ws;
85
+ this.id = config.id || `comp_${Math.random().toString(36).substr(2, 9)}`;
86
+ this.element = null;
87
+ this.children = [];
88
+ this.subscriptions = [];
89
+ }
90
+
91
+ // Evaluate value - handles static values, state bindings, and expressions
92
+ evaluate(value) {
93
+ if (typeof value !== 'string') return value;
94
+
95
+ // State binding: ${state.path}
96
+ if (value.startsWith('${') && value.endsWith('}')) {
97
+ const path = value.slice(2, -1).trim();
98
+ if (path.startsWith('state.')) {
99
+ return state.get(path.substring(6));
100
+ }
101
+ if (path.startsWith('component.')) {
102
+ const componentState = state.getComponentState(this.id);
103
+ const componentPath = path.substring(10);
104
+ return componentPath.split('.').reduce((obj, key) => obj?.[key], componentState);
105
+ }
106
+ // Evaluate as expression
107
+ try {
108
+ return new Function('state', 'component', `return ${path}`)(
109
+ state.state,
110
+ state.getComponentState(this.id)
111
+ );
112
+ } catch (e) {
113
+ console.warn(`Failed to evaluate expression: ${path}`, e);
114
+ return value;
115
+ }
116
+ }
117
+
118
+ return value;
119
+ }
120
+
121
+ // Bind to state changes
122
+ bind(path, callback) {
123
+ const unsubscribe = state.subscribe(path, callback);
124
+ this.subscriptions.push(unsubscribe);
125
+ return unsubscribe;
126
+ }
127
+
128
+ // Process event handlers
129
+ handleEvent(eventConfig) {
130
+ return async (event) => {
131
+ event.preventDefault?.();
132
+
133
+ for (const action of (eventConfig.actions || [])) {
134
+ await this.executeAction(action, event);
135
+ }
136
+ };
137
+ }
138
+
139
+ // Execute an action
140
+ async executeAction(action, event) {
141
+ switch (action.type) {
142
+ case 'setState':
143
+ state.set(action.path, this.evaluate(action.value));
144
+ break;
145
+
146
+ case 'setComponentState':
147
+ const currentState = state.getComponentState(this.id);
148
+ const newState = { ...currentState, [action.key]: this.evaluate(action.value) };
149
+ state.setComponentState(this.id, newState);
150
+ break;
151
+
152
+ case 'call':
153
+ await this.callDZQL(action);
154
+ break;
155
+
156
+ case 'emit':
157
+ this.emit(action.event, this.evaluate(action.data));
158
+ break;
159
+
160
+ case 'navigate':
161
+ window.location.href = this.evaluate(action.url);
162
+ break;
163
+
164
+ case 'alert':
165
+ alert(this.evaluate(action.message));
166
+ break;
167
+
168
+ case 'console':
169
+ console[action.level || 'log'](this.evaluate(action.message));
170
+ break;
171
+
172
+ default:
173
+ console.warn(`Unknown action type: ${action.type}`);
174
+ }
175
+ }
176
+
177
+ // Call DZQL API
178
+ async callDZQL(action) {
179
+ try {
180
+ const { operation, entity, params, onSuccess, onError, resultPath } = action;
181
+
182
+ // Evaluate params
183
+ const evaluatedParams = {};
184
+ for (const [key, value] of Object.entries(params || {})) {
185
+ evaluatedParams[key] = this.evaluate(value);
186
+ }
187
+
188
+ // Make the call
189
+ let result;
190
+ if (operation && entity) {
191
+ // DZQL operation: ws.api.{operation}.{entity}(params)
192
+ result = await this.ws.api[operation][entity](evaluatedParams);
193
+ } else if (action.method) {
194
+ // Direct method call: ws.call(method, params)
195
+ result = await this.ws.call(action.method, evaluatedParams);
196
+ }
197
+
198
+ // Store result if path provided
199
+ if (resultPath) {
200
+ state.set(resultPath, result);
201
+ }
202
+
203
+ // Execute success actions
204
+ if (onSuccess) {
205
+ for (const successAction of onSuccess) {
206
+ await this.executeAction(successAction);
207
+ }
208
+ }
209
+ } catch (error) {
210
+ console.error('DZQL call failed:', error);
211
+
212
+ // Store error if path provided
213
+ if (action.errorPath) {
214
+ state.set(action.errorPath, error.message);
215
+ }
216
+
217
+ // Execute error actions
218
+ if (action.onError) {
219
+ for (const errorAction of action.onError) {
220
+ await this.executeAction(errorAction);
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ // Emit custom event
227
+ emit(eventName, data) {
228
+ const event = new CustomEvent(eventName, { detail: data, bubbles: true });
229
+ this.element?.dispatchEvent(event);
230
+ }
231
+
232
+ // Cleanup
233
+ destroy() {
234
+ this.subscriptions.forEach(unsub => unsub());
235
+ this.children.forEach(child => child.destroy());
236
+ this.element?.remove();
237
+ }
238
+
239
+ // Base render method (override in subclasses)
240
+ render() {
241
+ throw new Error('Component must implement render()');
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Container Component - Renders child components
247
+ */
248
+ class ContainerComponent extends Component {
249
+ render() {
250
+ const el = document.createElement(this.config.tag || 'div');
251
+ this.element = el;
252
+
253
+ // Apply attributes
254
+ if (this.config.attributes) {
255
+ for (const [key, value] of Object.entries(this.config.attributes)) {
256
+ el.setAttribute(key, this.evaluate(value));
257
+ }
258
+ }
259
+
260
+ // Apply styles
261
+ if (this.config.style) {
262
+ Object.assign(el.style, this.config.style);
263
+ }
264
+
265
+ // Apply classes
266
+ if (this.config.class) {
267
+ el.className = this.evaluate(this.config.class);
268
+ }
269
+
270
+ // Render children
271
+ if (this.config.children) {
272
+ for (const childConfig of this.config.children) {
273
+ const child = renderComponent(childConfig, this.ws);
274
+ if (child) {
275
+ this.children.push(child);
276
+ el.appendChild(child.element);
277
+ }
278
+ }
279
+ }
280
+
281
+ // Bind to visibility
282
+ if (this.config.visible) {
283
+ const updateVisibility = () => {
284
+ el.style.display = this.evaluate(this.config.visible) ? '' : 'none';
285
+ };
286
+ updateVisibility();
287
+
288
+ if (this.config.visible.includes('${')) {
289
+ const path = this.config.visible.slice(2, -1).split('.').slice(1).join('.');
290
+ this.bind(path, updateVisibility);
291
+ }
292
+ }
293
+
294
+ return el;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Text Component - Renders text content
300
+ */
301
+ class TextComponent extends Component {
302
+ render() {
303
+ const el = document.createElement(this.config.tag || 'span');
304
+ this.element = el;
305
+
306
+ const updateText = () => {
307
+ el.textContent = this.evaluate(this.config.text || this.config.content || '');
308
+ };
309
+ updateText();
310
+
311
+ // Bind to state changes if needed
312
+ const content = this.config.text || this.config.content || '';
313
+ if (typeof content === 'string' && content.includes('${')) {
314
+ const match = content.match(/\${state\.([^}]+)}/);
315
+ if (match) {
316
+ this.bind(match[1], updateText);
317
+ }
318
+ }
319
+
320
+ // Apply attributes
321
+ if (this.config.attributes) {
322
+ for (const [key, value] of Object.entries(this.config.attributes)) {
323
+ el.setAttribute(key, this.evaluate(value));
324
+ }
325
+ }
326
+
327
+ // Apply styles
328
+ if (this.config.style) {
329
+ Object.assign(el.style, this.config.style);
330
+ }
331
+
332
+ return el;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Input Component - Form input with two-way binding
338
+ */
339
+ class InputComponent extends Component {
340
+ render() {
341
+ const el = document.createElement('input');
342
+ this.element = el;
343
+
344
+ // Set type
345
+ el.type = this.config.inputType || 'text';
346
+
347
+ // Set initial value and bind
348
+ if (this.config.bind) {
349
+ const path = this.config.bind.replace('${state.', '').replace('}', '');
350
+ el.value = state.get(path) || '';
351
+
352
+ // Two-way binding
353
+ el.addEventListener('input', () => {
354
+ state.set(path, el.value);
355
+ });
356
+
357
+ this.bind(path, (value) => {
358
+ if (el.value !== value) {
359
+ el.value = value || '';
360
+ }
361
+ });
362
+ }
363
+
364
+ // Apply attributes
365
+ if (this.config.attributes) {
366
+ for (const [key, value] of Object.entries(this.config.attributes)) {
367
+ el.setAttribute(key, this.evaluate(value));
368
+ }
369
+ }
370
+
371
+ // Handle events
372
+ if (this.config.events) {
373
+ for (const [eventName, eventConfig] of Object.entries(this.config.events)) {
374
+ el.addEventListener(eventName, this.handleEvent(eventConfig));
375
+ }
376
+ }
377
+
378
+ return el;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Button Component
384
+ */
385
+ class ButtonComponent extends Component {
386
+ render() {
387
+ const el = document.createElement('button');
388
+ this.element = el;
389
+
390
+ // Set text
391
+ const updateText = () => {
392
+ el.textContent = this.evaluate(this.config.text || 'Button');
393
+ };
394
+ updateText();
395
+
396
+ // Bind text to state if needed
397
+ if (this.config.text?.includes('${')) {
398
+ const match = this.config.text.match(/\${state\.([^}]+)}/);
399
+ if (match) {
400
+ this.bind(match[1], updateText);
401
+ }
402
+ }
403
+
404
+ // Handle click event
405
+ if (this.config.onClick) {
406
+ el.addEventListener('click', this.handleEvent(this.config.onClick));
407
+ }
408
+
409
+ // Apply attributes
410
+ if (this.config.attributes) {
411
+ for (const [key, value] of Object.entries(this.config.attributes)) {
412
+ el.setAttribute(key, this.evaluate(value));
413
+ }
414
+ }
415
+
416
+ // Apply styles
417
+ if (this.config.style) {
418
+ Object.assign(el.style, this.config.style);
419
+ }
420
+
421
+ // Handle disabled state
422
+ if (this.config.disabled !== undefined) {
423
+ const updateDisabled = () => {
424
+ el.disabled = this.evaluate(this.config.disabled);
425
+ };
426
+ updateDisabled();
427
+
428
+ if (typeof this.config.disabled === 'string' && this.config.disabled.includes('${')) {
429
+ const match = this.config.disabled.match(/\${state\.([^}]+)}/);
430
+ if (match) {
431
+ this.bind(match[1], updateDisabled);
432
+ }
433
+ }
434
+ }
435
+
436
+ return el;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Table Component - Renders data tables with DZQL integration
442
+ */
443
+ class TableComponent extends Component {
444
+ render() {
445
+ const container = document.createElement('div');
446
+ this.element = container;
447
+ container.className = 'table-container';
448
+
449
+ const renderTable = (data) => {
450
+ container.innerHTML = '';
451
+
452
+ if (!data || !Array.isArray(data) || data.length === 0) {
453
+ container.innerHTML = '<p>No data available</p>';
454
+ return;
455
+ }
456
+
457
+ const table = document.createElement('table');
458
+ table.className = this.config.class || '';
459
+
460
+ // Header
461
+ const thead = document.createElement('thead');
462
+ const headerRow = document.createElement('tr');
463
+ const columns = this.config.columns || Object.keys(data[0]);
464
+
465
+ columns.forEach(col => {
466
+ const th = document.createElement('th');
467
+ th.textContent = typeof col === 'object' ? col.label : col;
468
+ headerRow.appendChild(th);
469
+ });
470
+ thead.appendChild(headerRow);
471
+ table.appendChild(thead);
472
+
473
+ // Body
474
+ const tbody = document.createElement('tbody');
475
+ data.forEach(row => {
476
+ const tr = document.createElement('tr');
477
+ columns.forEach(col => {
478
+ const td = document.createElement('td');
479
+ const field = typeof col === 'object' ? col.field : col;
480
+ const value = row[field];
481
+
482
+ // Handle nested values
483
+ if (field.includes('.')) {
484
+ const parts = field.split('.');
485
+ let nestedValue = row;
486
+ for (const part of parts) {
487
+ nestedValue = nestedValue?.[part];
488
+ }
489
+ td.textContent = nestedValue ?? '';
490
+ } else {
491
+ td.textContent = value ?? '';
492
+ }
493
+
494
+ tr.appendChild(td);
495
+ });
496
+
497
+ // Row click handler
498
+ if (this.config.onRowClick) {
499
+ tr.style.cursor = 'pointer';
500
+ tr.addEventListener('click', () => {
501
+ this.executeAction({
502
+ ...this.config.onRowClick,
503
+ value: row
504
+ });
505
+ });
506
+ }
507
+
508
+ tbody.appendChild(tr);
509
+ });
510
+ table.appendChild(tbody);
511
+
512
+ container.appendChild(table);
513
+ };
514
+
515
+ // Initial render
516
+ if (this.config.data) {
517
+ const data = this.evaluate(this.config.data);
518
+ renderTable(data);
519
+ }
520
+
521
+ // Auto-fetch if configured
522
+ if (this.config.fetch) {
523
+ this.callDZQL({
524
+ ...this.config.fetch,
525
+ onSuccess: [{
526
+ type: 'setState',
527
+ path: this.config.dataPath || `tables.${this.id}`,
528
+ value: '${result}'
529
+ }]
530
+ });
531
+ }
532
+
533
+ // Bind to data changes
534
+ if (this.config.dataPath || this.config.data?.includes('${')) {
535
+ const path = this.config.dataPath || this.config.data.slice(9, -1); // Remove ${state. and }
536
+ this.bind(path, renderTable);
537
+ }
538
+
539
+ return container;
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Form Component - Handles form submission with validation
545
+ */
546
+ class FormComponent extends Component {
547
+ render() {
548
+ const form = document.createElement('form');
549
+ this.element = form;
550
+
551
+ // Render fields
552
+ if (this.config.fields) {
553
+ this.config.fields.forEach(fieldConfig => {
554
+ const fieldContainer = document.createElement('div');
555
+ fieldContainer.className = 'form-field';
556
+
557
+ // Label
558
+ if (fieldConfig.label) {
559
+ const label = document.createElement('label');
560
+ label.textContent = fieldConfig.label;
561
+ if (fieldConfig.id) {
562
+ label.setAttribute('for', fieldConfig.id);
563
+ }
564
+ fieldContainer.appendChild(label);
565
+ }
566
+
567
+ // Input
568
+ const input = renderComponent({
569
+ type: 'input',
570
+ ...fieldConfig
571
+ }, this.ws);
572
+
573
+ this.children.push(input);
574
+ fieldContainer.appendChild(input.element);
575
+
576
+ form.appendChild(fieldContainer);
577
+ });
578
+ }
579
+
580
+ // Submit button
581
+ if (this.config.submitButton) {
582
+ const submitBtn = renderComponent({
583
+ type: 'button',
584
+ text: 'Submit',
585
+ ...this.config.submitButton
586
+ }, this.ws);
587
+ this.children.push(submitBtn);
588
+ form.appendChild(submitBtn.element);
589
+ }
590
+
591
+ // Handle form submission
592
+ if (this.config.onSubmit) {
593
+ form.addEventListener('submit', async (e) => {
594
+ e.preventDefault();
595
+
596
+ // Collect form data
597
+ const formData = {};
598
+ const inputs = form.querySelectorAll('input, select, textarea');
599
+ inputs.forEach(input => {
600
+ if (input.name) {
601
+ formData[input.name] = input.value;
602
+ }
603
+ });
604
+
605
+ // Store form data in state
606
+ if (this.config.dataPath) {
607
+ state.set(this.config.dataPath, formData);
608
+ }
609
+
610
+ // Execute submit actions
611
+ await this.handleEvent(this.config.onSubmit)(e);
612
+ });
613
+ }
614
+
615
+ return form;
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Select Component - Dropdown with DZQL lookup integration
621
+ */
622
+ class SelectComponent extends Component {
623
+ render() {
624
+ const select = document.createElement('select');
625
+ this.element = select;
626
+
627
+ // Set name
628
+ if (this.config.name) {
629
+ select.name = this.config.name;
630
+ }
631
+
632
+ // Render options
633
+ const renderOptions = (options) => {
634
+ select.innerHTML = '';
635
+
636
+ // Add placeholder
637
+ if (this.config.placeholder) {
638
+ const option = document.createElement('option');
639
+ option.value = '';
640
+ option.textContent = this.config.placeholder;
641
+ option.disabled = true;
642
+ option.selected = true;
643
+ select.appendChild(option);
644
+ }
645
+
646
+ // Add options
647
+ if (options && Array.isArray(options)) {
648
+ options.forEach(opt => {
649
+ const option = document.createElement('option');
650
+ if (typeof opt === 'object') {
651
+ option.value = opt.value;
652
+ option.textContent = opt.label || opt.text;
653
+ } else {
654
+ option.value = opt;
655
+ option.textContent = opt;
656
+ }
657
+ select.appendChild(option);
658
+ });
659
+ }
660
+ };
661
+
662
+ // Initial options
663
+ if (this.config.options) {
664
+ renderOptions(this.evaluate(this.config.options));
665
+ }
666
+
667
+ // Auto-fetch options via lookup
668
+ if (this.config.lookup) {
669
+ this.callDZQL({
670
+ operation: 'lookup',
671
+ entity: this.config.lookup.entity,
672
+ params: this.config.lookup.params || {},
673
+ resultPath: `selects.${this.id}.options`
674
+ }).then(() => {
675
+ const options = state.get(`selects.${this.id}.options`);
676
+ renderOptions(options);
677
+ });
678
+ }
679
+
680
+ // Two-way binding
681
+ if (this.config.bind) {
682
+ const path = this.config.bind.replace('${state.', '').replace('}', '');
683
+ select.value = state.get(path) || '';
684
+
685
+ select.addEventListener('change', () => {
686
+ state.set(path, select.value);
687
+ });
688
+
689
+ this.bind(path, (value) => {
690
+ if (select.value !== value) {
691
+ select.value = value || '';
692
+ }
693
+ });
694
+ }
695
+
696
+ // Handle change event
697
+ if (this.config.onChange) {
698
+ select.addEventListener('change', this.handleEvent(this.config.onChange));
699
+ }
700
+
701
+ return select;
702
+ }
703
+ }
704
+
705
+ /**
706
+ * List Component - Renders lists with templates
707
+ */
708
+ class ListComponent extends Component {
709
+ render() {
710
+ const container = document.createElement(this.config.tag || 'ul');
711
+ this.element = container;
712
+
713
+ const renderList = (items) => {
714
+ container.innerHTML = '';
715
+ this.children.forEach(child => child.destroy());
716
+ this.children = [];
717
+
718
+ if (!items || !Array.isArray(items)) return;
719
+
720
+ items.forEach((item, index) => {
721
+ const itemElement = document.createElement(this.config.itemTag || 'li');
722
+
723
+ // Render item template
724
+ if (this.config.template) {
725
+ // Create a temporary state context for the item
726
+ const itemComponent = renderComponent({
727
+ ...this.config.template,
728
+ // Inject item data into template
729
+ context: { item, index }
730
+ }, this.ws);
731
+
732
+ this.children.push(itemComponent);
733
+ itemElement.appendChild(itemComponent.element);
734
+ } else {
735
+ // Simple text rendering
736
+ itemElement.textContent = typeof item === 'object' ? JSON.stringify(item) : item;
737
+ }
738
+
739
+ container.appendChild(itemElement);
740
+ });
741
+ };
742
+
743
+ // Initial render
744
+ if (this.config.items) {
745
+ renderList(this.evaluate(this.config.items));
746
+ }
747
+
748
+ // Auto-fetch if configured
749
+ if (this.config.fetch) {
750
+ this.callDZQL({
751
+ ...this.config.fetch,
752
+ resultPath: `lists.${this.id}`
753
+ }).then(() => {
754
+ const items = state.get(`lists.${this.id}`);
755
+ renderList(items);
756
+ });
757
+ }
758
+
759
+ // Bind to data changes
760
+ if (this.config.itemsPath || this.config.items?.includes('${')) {
761
+ const path = this.config.itemsPath || this.config.items.slice(9, -1);
762
+ this.bind(path, renderList);
763
+ }
764
+
765
+ return container;
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Conditional Component - Renders based on condition
771
+ */
772
+ class ConditionalComponent extends Component {
773
+ render() {
774
+ const container = document.createElement('div');
775
+ this.element = container;
776
+ container.style.display = 'contents'; // Invisible wrapper
777
+
778
+ const updateContent = () => {
779
+ // Clear previous content
780
+ container.innerHTML = '';
781
+ this.children.forEach(child => child.destroy());
782
+ this.children = [];
783
+
784
+ // Evaluate condition
785
+ const condition = this.evaluate(this.config.condition);
786
+
787
+ // Render appropriate branch
788
+ const branch = condition ? this.config.then : this.config.else;
789
+ if (branch) {
790
+ const child = renderComponent(branch, this.ws);
791
+ if (child) {
792
+ this.children.push(child);
793
+ container.appendChild(child.element);
794
+ }
795
+ }
796
+ };
797
+
798
+ updateContent();
799
+
800
+ // Bind to condition changes
801
+ if (typeof this.config.condition === 'string' && this.config.condition.includes('${')) {
802
+ const match = this.config.condition.match(/\${state\.([^}]+)}/);
803
+ if (match) {
804
+ this.bind(match[1], updateContent);
805
+ }
806
+ }
807
+
808
+ return container;
809
+ }
810
+ }
811
+
812
+ // Register built-in components
813
+ componentRegistry.set('container', ContainerComponent);
814
+ componentRegistry.set('div', ContainerComponent);
815
+ componentRegistry.set('section', ContainerComponent);
816
+ componentRegistry.set('text', TextComponent);
817
+ componentRegistry.set('span', TextComponent);
818
+ componentRegistry.set('p', TextComponent);
819
+ componentRegistry.set('h1', TextComponent);
820
+ componentRegistry.set('h2', TextComponent);
821
+ componentRegistry.set('h3', TextComponent);
822
+ componentRegistry.set('input', InputComponent);
823
+ componentRegistry.set('button', ButtonComponent);
824
+ componentRegistry.set('table', TableComponent);
825
+ componentRegistry.set('form', FormComponent);
826
+ componentRegistry.set('select', SelectComponent);
827
+ componentRegistry.set('list', ListComponent);
828
+ componentRegistry.set('if', ConditionalComponent);
829
+
830
+ /**
831
+ * Main render function
832
+ */
833
+ function renderComponent(config, ws) {
834
+ if (!config) return null;
835
+
836
+ const ComponentClass = componentRegistry.get(config.type) || ContainerComponent;
837
+ const component = new ComponentClass(config, ws);
838
+ component.render();
839
+ return component;
840
+ }
841
+
842
+ /**
843
+ * Mount a UI configuration to a DOM element
844
+ */
845
+ export function mount(config, element, ws) {
846
+ // Clear existing content
847
+ element.innerHTML = '';
848
+
849
+ // Render root component
850
+ const root = renderComponent(config, ws);
851
+ if (root) {
852
+ element.appendChild(root.element);
853
+ }
854
+
855
+ return {
856
+ root,
857
+ state,
858
+ destroy: () => root?.destroy()
859
+ };
860
+ }
861
+
862
+ /**
863
+ * Register a custom component
864
+ */
865
+ export function registerComponent(name, ComponentClass) {
866
+ componentRegistry.set(name, ComponentClass);
867
+ }
868
+
869
+ /**
870
+ * Export state manager for external access
871
+ */
872
+ export { state, Component };
873
+
874
+ /**
875
+ * Example UI configuration
876
+ */
877
+ export const exampleUI = {
878
+ type: 'container',
879
+ class: 'app',
880
+ children: [
881
+ {
882
+ type: 'h1',
883
+ text: 'DZQL Declarative UI'
884
+ },
885
+ {
886
+ type: 'section',
887
+ class: 'search-section',
888
+ children: [
889
+ {
890
+ type: 'h2',
891
+ text: 'Search Venues'
892
+ },
893
+ {
894
+ type: 'form',
895
+ dataPath: 'searchForm',
896
+ fields: [
897
+ {
898
+ name: 'search',
899
+ label: 'Search',
900
+ inputType: 'text',
901
+ bind: '${state.searchQuery}',
902
+ attributes: { placeholder: 'Enter search term...' }
903
+ },
904
+ {
905
+ name: 'city',
906
+ label: 'City',
907
+ inputType: 'text',
908
+ bind: '${state.searchCity}'
909
+ }
910
+ ],
911
+ submitButton: {
912
+ text: 'Search',
913
+ onClick: {
914
+ actions: [
915
+ {
916
+ type: 'call',
917
+ operation: 'search',
918
+ entity: 'venues',
919
+ params: {
920
+ filters: {
921
+ _search: '${state.searchQuery}',
922
+ city: '${state.searchCity}'
923
+ },
924
+ limit: 10
925
+ },
926
+ resultPath: 'searchResults'
927
+ }
928
+ ]
929
+ }
930
+ }
931
+ },
932
+ {
933
+ type: 'if',
934
+ condition: '${state.searchResults}',
935
+ then: {
936
+ type: 'div',
937
+ children: [
938
+ {
939
+ type: 'p',
940
+ text: 'Found ${state.searchResults.total} results'
941
+ },
942
+ {
943
+ type: 'table',
944
+ data: '${state.searchResults.data}',
945
+ columns: [
946
+ { field: 'id', label: 'ID' },
947
+ { field: 'name', label: 'Name' },
948
+ { field: 'city', label: 'City' },
949
+ { field: 'capacity', label: 'Capacity' }
950
+ ],
951
+ onRowClick: {
952
+ type: 'setState',
953
+ path: 'selectedVenue',
954
+ value: '${row}'
955
+ }
956
+ }
957
+ ]
958
+ }
959
+ }
960
+ ]
961
+ },
962
+ {
963
+ type: 'if',
964
+ condition: '${state.selectedVenue}',
965
+ then: {
966
+ type: 'section',
967
+ class: 'detail-section',
968
+ children: [
969
+ {
970
+ type: 'h2',
971
+ text: 'Selected Venue: ${state.selectedVenue.name}'
972
+ },
973
+ {
974
+ type: 'button',
975
+ text: 'Edit',
976
+ onClick: {
977
+ actions: [
978
+ {
979
+ type: 'setState',
980
+ path: 'editMode',
981
+ value: true
982
+ }
983
+ ]
984
+ }
985
+ }
986
+ ]
987
+ }
988
+ }
989
+ ]
990
+ };