create-mendix-widget-gleam 2.0.21 → 3.0.0
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 -1
- package/package.json +1 -1
- package/src/i18n.mjs +3 -3
- package/src/templates/claude_md.mjs +4 -3
- package/src/templates/readme_md.mjs +78 -66
- package/src/templates/widgets_readme.mjs +18 -15
- package/template/docs/glendix_guide.md +1050 -2538
- package/template/gleam.toml +4 -1
- package/template/package.json +4 -1
- package/template/src/__widget_name__.gleam +3 -3
- package/template/src/components/hello_world.gleam +5 -5
- package/template/src/editor_config.gleam +4 -8
- package/template/src/editor_preview.gleam +3 -3
|
@@ -1,2538 +1,1050 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
gleam
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
import glendix/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
###
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
[
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
[
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
html.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```gleam
|
|
174
|
-
import
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
attribute.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
attribute.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
```gleam
|
|
672
|
-
import glendix/
|
|
673
|
-
import glendix/
|
|
674
|
-
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
`glendix/
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
import glendix/
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
)
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
let my_component = react.define_component("MyComponent", fn(props) {
|
|
1052
|
-
html.div_([react.text("Hello")])
|
|
1053
|
-
})
|
|
1054
|
-
|
|
1055
|
-
// React.memo (props 동일 시 리렌더 방지)
|
|
1056
|
-
let memoized = react.memo(my_component)
|
|
1057
|
-
|
|
1058
|
-
// 커스텀 비교 함수
|
|
1059
|
-
let memoized = react.memo_(my_component, fn(prev, next) {
|
|
1060
|
-
// True면 리렌더 건너뜀
|
|
1061
|
-
prev == next
|
|
1062
|
-
})
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
#### StrictMode
|
|
1066
|
-
|
|
1067
|
-
```gleam
|
|
1068
|
-
react.strict_mode([
|
|
1069
|
-
// 개발 모드 이중 렌더링 감지
|
|
1070
|
-
my_widget(props),
|
|
1071
|
-
])
|
|
1072
|
-
```
|
|
1073
|
-
|
|
1074
|
-
#### Suspense
|
|
1075
|
-
|
|
1076
|
-
```gleam
|
|
1077
|
-
react.suspense(
|
|
1078
|
-
html.div_([react.text("로딩 중...")]), // fallback
|
|
1079
|
-
[lazy_component], // children
|
|
1080
|
-
)
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
#### Portal
|
|
1084
|
-
|
|
1085
|
-
```gleam
|
|
1086
|
-
// 위젯 DOM 외부에 렌더링 (모달, 팝업)
|
|
1087
|
-
react.portal(modal_element, document_body)
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
#### forwardRef
|
|
1091
|
-
|
|
1092
|
-
```gleam
|
|
1093
|
-
let fancy_input = react.forward_ref(fn(props, ref) {
|
|
1094
|
-
html.input([attribute.ref(ref), attribute.class("fancy")])
|
|
1095
|
-
})
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
#### startTransition / flushSync
|
|
1099
|
-
|
|
1100
|
-
```gleam
|
|
1101
|
-
// 훅 없이 비긴급 업데이트 표시
|
|
1102
|
-
react.start_transition(fn() {
|
|
1103
|
-
set_data(new_data)
|
|
1104
|
-
})
|
|
1105
|
-
|
|
1106
|
-
// 동기 DOM 업데이트 강제 (상태 변경 후 DOM 측정 시 필요) (Round 3)
|
|
1107
|
-
react.flush_sync(fn() {
|
|
1108
|
-
set_count(count + 1)
|
|
1109
|
-
})
|
|
1110
|
-
// 이 시점에 DOM이 이미 업데이트되어 있음
|
|
1111
|
-
```
|
|
1112
|
-
|
|
1113
|
-
#### Profiler
|
|
1114
|
-
|
|
1115
|
-
```gleam
|
|
1116
|
-
react.profiler("MyWidget", fn(id, phase, actual, base, start, commit) {
|
|
1117
|
-
// 렌더링 성능 측정
|
|
1118
|
-
Nil
|
|
1119
|
-
}, [my_widget(props)])
|
|
1120
|
-
```
|
|
1121
|
-
|
|
1122
|
-
#### Context API
|
|
1123
|
-
|
|
1124
|
-
```gleam
|
|
1125
|
-
// Context 생성
|
|
1126
|
-
let theme_ctx = react.create_context("light")
|
|
1127
|
-
|
|
1128
|
-
// Provider로 값 공급
|
|
1129
|
-
react.provider(theme_ctx, "dark", [
|
|
1130
|
-
child_component,
|
|
1131
|
-
])
|
|
1132
|
-
|
|
1133
|
-
// 소비 (hook)
|
|
1134
|
-
let theme = hook.use_context(theme_ctx)
|
|
1135
|
-
```
|
|
1136
|
-
|
|
1137
|
-
---
|
|
1138
|
-
|
|
1139
|
-
## 4. Mendix 바인딩
|
|
1140
|
-
|
|
1141
|
-
### 4.1 Props 접근
|
|
1142
|
-
|
|
1143
|
-
`glendix/mendix` 모듈로 Mendix가 위젯에 전달하는 props에 접근합니다.
|
|
1144
|
-
|
|
1145
|
-
```gleam
|
|
1146
|
-
import glendix/mendix
|
|
1147
|
-
|
|
1148
|
-
// Option 반환 (undefined면 None)
|
|
1149
|
-
case mendix.get_prop(props, "myAttribute") {
|
|
1150
|
-
Some(attr) -> use_attribute(attr)
|
|
1151
|
-
None -> react.none()
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// 항상 존재하는 prop (undefined일 수 없는 경우)
|
|
1155
|
-
let value = mendix.get_prop_required(props, "alwaysPresent")
|
|
1156
|
-
|
|
1157
|
-
// String prop (없으면 빈 문자열 반환)
|
|
1158
|
-
let text = mendix.get_string_prop(props, "caption")
|
|
1159
|
-
|
|
1160
|
-
// prop 존재 여부 확인
|
|
1161
|
-
let has_action = mendix.has_prop(props, "onClick")
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
#### ValueStatus 확인
|
|
1165
|
-
|
|
1166
|
-
Mendix의 모든 동적 값은 상태(status)를 가집니다:
|
|
1167
|
-
|
|
1168
|
-
```gleam
|
|
1169
|
-
import glendix/mendix.{Available, Loading, Unavailable}
|
|
1170
|
-
|
|
1171
|
-
case mendix.get_status(some_value) {
|
|
1172
|
-
Available -> // 값 사용 가능
|
|
1173
|
-
Loading -> // 로딩 중
|
|
1174
|
-
Unavailable -> // 사용 불가
|
|
1175
|
-
}
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
### 4.2 EditableValue — 편집 가능한 값
|
|
1179
|
-
|
|
1180
|
-
`glendix/mendix/editable_value`는 텍스트, 숫자, 날짜 등 편집 가능한 Mendix 속성을 다룹니다.
|
|
1181
|
-
|
|
1182
|
-
```gleam
|
|
1183
|
-
import gleam/option.{None, Some}
|
|
1184
|
-
import glendix/mendix
|
|
1185
|
-
import glendix/mendix/editable_value as ev
|
|
1186
|
-
|
|
1187
|
-
pub fn text_input(props: JsProps) -> ReactElement {
|
|
1188
|
-
case mendix.get_prop(props, "textAttribute") {
|
|
1189
|
-
Some(attr) -> render_input(attr)
|
|
1190
|
-
None -> react.none()
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
fn render_input(attr) -> ReactElement {
|
|
1195
|
-
// 값 읽기
|
|
1196
|
-
let current_value = ev.value(attr) // Option(a)
|
|
1197
|
-
let display = ev.display_value(attr) // String (포맷된 표시값)
|
|
1198
|
-
let is_editable = ev.is_editable(attr) // Bool (Available && !read_only)
|
|
1199
|
-
|
|
1200
|
-
// 유효성 검사 메시지 확인
|
|
1201
|
-
let validation_msg = ev.validation(attr) // Option(String)
|
|
1202
|
-
|
|
1203
|
-
html.div_([
|
|
1204
|
-
html.input([
|
|
1205
|
-
attribute.value(display),
|
|
1206
|
-
attribute.readonly(!is_editable),
|
|
1207
|
-
event.on_change(fn(e) {
|
|
1208
|
-
// 텍스트로 값 설정 (Mendix가 파싱)
|
|
1209
|
-
ev.set_text_value(attr, event.target_value(e))
|
|
1210
|
-
}),
|
|
1211
|
-
]),
|
|
1212
|
-
// 유효성 검사 에러 표시
|
|
1213
|
-
react.when_some(validation_msg, fn(msg) {
|
|
1214
|
-
html.span([attribute.class("text-danger")], [
|
|
1215
|
-
react.text(msg),
|
|
1216
|
-
])
|
|
1217
|
-
}),
|
|
1218
|
-
])
|
|
1219
|
-
}
|
|
1220
|
-
```
|
|
1221
|
-
|
|
1222
|
-
#### 값 설정 방법
|
|
1223
|
-
|
|
1224
|
-
```gleam
|
|
1225
|
-
// Option으로 직접 설정 (타입이 맞아야 함)
|
|
1226
|
-
ev.set_value(attr, Some(new_value)) // 값 설정
|
|
1227
|
-
ev.set_value(attr, None) // 값 비우기
|
|
1228
|
-
|
|
1229
|
-
// 텍스트로 설정 (Mendix가 자동 파싱 — 숫자, 날짜 등에 유용)
|
|
1230
|
-
ev.set_text_value(attr, "2024-01-15")
|
|
1231
|
-
|
|
1232
|
-
// 커스텀 유효성 검사 함수 설정
|
|
1233
|
-
ev.set_validator(attr, Some(fn(value) {
|
|
1234
|
-
case value {
|
|
1235
|
-
Some(v) if v == "" -> Some("값을 입력하세요")
|
|
1236
|
-
_ -> None // 유효함
|
|
1237
|
-
}
|
|
1238
|
-
}))
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
#### 가능한 값 목록 (Enum, Boolean 등)
|
|
1242
|
-
|
|
1243
|
-
```gleam
|
|
1244
|
-
case ev.universe(attr) {
|
|
1245
|
-
Some(options) ->
|
|
1246
|
-
// options: List(a) — 선택 가능한 모든 값
|
|
1247
|
-
html.select_(
|
|
1248
|
-
list.map(options, fn(opt) {
|
|
1249
|
-
html.option_([react.text(string.inspect(opt))])
|
|
1250
|
-
}),
|
|
1251
|
-
)
|
|
1252
|
-
None -> react.none()
|
|
1253
|
-
}
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
### 4.3 ActionValue — 액션 실행
|
|
1257
|
-
|
|
1258
|
-
`glendix/mendix/action`으로 Mendix 마이크로플로우/나노플로우를 실행합니다.
|
|
1259
|
-
|
|
1260
|
-
```gleam
|
|
1261
|
-
import glendix/mendix
|
|
1262
|
-
import glendix/mendix/action
|
|
1263
|
-
|
|
1264
|
-
pub fn action_button(props: JsProps) -> ReactElement {
|
|
1265
|
-
let on_click = mendix.get_prop(props, "onClick") // Option(ActionValue)
|
|
1266
|
-
|
|
1267
|
-
html.button(
|
|
1268
|
-
[
|
|
1269
|
-
attribute.class("btn"),
|
|
1270
|
-
attribute.disabled(case on_click {
|
|
1271
|
-
Some(a) -> !action.can_execute(a)
|
|
1272
|
-
None -> True
|
|
1273
|
-
}),
|
|
1274
|
-
event.on_click(fn(_) {
|
|
1275
|
-
// Option(ActionValue) 안전하게 실행
|
|
1276
|
-
action.execute_action(on_click)
|
|
1277
|
-
}),
|
|
1278
|
-
],
|
|
1279
|
-
[react.text("실행")],
|
|
1280
|
-
)
|
|
1281
|
-
}
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
#### 액션 실행 방법
|
|
1285
|
-
|
|
1286
|
-
```gleam
|
|
1287
|
-
// 직접 실행 (can_execute 확인 없이)
|
|
1288
|
-
action.execute(my_action)
|
|
1289
|
-
|
|
1290
|
-
// can_execute가 True일 때만 실행
|
|
1291
|
-
action.execute_if_can(my_action)
|
|
1292
|
-
|
|
1293
|
-
// Option(ActionValue)에서 안전하게 실행
|
|
1294
|
-
action.execute_action(maybe_action) // None이면 아무것도 안 함
|
|
1295
|
-
|
|
1296
|
-
// 실행 상태 확인
|
|
1297
|
-
let can = action.can_execute(my_action) // Bool
|
|
1298
|
-
let running = action.is_executing(my_action) // Bool
|
|
1299
|
-
```
|
|
1300
|
-
|
|
1301
|
-
### 4.4 DynamicValue — 읽기 전용 표현식
|
|
1302
|
-
|
|
1303
|
-
`glendix/mendix/dynamic_value`는 Mendix 표현식(Expression) 속성을 다룹니다.
|
|
1304
|
-
|
|
1305
|
-
```gleam
|
|
1306
|
-
import glendix/mendix/dynamic_value as dv
|
|
1307
|
-
|
|
1308
|
-
pub fn display_expression(props: JsProps) -> ReactElement {
|
|
1309
|
-
case mendix.get_prop(props, "expression") {
|
|
1310
|
-
Some(expr) ->
|
|
1311
|
-
case dv.value(expr) {
|
|
1312
|
-
Some(text) -> html.span_([react.text(text)])
|
|
1313
|
-
None -> react.none()
|
|
1314
|
-
}
|
|
1315
|
-
None -> react.none()
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// 상태 확인
|
|
1320
|
-
let status = dv.status(expr)
|
|
1321
|
-
let ready = dv.is_available(expr)
|
|
1322
|
-
```
|
|
1323
|
-
|
|
1324
|
-
### 4.5 ListValue — 리스트 데이터
|
|
1325
|
-
|
|
1326
|
-
`glendix/mendix/list_value`는 Mendix 데이터 소스 리스트를 다룹니다.
|
|
1327
|
-
|
|
1328
|
-
```gleam
|
|
1329
|
-
import glendix/mendix
|
|
1330
|
-
import glendix/mendix/list_value as lv
|
|
1331
|
-
|
|
1332
|
-
pub fn data_list(props: JsProps) -> ReactElement {
|
|
1333
|
-
case mendix.get_prop(props, "dataSource") {
|
|
1334
|
-
Some(list_val) -> render_list(list_val, props)
|
|
1335
|
-
None -> react.none()
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
fn render_list(list_val, props) -> ReactElement {
|
|
1340
|
-
case lv.items(list_val) {
|
|
1341
|
-
Some(items) ->
|
|
1342
|
-
html.ul_(
|
|
1343
|
-
list.map(items, fn(item) {
|
|
1344
|
-
let id = mendix.object_id(item)
|
|
1345
|
-
html.li([attribute.key(id)], [
|
|
1346
|
-
react.text("Item: " <> id),
|
|
1347
|
-
])
|
|
1348
|
-
}),
|
|
1349
|
-
)
|
|
1350
|
-
None ->
|
|
1351
|
-
html.div_([react.text("로딩 중...")])
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
```
|
|
1355
|
-
|
|
1356
|
-
#### 페이지네이션
|
|
1357
|
-
|
|
1358
|
-
```gleam
|
|
1359
|
-
// 현재 페이지 정보
|
|
1360
|
-
let offset = lv.offset(list_val) // 현재 오프셋
|
|
1361
|
-
let limit = lv.limit(list_val) // 페이지 크기
|
|
1362
|
-
let has_more = lv.has_more_items(list_val) // Option(Bool)
|
|
1363
|
-
|
|
1364
|
-
// 페이지 이동
|
|
1365
|
-
lv.set_offset(list_val, offset + limit) // 다음 페이지
|
|
1366
|
-
lv.set_limit(list_val, 20) // 페이지 크기 변경
|
|
1367
|
-
|
|
1368
|
-
// 전체 개수 요청 (성능 고려)
|
|
1369
|
-
lv.request_total_count(list_val, True)
|
|
1370
|
-
let total = lv.total_count(list_val) // Option(Int)
|
|
1371
|
-
```
|
|
1372
|
-
|
|
1373
|
-
#### 정렬
|
|
1374
|
-
|
|
1375
|
-
```gleam
|
|
1376
|
-
import glendix/mendix/list_value as lv
|
|
1377
|
-
|
|
1378
|
-
// 정렬 적용
|
|
1379
|
-
lv.set_sort_order(list_val, [
|
|
1380
|
-
lv.sort("Name", lv.Asc),
|
|
1381
|
-
lv.sort("CreatedDate", lv.Desc),
|
|
1382
|
-
])
|
|
1383
|
-
|
|
1384
|
-
// 현재 정렬 확인
|
|
1385
|
-
let current_sort = lv.sort_order(list_val)
|
|
1386
|
-
```
|
|
1387
|
-
|
|
1388
|
-
#### 데이터 갱신
|
|
1389
|
-
|
|
1390
|
-
```gleam
|
|
1391
|
-
lv.reload(list_val) // 데이터 다시 로드
|
|
1392
|
-
```
|
|
1393
|
-
|
|
1394
|
-
### 4.6 ListAttribute — 리스트 아이템 접근
|
|
1395
|
-
|
|
1396
|
-
`glendix/mendix/list_attribute`는 리스트의 각 아이템에서 속성, 액션, 표현식, 위젯을 추출합니다.
|
|
1397
|
-
|
|
1398
|
-
```gleam
|
|
1399
|
-
import glendix/mendix/list_attribute as la
|
|
1400
|
-
|
|
1401
|
-
pub fn render_table(props: JsProps) -> ReactElement {
|
|
1402
|
-
let list_val = mendix.get_prop_required(props, "dataSource")
|
|
1403
|
-
let name_attr = mendix.get_prop_required(props, "nameAttr")
|
|
1404
|
-
let edit_action = mendix.get_prop(props, "onEdit")
|
|
1405
|
-
|
|
1406
|
-
case lv.items(list_val) {
|
|
1407
|
-
Some(items) ->
|
|
1408
|
-
html.table_([
|
|
1409
|
-
html.tbody_(
|
|
1410
|
-
list.map(items, fn(item) {
|
|
1411
|
-
let id = mendix.object_id(item)
|
|
1412
|
-
|
|
1413
|
-
// 아이템에서 속성값 추출
|
|
1414
|
-
let name_ev = la.get_attribute(name_attr, item)
|
|
1415
|
-
let display = ev.display_value(name_ev)
|
|
1416
|
-
|
|
1417
|
-
// 아이템에서 액션 추출
|
|
1418
|
-
let action_opt = case edit_action {
|
|
1419
|
-
Some(act) -> la.get_action(act, item)
|
|
1420
|
-
None -> None
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
html.tr([attribute.key(id)], [
|
|
1424
|
-
html.td_([react.text(display)]),
|
|
1425
|
-
html.td_([
|
|
1426
|
-
html.button(
|
|
1427
|
-
[event.on_click(fn(_) {
|
|
1428
|
-
action.execute_action(action_opt)
|
|
1429
|
-
})],
|
|
1430
|
-
[react.text("편집")],
|
|
1431
|
-
),
|
|
1432
|
-
]),
|
|
1433
|
-
])
|
|
1434
|
-
}),
|
|
1435
|
-
),
|
|
1436
|
-
])
|
|
1437
|
-
None -> html.div_([react.text("로딩 중...")])
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
```
|
|
1441
|
-
|
|
1442
|
-
#### ListAttributeValue 메타데이터
|
|
1443
|
-
|
|
1444
|
-
```gleam
|
|
1445
|
-
// 속성 정보 확인
|
|
1446
|
-
let id = la.attr_id(name_attr) // String - 속성 ID
|
|
1447
|
-
let sortable = la.attr_sortable(name_attr) // Bool
|
|
1448
|
-
let filterable = la.attr_filterable(name_attr) // Bool
|
|
1449
|
-
let type_name = la.attr_type(name_attr) // "String", "Integer" 등
|
|
1450
|
-
let formatter = la.attr_formatter(name_attr) // ValueFormatter
|
|
1451
|
-
```
|
|
1452
|
-
|
|
1453
|
-
#### 위젯 렌더링
|
|
1454
|
-
|
|
1455
|
-
```gleam
|
|
1456
|
-
// 리스트 아이템별 위젯 (Mendix Studio에서 구성)
|
|
1457
|
-
let content_widget = mendix.get_prop_required(props, "content")
|
|
1458
|
-
|
|
1459
|
-
list.map(items, fn(item) {
|
|
1460
|
-
let widget_element = la.get_widget(content_widget, item)
|
|
1461
|
-
html.div([attribute.key(mendix.object_id(item))], [
|
|
1462
|
-
widget_element, // ReactElement로 직접 사용
|
|
1463
|
-
])
|
|
1464
|
-
})
|
|
1465
|
-
```
|
|
1466
|
-
|
|
1467
|
-
### 4.7 Selection — 선택
|
|
1468
|
-
|
|
1469
|
-
`glendix/mendix/selection`으로 단일/다중 선택을 관리합니다.
|
|
1470
|
-
|
|
1471
|
-
#### 단일 선택
|
|
1472
|
-
|
|
1473
|
-
```gleam
|
|
1474
|
-
import glendix/mendix/selection
|
|
1475
|
-
|
|
1476
|
-
// 현재 선택된 항목
|
|
1477
|
-
let selected = selection.selection(single_sel) // Option(ObjectItem)
|
|
1478
|
-
|
|
1479
|
-
// 선택 설정/해제
|
|
1480
|
-
selection.set_selection(single_sel, Some(item)) // 선택
|
|
1481
|
-
selection.set_selection(single_sel, None) // 선택 해제
|
|
1482
|
-
```
|
|
1483
|
-
|
|
1484
|
-
#### 다중 선택
|
|
1485
|
-
|
|
1486
|
-
```gleam
|
|
1487
|
-
// 선택된 항목들
|
|
1488
|
-
let selected_items = selection.selections(multi_sel) // List(ObjectItem)
|
|
1489
|
-
|
|
1490
|
-
// 선택 설정
|
|
1491
|
-
selection.set_selections(multi_sel, [item1, item2])
|
|
1492
|
-
```
|
|
1493
|
-
|
|
1494
|
-
### 4.8 Reference — 연관 관계
|
|
1495
|
-
|
|
1496
|
-
`glendix/mendix/reference`로 단일 연관 관계, `glendix/mendix/reference_set`으로 다중 연관 관계를 다룹니다.
|
|
1497
|
-
|
|
1498
|
-
```gleam
|
|
1499
|
-
import glendix/mendix/reference as ref
|
|
1500
|
-
import glendix/mendix/reference_set as ref_set
|
|
1501
|
-
|
|
1502
|
-
// 단일 참조 (1:1, N:1)
|
|
1503
|
-
let referenced = ref.value(my_ref) // Option(a)
|
|
1504
|
-
let is_readonly = ref.read_only(my_ref) // Bool
|
|
1505
|
-
let error = ref.validation(my_ref) // Option(String)
|
|
1506
|
-
|
|
1507
|
-
ref.set_value(my_ref, Some(new_item)) // 참조 설정
|
|
1508
|
-
ref.set_value(my_ref, None) // 참조 해제
|
|
1509
|
-
|
|
1510
|
-
// 다중 참조 (M:N)
|
|
1511
|
-
let items = ref_set.value(my_ref_set) // Option(List(a))
|
|
1512
|
-
ref_set.set_value(my_ref_set, Some([item1, item2]))
|
|
1513
|
-
```
|
|
1514
|
-
|
|
1515
|
-
### 4.9 Filter — 필터 조건 빌더
|
|
1516
|
-
|
|
1517
|
-
`glendix/mendix/filter`로 ListValue에 적용할 필터 조건을 프로그래밍 방식으로 구성합니다.
|
|
1518
|
-
|
|
1519
|
-
```gleam
|
|
1520
|
-
import glendix/mendix/filter
|
|
1521
|
-
import glendix/mendix/list_value as lv
|
|
1522
|
-
|
|
1523
|
-
// 단순 비교
|
|
1524
|
-
let name_filter =
|
|
1525
|
-
filter.contains(
|
|
1526
|
-
filter.attribute("Name"),
|
|
1527
|
-
filter.literal("검색어"),
|
|
1528
|
-
)
|
|
1529
|
-
|
|
1530
|
-
// 복합 조건 (AND)
|
|
1531
|
-
let complex_filter =
|
|
1532
|
-
filter.and_([
|
|
1533
|
-
filter.equals(
|
|
1534
|
-
filter.attribute("Status"),
|
|
1535
|
-
filter.literal("Active"),
|
|
1536
|
-
),
|
|
1537
|
-
filter.greater_than(
|
|
1538
|
-
filter.attribute("Amount"),
|
|
1539
|
-
filter.literal(100),
|
|
1540
|
-
),
|
|
1541
|
-
])
|
|
1542
|
-
|
|
1543
|
-
// 필터 적용
|
|
1544
|
-
lv.set_filter(list_val, Some(complex_filter))
|
|
1545
|
-
|
|
1546
|
-
// 필터 해제
|
|
1547
|
-
lv.set_filter(list_val, None)
|
|
1548
|
-
```
|
|
1549
|
-
|
|
1550
|
-
#### 사용 가능한 비교 연산자
|
|
1551
|
-
|
|
1552
|
-
| 함수 | 설명 |
|
|
1553
|
-
|---|---|
|
|
1554
|
-
| `equals(a, b)` | 같음 |
|
|
1555
|
-
| `not_equal(a, b)` | 다름 |
|
|
1556
|
-
| `greater_than(a, b)` | 초과 |
|
|
1557
|
-
| `greater_than_or_equal(a, b)` | 이상 |
|
|
1558
|
-
| `less_than(a, b)` | 미만 |
|
|
1559
|
-
| `less_than_or_equal(a, b)` | 이하 |
|
|
1560
|
-
| `contains(a, b)` | 포함 (문자열) |
|
|
1561
|
-
| `starts_with(a, b)` | 시작 (문자열) |
|
|
1562
|
-
| `ends_with(a, b)` | 끝 (문자열) |
|
|
1563
|
-
|
|
1564
|
-
#### 날짜 비교
|
|
1565
|
-
|
|
1566
|
-
```gleam
|
|
1567
|
-
filter.day_equals(filter.attribute("Birthday"), filter.literal(date))
|
|
1568
|
-
filter.day_greater_than(filter.attribute("CreatedDate"), filter.literal(start_date))
|
|
1569
|
-
```
|
|
1570
|
-
|
|
1571
|
-
#### 논리 조합
|
|
1572
|
-
|
|
1573
|
-
```gleam
|
|
1574
|
-
filter.and_([condition1, condition2]) // AND
|
|
1575
|
-
filter.or_([condition1, condition2]) // OR
|
|
1576
|
-
filter.not_(condition) // NOT
|
|
1577
|
-
```
|
|
1578
|
-
|
|
1579
|
-
#### 표현식 타입
|
|
1580
|
-
|
|
1581
|
-
```gleam
|
|
1582
|
-
filter.attribute("AttrName") // 속성 참조
|
|
1583
|
-
filter.association("AssocName") // 연관 관계 참조
|
|
1584
|
-
filter.literal(value) // 상수 값
|
|
1585
|
-
filter.empty() // 빈 값 (null 비교용)
|
|
1586
|
-
```
|
|
1587
|
-
|
|
1588
|
-
### 4.10 날짜와 숫자
|
|
1589
|
-
|
|
1590
|
-
#### JsDate — 날짜 처리
|
|
1591
|
-
|
|
1592
|
-
`glendix/mendix/date`는 JavaScript Date를 Gleam에서 안전하게 다룹니다.
|
|
1593
|
-
|
|
1594
|
-
> 핵심: Gleam에서 월(month)은 **1-based** (1~12), JavaScript에서는 0-based (0~11). glendix가 자동 변환합니다.
|
|
1595
|
-
|
|
1596
|
-
```gleam
|
|
1597
|
-
import glendix/mendix/date
|
|
1598
|
-
|
|
1599
|
-
// 생성
|
|
1600
|
-
let now = date.now()
|
|
1601
|
-
let parsed = date.from_iso("2024-03-15T10:30:00Z")
|
|
1602
|
-
let custom = date.create(2024, 3, 15, 10, 30, 0, 0) // 월: 1-12!
|
|
1603
|
-
let from_ts = date.from_timestamp(1710500000000)
|
|
1604
|
-
|
|
1605
|
-
// 읽기
|
|
1606
|
-
let year = date.year(now) // 예: 2024
|
|
1607
|
-
let month = date.month(now) // 1~12 (자동 변환!)
|
|
1608
|
-
let day = date.day(now) // 1~31
|
|
1609
|
-
let hours = date.hours(now) // 0~23
|
|
1610
|
-
let dow = date.day_of_week(now) // 0=일요일
|
|
1611
|
-
|
|
1612
|
-
// 변환
|
|
1613
|
-
let iso = date.to_iso(now) // "2024-03-15T10:30:00.000Z"
|
|
1614
|
-
let ts = date.to_timestamp(now) // Unix 밀리초
|
|
1615
|
-
let str = date.to_string(now) // 사람이 읽을 수 있는 형식
|
|
1616
|
-
let input_val = date.to_input_value(now) // "2024-03-15" (input[type="date"]용)
|
|
1617
|
-
|
|
1618
|
-
// input[type="date"]에서 파싱
|
|
1619
|
-
let maybe_date = date.from_input_value("2024-03-15") // Option(JsDate)
|
|
1620
|
-
```
|
|
1621
|
-
|
|
1622
|
-
#### Big — 고정밀 십진수
|
|
1623
|
-
|
|
1624
|
-
`glendix/mendix/big`는 Big.js를 래핑하여 Mendix의 Decimal 타입을 정밀하게 처리합니다.
|
|
1625
|
-
|
|
1626
|
-
```gleam
|
|
1627
|
-
import glendix/mendix/big
|
|
1628
|
-
import gleam/order
|
|
1629
|
-
|
|
1630
|
-
// 생성
|
|
1631
|
-
let a = big.from_string("123.456")
|
|
1632
|
-
let b = big.from_int(100)
|
|
1633
|
-
let c = big.from_float(99.99)
|
|
1634
|
-
|
|
1635
|
-
// 연산
|
|
1636
|
-
let sum = big.add(a, b) // 223.456
|
|
1637
|
-
let diff = big.subtract(a, b) // 23.456
|
|
1638
|
-
let prod = big.multiply(a, b) // 12345.6
|
|
1639
|
-
let quot = big.divide(a, b) // 1.23456
|
|
1640
|
-
let abs = big.absolute(diff) // 양수화
|
|
1641
|
-
let neg = big.negate(a) // -123.456
|
|
1642
|
-
|
|
1643
|
-
// 비교
|
|
1644
|
-
let cmp = big.compare(a, b) // order.Gt
|
|
1645
|
-
let eq = big.equal(a, b) // False
|
|
1646
|
-
|
|
1647
|
-
// 변환
|
|
1648
|
-
let str = big.to_string(sum) // "223.456"
|
|
1649
|
-
let f = big.to_float(sum) // 223.456
|
|
1650
|
-
let i = big.to_int(sum) // 223 (소수점 버림)
|
|
1651
|
-
let fixed = big.to_fixed(sum, 2) // "223.46"
|
|
1652
|
-
```
|
|
1653
|
-
|
|
1654
|
-
### 4.11 파일, 아이콘, 포맷터
|
|
1655
|
-
|
|
1656
|
-
#### FileValue / WebImage
|
|
1657
|
-
|
|
1658
|
-
```gleam
|
|
1659
|
-
import glendix/mendix/file
|
|
1660
|
-
|
|
1661
|
-
// FileValue
|
|
1662
|
-
let uri = file.uri(file_val) // String - 파일 URI
|
|
1663
|
-
let name = file.name(file_val) // Option(String) - 파일명
|
|
1664
|
-
|
|
1665
|
-
// WebImage (FileValue + alt 텍스트)
|
|
1666
|
-
let src = file.image_uri(img) // String
|
|
1667
|
-
let alt = file.alt_text(img) // Option(String)
|
|
1668
|
-
|
|
1669
|
-
html.img([
|
|
1670
|
-
attribute.src(src),
|
|
1671
|
-
attribute.alt(option.unwrap(alt, "")),
|
|
1672
|
-
])
|
|
1673
|
-
```
|
|
1674
|
-
|
|
1675
|
-
#### WebIcon
|
|
1676
|
-
|
|
1677
|
-
```gleam
|
|
1678
|
-
import glendix/mendix/icon
|
|
1679
|
-
|
|
1680
|
-
case icon.icon_type(my_icon) {
|
|
1681
|
-
icon.Glyph ->
|
|
1682
|
-
html.span([attribute.class(icon.icon_class(my_icon))], [])
|
|
1683
|
-
icon.Image ->
|
|
1684
|
-
html.img([attribute.src(icon.icon_url(my_icon))])
|
|
1685
|
-
icon.IconFont ->
|
|
1686
|
-
html.span([attribute.class(icon.icon_class(my_icon))], [])
|
|
1687
|
-
}
|
|
1688
|
-
```
|
|
1689
|
-
|
|
1690
|
-
#### ValueFormatter
|
|
1691
|
-
|
|
1692
|
-
```gleam
|
|
1693
|
-
import glendix/mendix/formatter
|
|
1694
|
-
|
|
1695
|
-
// 값을 문자열로 포맷
|
|
1696
|
-
let display = formatter.format(fmt, Some(value)) // String
|
|
1697
|
-
let empty = formatter.format(fmt, None) // ""
|
|
1698
|
-
|
|
1699
|
-
// 텍스트를 값으로 파싱
|
|
1700
|
-
case formatter.parse(fmt, "123.45") {
|
|
1701
|
-
Ok(Some(value)) -> // 파싱 성공
|
|
1702
|
-
Ok(None) -> // 빈 값
|
|
1703
|
-
Error(Nil) -> // 파싱 실패
|
|
1704
|
-
}
|
|
1705
|
-
```
|
|
1706
|
-
|
|
1707
|
-
### 4.12 Editor Configuration — 조건부 속성 제어
|
|
1708
|
-
|
|
1709
|
-
Studio Pro의 editorConfig에서 속성을 조건부로 숨기거나, 그룹을 탭으로 변환하는 등의 작업을 순수 Gleam으로 작성할 수 있습니다. `@mendix/pluggable-widgets-tools`의 헬퍼 함수를 래핑합니다.
|
|
1710
|
-
|
|
1711
|
-
> **Jint 호환성**: Studio Pro는 editorConfig를 **Jint**(.NET JavaScript 엔진)로 실행합니다. Jint는 Gleam 컴파일러가 생성하는 `List` 런타임(WeakMap, Symbol.iterator, class inheritance)을 지원하지 않으므로, 이 모듈의 모든 함수는 `List` 타입을 사용하지 않습니다. 여러 키를 전달할 때는 **콤마 구분 String**을 사용합니다.
|
|
1712
|
-
|
|
1713
|
-
#### Properties 타입
|
|
1714
|
-
|
|
1715
|
-
`Properties`는 Studio Pro가 `getProperties`에 전달하는 `PropertyGroup[]` 배열의 opaque 래퍼입니다. 모든 함수가 `Properties`를 반환하므로 파이프라인 체이닝이 가능합니다.
|
|
1716
|
-
|
|
1717
|
-
#### 속성 숨기기
|
|
1718
|
-
|
|
1719
|
-
```gleam
|
|
1720
|
-
import glendix/editor_config.{type Properties}
|
|
1721
|
-
|
|
1722
|
-
// 단일 속성 숨기기
|
|
1723
|
-
let props = editor_config.hide_property(default_properties, "barWidth")
|
|
1724
|
-
|
|
1725
|
-
// 여러 속성 한 번에 숨기기 (콤마 구분 String)
|
|
1726
|
-
let props = editor_config.hide_properties(default_properties, "barWidth,barColor")
|
|
1727
|
-
|
|
1728
|
-
// 중첩 속성 숨기기 (배열 타입 속성의 특정 인덱스 내부)
|
|
1729
|
-
let props = editor_config.hide_nested_property(default_properties, "columns", 0, "width")
|
|
1730
|
-
|
|
1731
|
-
// 여러 중첩 속성 한 번에 숨기기 (콤마 구분 String)
|
|
1732
|
-
let props = editor_config.hide_nested_properties(default_properties, "columns", 0, "width,alignment")
|
|
1733
|
-
```
|
|
1734
|
-
|
|
1735
|
-
#### 탭 변환 / 속성 순서 변경
|
|
1736
|
-
|
|
1737
|
-
```gleam
|
|
1738
|
-
// 속성 그룹을 탭으로 변환 (웹 플랫폼용)
|
|
1739
|
-
let props = editor_config.transform_groups_into_tabs(default_properties)
|
|
1740
|
-
|
|
1741
|
-
// 속성 순서 변경 (from_index → to_index)
|
|
1742
|
-
let props = editor_config.move_property(default_properties, 0, 2)
|
|
1743
|
-
```
|
|
1744
|
-
|
|
1745
|
-
#### 실전 예시 — 차트 유형별 조건부 속성
|
|
1746
|
-
|
|
1747
|
-
사용자의 `src/editor_config.gleam`에서 `getProperties` 로직을 작성합니다. 이 파일이 존재하면 `run_with_bridge` 실행 시 editorConfig 브릿지 JS가 자동 생성됩니다.
|
|
1748
|
-
|
|
1749
|
-
> **주의**: editorConfig 코드에서는 Gleam `List`를 사용하지 마세요. `["a", "b"]` 같은 리스트 리터럴은 Gleam List 런타임 클래스를 번들에 포함시켜 Jint에서 크래시를 일으킵니다. 여러 키를 조합할 때는 `const`와 String 연결(`<>`)을 사용하세요.
|
|
1750
|
-
|
|
1751
|
-
```gleam
|
|
1752
|
-
import glendix/editor_config.{type Properties}
|
|
1753
|
-
import glendix/mendix
|
|
1754
|
-
import glendix/react.{type JsProps}
|
|
1755
|
-
|
|
1756
|
-
const bar_keys = "barWidth,barColor"
|
|
1757
|
-
|
|
1758
|
-
const line_keys = "lineStyle,lineCurve"
|
|
1759
|
-
|
|
1760
|
-
pub fn get_properties(
|
|
1761
|
-
values: JsProps,
|
|
1762
|
-
default_properties: Properties,
|
|
1763
|
-
platform: String,
|
|
1764
|
-
) -> Properties {
|
|
1765
|
-
let chart_type = mendix.get_string_prop(values, "chartType")
|
|
1766
|
-
|
|
1767
|
-
let props = case chart_type {
|
|
1768
|
-
"line" ->
|
|
1769
|
-
default_properties
|
|
1770
|
-
|> editor_config.hide_properties(bar_keys)
|
|
1771
|
-
"bar" ->
|
|
1772
|
-
default_properties
|
|
1773
|
-
|> editor_config.hide_properties(line_keys)
|
|
1774
|
-
_ -> default_properties
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
case platform {
|
|
1778
|
-
"web" -> editor_config.transform_groups_into_tabs(props)
|
|
1779
|
-
_ -> props
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
```
|
|
1783
|
-
|
|
1784
|
-
#### 함수 요약
|
|
1785
|
-
|
|
1786
|
-
| 함수 | 설명 |
|
|
1787
|
-
|------|------|
|
|
1788
|
-
| `hide_property(properties, key)` | 단일 속성 숨기기 |
|
|
1789
|
-
| `hide_properties(properties, keys)` | 여러 속성 숨기기 (콤마 구분 String) |
|
|
1790
|
-
| `hide_nested_property(properties, key, index, nested_key)` | 중첩 속성 숨기기 |
|
|
1791
|
-
| `hide_nested_properties(properties, key, index, nested_keys)` | 여러 중첩 속성 숨기기 (콤마 구분 String) |
|
|
1792
|
-
| `transform_groups_into_tabs(properties)` | 그룹 → 탭 변환 |
|
|
1793
|
-
| `move_property(properties, from_index, to_index)` | 속성 순서 변경 |
|
|
1794
|
-
|
|
1795
|
-
---
|
|
1796
|
-
|
|
1797
|
-
## 5. 실전 패턴
|
|
1798
|
-
|
|
1799
|
-
### 5.1 폼 입력 위젯
|
|
1800
|
-
|
|
1801
|
-
```gleam
|
|
1802
|
-
import gleam/option.{None, Some}
|
|
1803
|
-
import glendix/mendix
|
|
1804
|
-
import glendix/mendix/action
|
|
1805
|
-
import glendix/mendix/editable_value as ev
|
|
1806
|
-
import glendix/react.{type JsProps, type ReactElement}
|
|
1807
|
-
import glendix/react/attribute
|
|
1808
|
-
import glendix/react/event
|
|
1809
|
-
import glendix/react/hook
|
|
1810
|
-
import glendix/react/html
|
|
1811
|
-
|
|
1812
|
-
pub fn text_input_widget(props: JsProps) -> ReactElement {
|
|
1813
|
-
let attr = mendix.get_prop(props, "textAttribute")
|
|
1814
|
-
let on_enter = mendix.get_prop(props, "onEnterAction")
|
|
1815
|
-
let placeholder = mendix.get_string_prop(props, "placeholder")
|
|
1816
|
-
|
|
1817
|
-
case attr {
|
|
1818
|
-
Some(text_attr) -> {
|
|
1819
|
-
let display = ev.display_value(text_attr)
|
|
1820
|
-
let editable = ev.is_editable(text_attr)
|
|
1821
|
-
let validation = ev.validation(text_attr)
|
|
1822
|
-
|
|
1823
|
-
html.div([attribute.class("form-group")], [
|
|
1824
|
-
html.input([
|
|
1825
|
-
attribute.class("form-control"),
|
|
1826
|
-
attribute.value(display),
|
|
1827
|
-
attribute.placeholder(placeholder),
|
|
1828
|
-
attribute.readonly(!editable),
|
|
1829
|
-
event.on_change(fn(e) {
|
|
1830
|
-
ev.set_text_value(text_attr, event.target_value(e))
|
|
1831
|
-
}),
|
|
1832
|
-
event.on_key_down(fn(e) {
|
|
1833
|
-
case event.key(e) {
|
|
1834
|
-
"Enter" -> action.execute_action(on_enter)
|
|
1835
|
-
_ -> Nil
|
|
1836
|
-
}
|
|
1837
|
-
}),
|
|
1838
|
-
]),
|
|
1839
|
-
// 유효성 검사 메시지
|
|
1840
|
-
react.when_some(validation, fn(msg) {
|
|
1841
|
-
html.div([attribute.class("alert alert-danger")], [
|
|
1842
|
-
react.text(msg),
|
|
1843
|
-
])
|
|
1844
|
-
}),
|
|
1845
|
-
])
|
|
1846
|
-
}
|
|
1847
|
-
None -> react.none()
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
```
|
|
1851
|
-
|
|
1852
|
-
### 5.2 데이터 테이블 위젯
|
|
1853
|
-
|
|
1854
|
-
```gleam
|
|
1855
|
-
import gleam/int
|
|
1856
|
-
import gleam/list
|
|
1857
|
-
import gleam/option.{None, Some}
|
|
1858
|
-
import glendix/mendix
|
|
1859
|
-
import glendix/mendix/editable_value as ev
|
|
1860
|
-
import glendix/mendix/list_attribute as la
|
|
1861
|
-
import glendix/mendix/list_value as lv
|
|
1862
|
-
import glendix/react.{type JsProps, type ReactElement}
|
|
1863
|
-
import glendix/react/attribute
|
|
1864
|
-
import glendix/react/event
|
|
1865
|
-
import glendix/react/html
|
|
1866
|
-
|
|
1867
|
-
pub fn data_table(props: JsProps) -> ReactElement {
|
|
1868
|
-
let ds = mendix.get_prop_required(props, "dataSource")
|
|
1869
|
-
let col_name = mendix.get_prop_required(props, "nameColumn")
|
|
1870
|
-
let col_status = mendix.get_prop_required(props, "statusColumn")
|
|
1871
|
-
|
|
1872
|
-
html.div([attribute.class("table-responsive")], [
|
|
1873
|
-
html.table([attribute.class("table table-striped")], [
|
|
1874
|
-
// 헤더
|
|
1875
|
-
html.thead_([
|
|
1876
|
-
html.tr_([
|
|
1877
|
-
html.th_([react.text("이름")]),
|
|
1878
|
-
html.th_([react.text("상태")]),
|
|
1879
|
-
]),
|
|
1880
|
-
]),
|
|
1881
|
-
// 바디
|
|
1882
|
-
html.tbody_(
|
|
1883
|
-
case lv.items(ds) {
|
|
1884
|
-
Some(items) ->
|
|
1885
|
-
list.map(items, fn(item) {
|
|
1886
|
-
let id = mendix.object_id(item)
|
|
1887
|
-
let name = ev.display_value(la.get_attribute(col_name, item))
|
|
1888
|
-
let status = ev.display_value(la.get_attribute(col_status, item))
|
|
1889
|
-
|
|
1890
|
-
html.tr([attribute.key(id)], [
|
|
1891
|
-
html.td_([react.text(name)]),
|
|
1892
|
-
html.td_([react.text(status)]),
|
|
1893
|
-
])
|
|
1894
|
-
})
|
|
1895
|
-
None -> [
|
|
1896
|
-
html.tr_([
|
|
1897
|
-
html.td(
|
|
1898
|
-
[attribute.col_span(2)],
|
|
1899
|
-
[react.text("로딩 중...")],
|
|
1900
|
-
),
|
|
1901
|
-
]),
|
|
1902
|
-
]
|
|
1903
|
-
},
|
|
1904
|
-
),
|
|
1905
|
-
]),
|
|
1906
|
-
// 페이지네이션
|
|
1907
|
-
render_pagination(ds),
|
|
1908
|
-
])
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
fn render_pagination(ds) -> ReactElement {
|
|
1912
|
-
let offset = lv.offset(ds)
|
|
1913
|
-
let limit = lv.limit(ds)
|
|
1914
|
-
let has_more = lv.has_more_items(ds)
|
|
1915
|
-
|
|
1916
|
-
html.div([attribute.class("pagination")], [
|
|
1917
|
-
html.button(
|
|
1918
|
-
[
|
|
1919
|
-
attribute.disabled(offset == 0),
|
|
1920
|
-
event.on_click(fn(_) {
|
|
1921
|
-
lv.set_offset(ds, int.max(0, offset - limit))
|
|
1922
|
-
}),
|
|
1923
|
-
],
|
|
1924
|
-
[react.text("이전")],
|
|
1925
|
-
),
|
|
1926
|
-
html.button(
|
|
1927
|
-
[
|
|
1928
|
-
attribute.disabled(has_more == Some(False)),
|
|
1929
|
-
event.on_click(fn(_) {
|
|
1930
|
-
lv.set_offset(ds, offset + limit)
|
|
1931
|
-
}),
|
|
1932
|
-
],
|
|
1933
|
-
[react.text("다음")],
|
|
1934
|
-
),
|
|
1935
|
-
])
|
|
1936
|
-
}
|
|
1937
|
-
```
|
|
1938
|
-
|
|
1939
|
-
### 5.3 검색 가능한 리스트
|
|
1940
|
-
|
|
1941
|
-
```gleam
|
|
1942
|
-
import gleam/option.{None, Some}
|
|
1943
|
-
import glendix/mendix
|
|
1944
|
-
import glendix/mendix/filter
|
|
1945
|
-
import glendix/mendix/list_value as lv
|
|
1946
|
-
import glendix/react.{type JsProps, type ReactElement}
|
|
1947
|
-
import glendix/react/attribute
|
|
1948
|
-
import glendix/react/event
|
|
1949
|
-
import glendix/react/hook
|
|
1950
|
-
import glendix/react/html
|
|
1951
|
-
|
|
1952
|
-
pub fn searchable_list(props: JsProps) -> ReactElement {
|
|
1953
|
-
let ds = mendix.get_prop_required(props, "dataSource")
|
|
1954
|
-
let search_attr = mendix.get_string_prop(props, "searchAttribute")
|
|
1955
|
-
let #(query, set_query) = hook.use_state("")
|
|
1956
|
-
|
|
1957
|
-
// 검색어 변경 시 필터 적용
|
|
1958
|
-
hook.use_effect(fn() {
|
|
1959
|
-
case query {
|
|
1960
|
-
"" -> lv.set_filter(ds, None)
|
|
1961
|
-
q ->
|
|
1962
|
-
lv.set_filter(ds, Some(
|
|
1963
|
-
filter.contains(
|
|
1964
|
-
filter.attribute(search_attr),
|
|
1965
|
-
filter.literal(q),
|
|
1966
|
-
),
|
|
1967
|
-
))
|
|
1968
|
-
}
|
|
1969
|
-
Nil
|
|
1970
|
-
}, [query])
|
|
1971
|
-
|
|
1972
|
-
html.div_([
|
|
1973
|
-
// 검색 입력
|
|
1974
|
-
html.input([
|
|
1975
|
-
attribute.class("form-control"),
|
|
1976
|
-
attribute.type_("search"),
|
|
1977
|
-
attribute.placeholder("검색..."),
|
|
1978
|
-
attribute.value(query),
|
|
1979
|
-
event.on_change(fn(e) { set_query(event.target_value(e)) }),
|
|
1980
|
-
]),
|
|
1981
|
-
// 결과 리스트 렌더링
|
|
1982
|
-
render_results(ds),
|
|
1983
|
-
])
|
|
1984
|
-
}
|
|
1985
|
-
```
|
|
1986
|
-
|
|
1987
|
-
### 5.4 컴포넌트 합성
|
|
1988
|
-
|
|
1989
|
-
Gleam 함수를 컴포넌트처럼 활용하여 UI를 분리합니다:
|
|
1990
|
-
|
|
1991
|
-
```gleam
|
|
1992
|
-
import glendix/react.{type ReactElement}
|
|
1993
|
-
import glendix/react/attribute
|
|
1994
|
-
import glendix/react/html
|
|
1995
|
-
|
|
1996
|
-
// 재사용 가능한 카드 컴포넌트
|
|
1997
|
-
fn card(title: String, children: List(ReactElement)) -> ReactElement {
|
|
1998
|
-
html.div([attribute.class("card")], [
|
|
1999
|
-
html.div([attribute.class("card-header")], [
|
|
2000
|
-
html.h3_([react.text(title)]),
|
|
2001
|
-
]),
|
|
2002
|
-
html.div([attribute.class("card-body")], children),
|
|
2003
|
-
])
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
// 재사용 가능한 빈 상태 컴포넌트
|
|
2007
|
-
fn empty_state(message: String) -> ReactElement {
|
|
2008
|
-
html.div([attribute.class("empty-state")], [
|
|
2009
|
-
html.p_([react.text(message)]),
|
|
2010
|
-
])
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
// 조합하여 사용
|
|
2014
|
-
pub fn dashboard(props) -> ReactElement {
|
|
2015
|
-
html.div([attribute.class("dashboard")], [
|
|
2016
|
-
card("사용자 목록", [
|
|
2017
|
-
// 리스트 내용...
|
|
2018
|
-
]),
|
|
2019
|
-
card("최근 활동", [
|
|
2020
|
-
empty_state("아직 활동이 없습니다."),
|
|
2021
|
-
]),
|
|
2022
|
-
])
|
|
2023
|
-
}
|
|
2024
|
-
```
|
|
2025
|
-
|
|
2026
|
-
### 5.5 SVG 아이콘 컴포넌트
|
|
2027
|
-
|
|
2028
|
-
```gleam
|
|
2029
|
-
import glendix/react.{type ReactElement}
|
|
2030
|
-
import glendix/react/attribute
|
|
2031
|
-
import glendix/react/svg
|
|
2032
|
-
import glendix/react/svg_attribute as sa
|
|
2033
|
-
|
|
2034
|
-
fn check_icon(size: String) -> ReactElement {
|
|
2035
|
-
svg.svg(
|
|
2036
|
-
[
|
|
2037
|
-
sa.view_box("0 0 24 24"),
|
|
2038
|
-
attribute.width(size),
|
|
2039
|
-
attribute.height(size),
|
|
2040
|
-
sa.fill("none"),
|
|
2041
|
-
sa.stroke("currentColor"),
|
|
2042
|
-
sa.stroke_width("2"),
|
|
2043
|
-
sa.stroke_linecap("round"),
|
|
2044
|
-
sa.stroke_linejoin("round"),
|
|
2045
|
-
],
|
|
2046
|
-
[svg.path([sa.d("M20 6L9 17l-5-5")], [])],
|
|
2047
|
-
)
|
|
2048
|
-
}
|
|
2049
|
-
```
|
|
2050
|
-
|
|
2051
|
-
### 5.6 Marketplace 위젯 다운로드
|
|
2052
|
-
|
|
2053
|
-
Mendix Marketplace에서 위젯(.mpk)을 인터랙티브하게 검색하고 다운로드할 수 있습니다. 다운로드 완료 후 바인딩 `.gleam` 파일이 자동 생성되어, 별도의 수동 설정 없이 바로 사용할 수 있습니다.
|
|
2054
|
-
|
|
2055
|
-
#### 사전 준비
|
|
2056
|
-
|
|
2057
|
-
`.env` 파일에 Mendix Personal Access Token을 설정합니다:
|
|
2058
|
-
|
|
2059
|
-
```
|
|
2060
|
-
MENDIX_PAT=your_personal_access_token
|
|
2061
|
-
```
|
|
2062
|
-
|
|
2063
|
-
> PAT는 [Mendix Developer Settings](https://user-settings.mendix.com/link/developersettings)에서 **Personal Access Tokens** 섹션의 **New Token**을 클릭하여 발급합니다.
|
|
2064
|
-
> 필요한 scope: `mx:marketplace-content:read`
|
|
2065
|
-
|
|
2066
|
-
#### 실행
|
|
2067
|
-
|
|
2068
|
-
```bash
|
|
2069
|
-
gleam run -m glendix/marketplace
|
|
2070
|
-
```
|
|
2071
|
-
|
|
2072
|
-
#### 인터랙티브 TUI
|
|
2073
|
-
|
|
2074
|
-
실행하면 Content API(`GET /content`)로 위젯 목록을 로드하고, 인터랙티브 TUI가 표시됩니다:
|
|
2075
|
-
|
|
2076
|
-
```
|
|
2077
|
-
── 페이지 1/5+ ──
|
|
2078
|
-
|
|
2079
|
-
[0] Star Rating (54611) v3.2.2 — Mendix
|
|
2080
|
-
[1] Switch (50324) v4.0.0 — Mendix
|
|
2081
|
-
[2] Progress Bar (48019) v3.1.0 — Mendix
|
|
2082
|
-
...
|
|
2083
|
-
|
|
2084
|
-
번호: 다운로드 | 검색어: 이름 검색 | n: 다음 | p: 이전 | r: 초기화 | q: 종료
|
|
2085
|
-
|
|
2086
|
-
>
|
|
2087
|
-
```
|
|
2088
|
-
|
|
2089
|
-
**주요 명령어:**
|
|
2090
|
-
|
|
2091
|
-
| 입력 | 동작 |
|
|
2092
|
-
|------|------|
|
|
2093
|
-
| `0` | 0번 위젯 다운로드 |
|
|
2094
|
-
| `0,1,3` | 여러 위젯 동시 다운로드 (쉼표 구분) |
|
|
2095
|
-
| `star` | 이름/퍼블리셔로 검색 필터링 |
|
|
2096
|
-
| `n` / `p` | 다음/이전 페이지 |
|
|
2097
|
-
| `r` | 검색 초기화 (전체 목록 복귀) |
|
|
2098
|
-
| `q` | 종료 |
|
|
2099
|
-
|
|
2100
|
-
#### 버전 선택
|
|
2101
|
-
|
|
2102
|
-
위젯을 선택하면 버전 목록이 표시됩니다. Pluggable/Classic 타입이 자동 구분됩니다:
|
|
2103
|
-
|
|
2104
|
-
```
|
|
2105
|
-
Star Rating — 버전 선택:
|
|
2106
|
-
|
|
2107
|
-
[0] v3.2.2 (2024-01-15) (Mendix ≥9.24.0) [Pluggable] ← 기본
|
|
2108
|
-
[1] v3.1.0 (2023-08-20) (Mendix ≥9.18.0) [Pluggable]
|
|
2109
|
-
[2] v2.5.1 (2022-03-10) (Mendix ≥8.0.0) [Classic]
|
|
2110
|
-
|
|
2111
|
-
버전 번호 (Enter=최신):
|
|
2112
|
-
```
|
|
2113
|
-
|
|
2114
|
-
Enter를 누르면 최신 버전이 다운로드됩니다.
|
|
2115
|
-
|
|
2116
|
-
#### 동작 흐름
|
|
2117
|
-
|
|
2118
|
-
1. **첫 배치 로드** — Content API에서 첫 40개 아이템을 직접 로드하여 즉시 표시
|
|
2119
|
-
2. **백그라운드 로드** — 나머지 아이템을 별도 프로세스(fork)에서 비동기 로드, IPC로 메인 프로세스에 전달
|
|
2120
|
-
3. **위젯 선택 시** — Playwright(headless chromium)로 Marketplace 페이지에서 S3 다운로드 URL 추출
|
|
2121
|
-
4. **다운로드** — S3에서 `.mpk` 파일을 `widgets/` 디렉토리에 저장
|
|
2122
|
-
5. **바인딩 생성** — `cmd.generate_widget_bindings()`가 자동 호출되어 `src/widgets/`에 바인딩 `.gleam` 파일 생성
|
|
2123
|
-
|
|
2124
|
-
> 버전 정보 조회에 Playwright를 사용하므로, 첫 다운로드 시 브라우저 로그인이 필요합니다. 세션은 `.marketplace-cache/session.json`에 저장되어 이후 재사용됩니다.
|
|
2125
|
-
|
|
2126
|
-
#### 다운로드 후 사용
|
|
2127
|
-
|
|
2128
|
-
다운로드된 위젯은 자동으로 바인딩이 생성됩니다. Pluggable 위젯과 Classic 위젯은 각각 다른 패턴으로 사용합니다:
|
|
2129
|
-
|
|
2130
|
-
**Pluggable 위젯** (`glendix/widget` 사용):
|
|
2131
|
-
|
|
2132
|
-
```gleam
|
|
2133
|
-
// src/widgets/star_rating.gleam (자동 생성)
|
|
2134
|
-
import glendix/mendix
|
|
2135
|
-
import glendix/react.{type JsProps, type ReactElement}
|
|
2136
|
-
import glendix/react/attribute
|
|
2137
|
-
import glendix/widget
|
|
2138
|
-
|
|
2139
|
-
pub fn render(props: JsProps) -> ReactElement {
|
|
2140
|
-
let rate_attribute = mendix.get_prop_required(props, "rateAttribute")
|
|
2141
|
-
let comp = widget.component("StarRating")
|
|
2142
|
-
react.component_el(
|
|
2143
|
-
comp,
|
|
2144
|
-
[attribute.attribute("rateAttribute", rate_attribute)],
|
|
2145
|
-
[],
|
|
2146
|
-
)
|
|
2147
|
-
}
|
|
2148
|
-
```
|
|
2149
|
-
|
|
2150
|
-
**Classic (Dojo) 위젯** (`glendix/classic` 사용):
|
|
2151
|
-
|
|
2152
|
-
```gleam
|
|
2153
|
-
// src/widgets/camera_widget.gleam (자동 생성)
|
|
2154
|
-
import gleam/dynamic
|
|
2155
|
-
import glendix/classic
|
|
2156
|
-
import glendix/mendix
|
|
2157
|
-
import glendix/react.{type JsProps, type ReactElement}
|
|
2158
|
-
|
|
2159
|
-
pub fn render(props: JsProps) -> ReactElement {
|
|
2160
|
-
let mf_to_execute = mendix.get_prop_required(props, "mfToExecute")
|
|
2161
|
-
classic.render("CameraWidget.widget.CameraWidget", [
|
|
2162
|
-
#("mfToExecute", dynamic.from(mf_to_execute)),
|
|
2163
|
-
])
|
|
2164
|
-
}
|
|
2165
|
-
```
|
|
2166
|
-
|
|
2167
|
-
**위젯에서 import:**
|
|
2168
|
-
|
|
2169
|
-
```gleam
|
|
2170
|
-
import widgets/star_rating
|
|
2171
|
-
import widgets/camera_widget
|
|
2172
|
-
|
|
2173
|
-
// 컴포넌트 내부에서
|
|
2174
|
-
star_rating.render(props)
|
|
2175
|
-
camera_widget.render(props)
|
|
2176
|
-
```
|
|
2177
|
-
|
|
2178
|
-
생성된 `src/widgets/*.gleam` 파일은 자유롭게 수정할 수 있으며, 이미 존재하는 파일은 재생성 시 덮어쓰지 않습니다.
|
|
2179
|
-
|
|
2180
|
-
---
|
|
2181
|
-
|
|
2182
|
-
## 6. JS Interop — 외부 JS 라이브러리 연동
|
|
2183
|
-
|
|
2184
|
-
`glendix/js/` 모듈은 외부 JavaScript 라이브러리(SpreadJS, Chart.js 등)와 직접 상호작용할 때 사용하는 저수준 interop API를 제공합니다. React 컴포넌트 바인딩(`glendix/binding`)으로 해결되지 않는 경우의 escape hatch입니다.
|
|
2185
|
-
|
|
2186
|
-
> 모든 JS 값은 `Dynamic` 타입으로 표현됩니다. 타입 안전성보다 유연성을 우선하는 설계이므로, 가능하면 `glendix/binding`이나 전용 opaque 래퍼를 먼저 고려하세요.
|
|
2187
|
-
|
|
2188
|
-
### 6.1 배열 변환 — `js/array`
|
|
2189
|
-
|
|
2190
|
-
Gleam List ↔ JS Array 변환 유틸리티입니다.
|
|
2191
|
-
|
|
2192
|
-
```gleam
|
|
2193
|
-
import gleam/dynamic.{type Dynamic}
|
|
2194
|
-
import glendix/js/array
|
|
2195
|
-
|
|
2196
|
-
// Gleam List → JS Array (Dynamic)
|
|
2197
|
-
let js_arr = array.from_list([1, 2, 3])
|
|
2198
|
-
|
|
2199
|
-
// JS Array → Gleam List
|
|
2200
|
-
let gleam_list: List(Int) = array.to_list(js_arr)
|
|
2201
|
-
```
|
|
2202
|
-
|
|
2203
|
-
### 6.2 객체 조작 — `js/object`
|
|
2204
|
-
|
|
2205
|
-
JS 객체 생성, 속성 접근, 메서드 호출을 제공합니다.
|
|
2206
|
-
|
|
2207
|
-
```gleam
|
|
2208
|
-
import gleam/dynamic
|
|
2209
|
-
import glendix/js/object
|
|
2210
|
-
|
|
2211
|
-
// 객체 생성
|
|
2212
|
-
let config = object.object([
|
|
2213
|
-
#("width", dynamic.from(800)),
|
|
2214
|
-
#("height", dynamic.from(600)),
|
|
2215
|
-
#("editable", dynamic.from(True)),
|
|
2216
|
-
])
|
|
2217
|
-
|
|
2218
|
-
// 빈 객체
|
|
2219
|
-
let obj = object.empty()
|
|
2220
|
-
|
|
2221
|
-
// 속성 읽기/쓰기 (set은 mutation — 원본 반환)
|
|
2222
|
-
let width = object.get(config, "width")
|
|
2223
|
-
let config = object.set(config, "theme", dynamic.from("dark"))
|
|
2224
|
-
|
|
2225
|
-
// 속성 삭제/존재 확인
|
|
2226
|
-
let config = object.delete(config, "editable")
|
|
2227
|
-
let has_theme = object.has(config, "theme") // True
|
|
2228
|
-
|
|
2229
|
-
// 메서드 호출
|
|
2230
|
-
let result = object.call_method(spreadsheet, "getCell", [
|
|
2231
|
-
dynamic.from(0),
|
|
2232
|
-
dynamic.from(0),
|
|
2233
|
-
])
|
|
2234
|
-
let value = object.call_method_0(cell, "getValue")
|
|
2235
|
-
|
|
2236
|
-
// new 연산자로 인스턴스 생성
|
|
2237
|
-
let instance = object.new_instance(constructor, [
|
|
2238
|
-
dynamic.from("arg1"),
|
|
2239
|
-
])
|
|
2240
|
-
```
|
|
2241
|
-
|
|
2242
|
-
### 6.3 JSON — `js/json`
|
|
2243
|
-
|
|
2244
|
-
JSON 직렬화/역직렬화입니다.
|
|
2245
|
-
|
|
2246
|
-
```gleam
|
|
2247
|
-
import gleam/dynamic
|
|
2248
|
-
import glendix/js/json
|
|
2249
|
-
|
|
2250
|
-
// 직렬화
|
|
2251
|
-
let json_str = json.stringify(dynamic.from(config))
|
|
2252
|
-
|
|
2253
|
-
// 역직렬화 (Result 반환)
|
|
2254
|
-
case json.parse("{\"key\": \"value\"}") {
|
|
2255
|
-
Ok(data) -> object.get(data, "key")
|
|
2256
|
-
Error(msg) -> // 파싱 에러 메시지
|
|
2257
|
-
}
|
|
2258
|
-
```
|
|
2259
|
-
|
|
2260
|
-
### 6.4 Promise — `js/promise`
|
|
2261
|
-
|
|
2262
|
-
Promise 체이닝, 에러 처리, 병렬 실행을 제공합니다. `glendix/react`의 `Promise(a)` 타입을 사용합니다.
|
|
2263
|
-
|
|
2264
|
-
```gleam
|
|
2265
|
-
import gleam/dynamic.{type Dynamic}
|
|
2266
|
-
import glendix/js/promise
|
|
2267
|
-
import glendix/react.{type Promise}
|
|
2268
|
-
|
|
2269
|
-
// 즉시 이행/거부
|
|
2270
|
-
let p = promise.resolve(42)
|
|
2271
|
-
let err = promise.reject("something went wrong")
|
|
2272
|
-
|
|
2273
|
-
// 체이닝 (flatMap)
|
|
2274
|
-
promise.then_(fetch_data(), fn(data) {
|
|
2275
|
-
promise.resolve(transform(data))
|
|
2276
|
-
})
|
|
2277
|
-
|
|
2278
|
-
// 값 변환 (map)
|
|
2279
|
-
promise.map(fetch_data(), fn(data) {
|
|
2280
|
-
object.get(data, "name")
|
|
2281
|
-
})
|
|
2282
|
-
|
|
2283
|
-
// 에러 처리
|
|
2284
|
-
promise.catch_(risky_operation(), fn(error: Dynamic) {
|
|
2285
|
-
promise.resolve(fallback_value)
|
|
2286
|
-
})
|
|
2287
|
-
|
|
2288
|
-
// 병렬 실행 (모든 Promise 대기)
|
|
2289
|
-
promise.all([fetch_users(), fetch_roles()])
|
|
2290
|
-
|> promise.map(fn(results) {
|
|
2291
|
-
// results: List(a)
|
|
2292
|
-
})
|
|
2293
|
-
|
|
2294
|
-
// 가장 빠른 결과
|
|
2295
|
-
promise.race([fetch_from_primary(), fetch_from_backup()])
|
|
2296
|
-
|
|
2297
|
-
// fire-and-forget 콜백
|
|
2298
|
-
promise.await_(save_data(), fn(result) {
|
|
2299
|
-
// 이행 시 실행, 반환값 무시
|
|
2300
|
-
Nil
|
|
2301
|
-
})
|
|
2302
|
-
```
|
|
2303
|
-
|
|
2304
|
-
### 6.5 DOM 조작 — `js/dom`
|
|
2305
|
-
|
|
2306
|
-
DOM 요소에 대한 직접 조작 유틸리티입니다. `hook.use_ref`로 얻은 ref의 current 값과 함께 사용합니다.
|
|
2307
|
-
|
|
2308
|
-
```gleam
|
|
2309
|
-
import glendix/js/dom
|
|
2310
|
-
import glendix/react/hook
|
|
2311
|
-
|
|
2312
|
-
let input_ref = hook.use_ref(Nil)
|
|
2313
|
-
|
|
2314
|
-
// 포커스/블러/클릭
|
|
2315
|
-
dom.focus(hook.get_ref(input_ref))
|
|
2316
|
-
dom.blur(hook.get_ref(input_ref))
|
|
2317
|
-
dom.click(hook.get_ref(input_ref))
|
|
2318
|
-
|
|
2319
|
-
// 스크롤
|
|
2320
|
-
dom.scroll_into_view(hook.get_ref(input_ref))
|
|
2321
|
-
|
|
2322
|
-
// 위치/크기 정보 (DOMRect)
|
|
2323
|
-
let rect = dom.get_bounding_client_rect(hook.get_ref(input_ref))
|
|
2324
|
-
|
|
2325
|
-
// CSS 선택자로 하위 요소 검색
|
|
2326
|
-
case dom.query_selector(hook.get_ref(container_ref), ".target") {
|
|
2327
|
-
Some(el) -> dom.focus(el)
|
|
2328
|
-
None -> Nil
|
|
2329
|
-
}
|
|
2330
|
-
```
|
|
2331
|
-
|
|
2332
|
-
### 6.6 타이머 — `js/timer`
|
|
2333
|
-
|
|
2334
|
-
setTimeout/setInterval 래퍼입니다. `TimerId`는 opaque type으로 숫자 조작을 방지합니다.
|
|
2335
|
-
|
|
2336
|
-
```gleam
|
|
2337
|
-
import glendix/js/timer
|
|
2338
|
-
|
|
2339
|
-
// 지연 실행
|
|
2340
|
-
let id = timer.set_timeout(fn() {
|
|
2341
|
-
// 1초 후 실행
|
|
2342
|
-
Nil
|
|
2343
|
-
}, 1000)
|
|
2344
|
-
|
|
2345
|
-
// 취소
|
|
2346
|
-
timer.clear_timeout(id)
|
|
2347
|
-
|
|
2348
|
-
// 반복 실행
|
|
2349
|
-
let interval_id = timer.set_interval(fn() {
|
|
2350
|
-
// 500ms마다 실행
|
|
2351
|
-
Nil
|
|
2352
|
-
}, 500)
|
|
2353
|
-
|
|
2354
|
-
// 반복 취소
|
|
2355
|
-
timer.clear_interval(interval_id)
|
|
2356
|
-
```
|
|
2357
|
-
|
|
2358
|
-
#### useEffect와 함께 사용 (클린업 패턴)
|
|
2359
|
-
|
|
2360
|
-
```gleam
|
|
2361
|
-
hook.use_effect_once_cleanup(fn() {
|
|
2362
|
-
let id = timer.set_interval(fn() {
|
|
2363
|
-
update_count(fn(prev) { prev + 1 })
|
|
2364
|
-
Nil
|
|
2365
|
-
}, 1000)
|
|
2366
|
-
|
|
2367
|
-
// 언마운트 시 타이머 정리
|
|
2368
|
-
fn() { timer.clear_interval(id) }
|
|
2369
|
-
})
|
|
2370
|
-
```
|
|
2371
|
-
|
|
2372
|
-
### 6.7 모듈 요약
|
|
2373
|
-
|
|
2374
|
-
| 모듈 | 용도 | FFI |
|
|
2375
|
-
|------|------|-----|
|
|
2376
|
-
| `js/array` | Gleam List ↔ JS Array 변환 | 없음 (react_ffi.mjs 재사용) |
|
|
2377
|
-
| `js/object` | 객체 생성/속성 CRUD/메서드 호출 | object_ffi.mjs |
|
|
2378
|
-
| `js/json` | JSON stringify/parse | json_ffi.mjs |
|
|
2379
|
-
| `js/promise` | Promise 체이닝/병렬/에러 처리 | promise_ffi.mjs |
|
|
2380
|
-
| `js/dom` | DOM 포커스/클릭/스크롤/쿼리 | dom_ffi.mjs |
|
|
2381
|
-
| `js/timer` | setTimeout/setInterval | timer_ffi.mjs |
|
|
2382
|
-
|
|
2383
|
-
---
|
|
2384
|
-
|
|
2385
|
-
## 위젯 프로퍼티 정의 (TUI 에디터)
|
|
2386
|
-
|
|
2387
|
-
`gleam run -m glendix/define`을 실행하면 터미널에서 위젯의 프로퍼티 정의를 인터랙티브하게 편집할 수 있습니다.
|
|
2388
|
-
|
|
2389
|
-
### 기능
|
|
2390
|
-
|
|
2391
|
-
- **프로퍼티 그룹 관리**: 그룹 추가/삭제/이름 변경
|
|
2392
|
-
- **프로퍼티 편집**: key, type, caption, description, required 등 모든 필드를 TUI에서 편집
|
|
2393
|
-
- **타입 선택**: Mendix가 지원하는 모든 프로퍼티 타입(string, boolean, integer, enumeration, object, action 등) 중 선택
|
|
2394
|
-
- **열거형(Enum) 관리**: 열거형 타입의 key/caption 값 추가/삭제/편집
|
|
2395
|
-
- **위젯 메타 편집**: 위젯 이름, 설명, Studio Pro 카테고리 등 메타 정보 편집
|
|
2396
|
-
- **시스템 프로퍼티**: Mendix 시스템 프로퍼티(Label, Name, TabIndex, Visibility, Editability) 토글
|
|
2397
|
-
- **XML 자동 생성**: 편집 결과를 Mendix Pluggable Widget XML 형식으로 저장
|
|
2398
|
-
|
|
2399
|
-
### 사용법
|
|
2400
|
-
|
|
2401
|
-
```bash
|
|
2402
|
-
gleam run -m glendix/define
|
|
2403
|
-
```
|
|
2404
|
-
|
|
2405
|
-
TUI가 실행되면 방향키, Enter, Escape, Tab 등으로 탐색하고 편집합니다:
|
|
2406
|
-
|
|
2407
|
-
- **방향키(↑↓)**: 트리 뷰에서 그룹/프로퍼티 탐색
|
|
2408
|
-
- **Enter**: 선택한 항목 편집 또는 확인
|
|
2409
|
-
- **Escape**: 이전 화면으로 돌아가기
|
|
2410
|
-
- **a**: 새 프로퍼티 추가
|
|
2411
|
-
- **g**: 새 그룹 추가
|
|
2412
|
-
- **d**: 선택한 항목 삭제 (확인 프롬프트)
|
|
2413
|
-
- **m**: 위젯 메타 정보 편집
|
|
2414
|
-
- **s**: 시스템 프로퍼티 편집
|
|
2415
|
-
- **Ctrl+C / q**: 종료 (저장 확인)
|
|
2416
|
-
|
|
2417
|
-
---
|
|
2418
|
-
|
|
2419
|
-
## 7. 트러블슈팅
|
|
2420
|
-
|
|
2421
|
-
### 빌드 에러
|
|
2422
|
-
|
|
2423
|
-
| 문제 | 원인 | 해결 |
|
|
2424
|
-
|---|---|---|
|
|
2425
|
-
| `gleam build` 실패: glendix를 찾을 수 없음 | `gleam.toml`의 경로가 잘못됨 | `path = "../glendix"` 경로 확인 |
|
|
2426
|
-
| `react is not defined` | peer dependency 미설치 | `gleam run -m glendix/install` |
|
|
2427
|
-
| `Big is not a constructor` | big.js 미설치 | `gleam run -m glendix/install` |
|
|
2428
|
-
|
|
2429
|
-
### 런타임 에러
|
|
2430
|
-
|
|
2431
|
-
| 문제 | 원인 | 해결 |
|
|
2432
|
-
|---|---|---|
|
|
2433
|
-
| `Cannot read property of undefined` | 존재하지 않는 prop 접근 | `get_prop` (Option) 대신 `get_prop_required` 사용 시 prop 이름 확인 |
|
|
2434
|
-
| `set_value` 호출 시 에러 | read_only 상태에서 값 설정 | `ev.is_editable(attr)` 확인 후 설정 |
|
|
2435
|
-
| Hook 순서 에러 | 조건부로 Hook 호출 | Hook은 항상 동일한 순서로 호출해야 함 (React Rules of Hooks) |
|
|
2436
|
-
| `바인딩이 생성되지 않았습니다` | `binding_ffi.mjs`가 스텁 상태 | `gleam run -m glendix/install` 실행 |
|
|
2437
|
-
| `위젯 바인딩이 생성되지 않았습니다` | `widget_ffi.mjs`가 스텁 상태 | `widgets/` 디렉토리에 `.mpk` 배치 후 `gleam run -m glendix/install` 실행 |
|
|
2438
|
-
| `위젯 바인딩에 등록되지 않은 위젯` | 해당 `.mpk`가 `widgets/`에 없음 | `.mpk` 파일 배치 후 재설치 |
|
|
2439
|
-
| `could not be resolved – treating it as an external dependency` | `bindings.json`에 등록한 패키지가 `node_modules`에 없음 | `npm install <패키지명>` 등으로 설치 후 재빌드 |
|
|
2440
|
-
| `바인딩에 등록되지 않은 모듈` | `bindings.json`에 해당 패키지 미등록 | `bindings.json`에 패키지와 컴포넌트 추가 후 재설치 |
|
|
2441
|
-
| `모듈에 없는 컴포넌트` | `bindings.json`의 `components`에 해당 컴포넌트 미등록 | `components` 배열에 추가 후 재설치 |
|
|
2442
|
-
| `.env 파일에 MENDIX_PAT가 필요합니다` | marketplace 실행 시 PAT 미설정 | `.env`에 `MENDIX_PAT=...` 추가 (scope: `mx:marketplace-content:read`) — [Developer Settings](https://user-settings.mendix.com/link/developersettings)에서 발급 |
|
|
2443
|
-
| `인증 실패 — MENDIX_PAT를 확인하세요` | PAT가 잘못되었거나 만료됨 | [Developer Settings](https://user-settings.mendix.com/link/developersettings)에서 새 PAT 발급 |
|
|
2444
|
-
| `위젯을 불러올 수 없습니다` | Content API 접근 실패 | 네트워크 및 PAT 확인 |
|
|
2445
|
-
| `Playwright 오류` | chromium 미설치 또는 세션 만료 | `npx playwright install chromium` 실행, 또는 브라우저 재로그인 |
|
|
2446
|
-
| `저장된 세션이 만료되었습니다` | Mendix 로그인 세션 만료 | 브라우저 로그인 팝업에서 재로그인 |
|
|
2447
|
-
|
|
2448
|
-
### 일반적인 실수
|
|
2449
|
-
|
|
2450
|
-
**1. Hook을 조건부로 호출하지 마세요:**
|
|
2451
|
-
|
|
2452
|
-
```gleam
|
|
2453
|
-
// 잘못된 예
|
|
2454
|
-
pub fn widget(props) {
|
|
2455
|
-
case mendix.get_prop(props, "attr") {
|
|
2456
|
-
Some(attr) -> {
|
|
2457
|
-
let #(count, set_count) = hook.use_state(0) // 조건 안에서 Hook!
|
|
2458
|
-
// ...
|
|
2459
|
-
}
|
|
2460
|
-
None -> react.none()
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
// 올바른 예
|
|
2465
|
-
pub fn widget(props) {
|
|
2466
|
-
let #(count, set_count) = hook.use_state(0) // 항상 최상위에서 호출
|
|
2467
|
-
|
|
2468
|
-
case mendix.get_prop(props, "attr") {
|
|
2469
|
-
Some(attr) -> // count 사용...
|
|
2470
|
-
None -> react.none()
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
```
|
|
2474
|
-
|
|
2475
|
-
**2. 리스트 렌더링에서 key를 빠뜨리지 마세요:**
|
|
2476
|
-
|
|
2477
|
-
```gleam
|
|
2478
|
-
// key가 있어야 React가 효율적으로 업데이트합니다
|
|
2479
|
-
list.map(items, fn(item) {
|
|
2480
|
-
html.div([attribute.key(mendix.object_id(item))], [
|
|
2481
|
-
// ...
|
|
2482
|
-
])
|
|
2483
|
-
})
|
|
2484
|
-
```
|
|
2485
|
-
|
|
2486
|
-
**3. 월(month) 변환을 직접 하지 마세요:**
|
|
2487
|
-
|
|
2488
|
-
```gleam
|
|
2489
|
-
// glendix/mendix/date가 자동으로 1-based ↔ 0-based 변환합니다
|
|
2490
|
-
let month = date.month(my_date) // 1~12 (Gleam 기준, 변환 불필요)
|
|
2491
|
-
```
|
|
2492
|
-
|
|
2493
|
-
**4. 외부 React 컴포넌트용 `.mjs` 파일을 직접 작성하지 마세요:**
|
|
2494
|
-
|
|
2495
|
-
```gleam
|
|
2496
|
-
// 잘못된 방법 — 수동 FFI 작성
|
|
2497
|
-
// recharts_ffi.mjs를 만들고 @external로 연결하는 것
|
|
2498
|
-
|
|
2499
|
-
// 올바른 방법 — bindings.json + glendix/binding 사용
|
|
2500
|
-
import glendix/binding
|
|
2501
|
-
let rc = binding.module("recharts")
|
|
2502
|
-
react.component_el(binding.resolve(rc, "PieChart"), attrs, children)
|
|
2503
|
-
react.void_component_el(binding.resolve(rc, "Tooltip"), attrs)
|
|
2504
|
-
```
|
|
2505
|
-
|
|
2506
|
-
**5. `.mpk` 위젯용 `.mjs` 파일을 직접 작성하지 마세요:**
|
|
2507
|
-
|
|
2508
|
-
```gleam
|
|
2509
|
-
// 잘못된 방법 — 수동 FFI 작성
|
|
2510
|
-
|
|
2511
|
-
// 올바른 방법 — widgets/ 디렉토리 + glendix/widget 사용
|
|
2512
|
-
import glendix/widget
|
|
2513
|
-
let switch_comp = widget.component("Switch")
|
|
2514
|
-
react.component_el(switch_comp, attrs, children)
|
|
2515
|
-
```
|
|
2516
|
-
|
|
2517
|
-
**6. `binding.resolve()`에서 컴포넌트 이름을 snake_case로 바꾸지 마세요:**
|
|
2518
|
-
|
|
2519
|
-
```gleam
|
|
2520
|
-
// 잘못된 예
|
|
2521
|
-
binding.resolve(m(), "pie_chart")
|
|
2522
|
-
|
|
2523
|
-
// 올바른 예 — JavaScript 원본 이름(PascalCase) 그대로 사용
|
|
2524
|
-
binding.resolve(m(), "PieChart")
|
|
2525
|
-
```
|
|
2526
|
-
|
|
2527
|
-
**7. `react.none()` 대신 빈 문자열이나 빈 리스트를 사용하지 마세요:**
|
|
2528
|
-
|
|
2529
|
-
```gleam
|
|
2530
|
-
// 잘못된 예
|
|
2531
|
-
react.text("") // 빈 텍스트 노드 생성
|
|
2532
|
-
react.fragment([]) // 빈 Fragment 생성
|
|
2533
|
-
|
|
2534
|
-
// 올바른 예
|
|
2535
|
-
react.none() // React null 반환
|
|
2536
|
-
```
|
|
2537
|
-
|
|
2538
|
-
---
|
|
1
|
+
# glendix v3.0 — Agent Reference Guide
|
|
2
|
+
|
|
3
|
+
> 이 문서는 AI 에이전트(LLM)가 glendix 코드를 작성할 때 참조하는 가이드입니다. 각 섹션은 독립적으로 읽을 수 있습니다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 아키텍처 개요
|
|
8
|
+
|
|
9
|
+
glendix는 Gleam으로 Mendix Pluggable Widget을 작성하는 FFI 라이브러리입니다.
|
|
10
|
+
|
|
11
|
+
**v3.0 설계 원칙: 위임**
|
|
12
|
+
|
|
13
|
+
| 관심사 | 담당 패키지 | glendix 역할 |
|
|
14
|
+
|--------|------------|-------------|
|
|
15
|
+
| React 바인딩 (엘리먼트, 훅, 이벤트, HTML/SVG) | `redraw`, `redraw_dom` | 사용하지 않음 — 직접 import |
|
|
16
|
+
| TEA 패턴 (Model-Update-View) | `lustre` | `glendix/lustre` 브릿지 제공 |
|
|
17
|
+
| Mendix API (JsProps, EditableValue, ListValue 등) | `glendix` | 핵심 담당 |
|
|
18
|
+
| 외부 JS 컴포넌트 (widget, binding) → React | `glendix/interop` | 브릿지 제공 |
|
|
19
|
+
| 빌드/설치/마켓플레이스 | `glendix` | 핵심 담당 |
|
|
20
|
+
|
|
21
|
+
**의존성 구조:**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
사용자 코드
|
|
25
|
+
├── redraw ← React 훅, 컴포넌트, fragment 등
|
|
26
|
+
├── redraw_dom ← HTML/SVG 태그, 속성, 이벤트
|
|
27
|
+
├── lustre ← TEA update/view (선택)
|
|
28
|
+
└── glendix
|
|
29
|
+
├── mendix ← Mendix API 타입 + props 접근
|
|
30
|
+
├── interop ← 외부 JS 컴포넌트 → redraw.Element
|
|
31
|
+
├── lustre ← Lustre Element → redraw.Element 브릿지
|
|
32
|
+
├── widget ← .mpk 위젯 컴포넌트
|
|
33
|
+
├── binding ← bindings.json 외부 React 컴포넌트
|
|
34
|
+
├── classic ← Classic (Dojo) 위젯
|
|
35
|
+
└── js/* ← JS interop escape hatch
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. 프로젝트 설정
|
|
41
|
+
|
|
42
|
+
사용자 프로젝트의 `gleam.toml`에 glendix를 추가합니다:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
[dependencies]
|
|
46
|
+
glendix = ">= 3.0.0 and < 4.0.0"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Peer dependency (위젯 프로젝트 `package.json`):
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "big.js": "^6.0.0" } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gleam run -m glendix/install # 의존성 설치 + 바인딩 생성
|
|
57
|
+
gleam build # 컴파일 확인
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. 위젯 함수 시그니처
|
|
63
|
+
|
|
64
|
+
모든 Mendix Pluggable Widget은 이 시그니처를 따릅니다:
|
|
65
|
+
|
|
66
|
+
```gleam
|
|
67
|
+
import glendix/mendix.{type JsProps}
|
|
68
|
+
import redraw.{type Element}
|
|
69
|
+
|
|
70
|
+
pub fn widget(props: JsProps) -> Element
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `JsProps` — Mendix가 전달하는 props 객체 (opaque). `glendix/mendix` 모듈의 접근자로만 읽는다.
|
|
74
|
+
- `Element` — redraw의 React 엘리먼트 타입. `redraw/dom/html`, `redraw.fragment()` 등으로 생성한다.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 3. 렌더링 경로 선택
|
|
79
|
+
|
|
80
|
+
glendix는 두 가지 렌더링 경로를 지원합니다. 둘 다 `redraw.Element`를 반환하므로 자유롭게 합성 가능합니다.
|
|
81
|
+
|
|
82
|
+
| 기준 | redraw (직접 React) | lustre (TEA 브릿지) |
|
|
83
|
+
|------|---------------------|---------------------|
|
|
84
|
+
| 상태 관리 | `redraw.use_state`, `redraw.use_reducer` | `update` 함수 (순수) |
|
|
85
|
+
| 뷰 작성 | `redraw/dom/html`, `redraw/dom/events` | `lustre/element/html`, `lustre/event` |
|
|
86
|
+
| 사이드 이펙트 | `redraw.use_effect` | `lustre/effect.Effect` |
|
|
87
|
+
| 진입점 | 위젯 함수 자체 | `glendix/lustre.use_tea()` 또는 `use_simple()` |
|
|
88
|
+
| 적합한 경우 | 단순 UI, Mendix 값 표시/수정 | 복잡한 상태 머신, TEA 선호 |
|
|
89
|
+
| 외부 라이브러리 | redraw 생태계 | lustre 생태계 (lustre_ui 등) |
|
|
90
|
+
| 합성 | lustre를 삽입: `gl.use_tea()` | redraw를 삽입: `gl.embed()` |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. redraw 렌더링 경로 — 레퍼런스
|
|
95
|
+
|
|
96
|
+
### 4.1 필수 import 패턴
|
|
97
|
+
|
|
98
|
+
```gleam
|
|
99
|
+
import glendix/mendix.{type JsProps} // Mendix props 타입
|
|
100
|
+
import redraw.{type Element} // 반환 타입
|
|
101
|
+
import redraw/dom/html // HTML 태그 함수
|
|
102
|
+
import redraw/dom/attribute // HTML 속성
|
|
103
|
+
import redraw/dom/events // 이벤트 핸들러
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4.2 HTML 엘리먼트 생성
|
|
107
|
+
|
|
108
|
+
```gleam
|
|
109
|
+
// 속성 + 자식
|
|
110
|
+
html.div([attribute.class("container")], [
|
|
111
|
+
html.h1([attribute.class("title")], [html.text("제목")]),
|
|
112
|
+
html.p([], [html.text("내용")]),
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
// void 엘리먼트 (자식 없음)
|
|
116
|
+
html.input([attribute.type_("text"), attribute.value(val)])
|
|
117
|
+
html.img([attribute.src("image.png"), attribute.alt("설명")])
|
|
118
|
+
html.br([])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4.3 텍스트, 빈 렌더링, Fragment
|
|
122
|
+
|
|
123
|
+
```gleam
|
|
124
|
+
html.text("안녕하세요") // 텍스트 노드
|
|
125
|
+
html.text("Count: " <> int.to_string(count))
|
|
126
|
+
|
|
127
|
+
html.none() // 아무것도 렌더링하지 않음 (React null)
|
|
128
|
+
|
|
129
|
+
redraw.fragment([child1, child2]) // Fragment
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 4.4 조건부 렌더링
|
|
133
|
+
|
|
134
|
+
v3.0에서는 Gleam `case` 표현식을 직접 사용합니다:
|
|
135
|
+
|
|
136
|
+
```gleam
|
|
137
|
+
// Bool 기반
|
|
138
|
+
case is_visible {
|
|
139
|
+
True -> html.div([], [html.text("보임")])
|
|
140
|
+
False -> html.none()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Option 기반
|
|
144
|
+
case maybe_user {
|
|
145
|
+
Some(user) -> html.span([], [html.text(user.name)])
|
|
146
|
+
None -> html.none()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 복잡한 조건
|
|
150
|
+
case mendix.get_status(value) {
|
|
151
|
+
Available -> html.div([], [html.text("완료")])
|
|
152
|
+
Loading -> html.div([], [html.text("로딩 중...")])
|
|
153
|
+
Unavailable -> html.none()
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 4.5 리스트 렌더링
|
|
158
|
+
|
|
159
|
+
```gleam
|
|
160
|
+
import gleam/list
|
|
161
|
+
|
|
162
|
+
html.ul([], list.map(items, fn(item) {
|
|
163
|
+
html.li([attribute.key(mendix.object_id(item))], [
|
|
164
|
+
html.text(ev.display_value(la.get_attribute(name_attr, item))),
|
|
165
|
+
])
|
|
166
|
+
}))
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> 리스트 렌더링 시 `attribute.key()`를 항상 설정해야 합니다. React reconciliation에 필요합니다.
|
|
170
|
+
|
|
171
|
+
### 4.6 속성
|
|
172
|
+
|
|
173
|
+
```gleam
|
|
174
|
+
import redraw/dom/attribute
|
|
175
|
+
|
|
176
|
+
// 기본
|
|
177
|
+
attribute.class("btn btn-primary") // className
|
|
178
|
+
attribute.id("main")
|
|
179
|
+
attribute.style([#("color", "red"), #("padding", "8px")])
|
|
180
|
+
|
|
181
|
+
// 폼
|
|
182
|
+
attribute.type_("text")
|
|
183
|
+
attribute.value("입력값")
|
|
184
|
+
attribute.placeholder("입력하세요")
|
|
185
|
+
attribute.disabled(True)
|
|
186
|
+
attribute.checked(True)
|
|
187
|
+
attribute.readonly(True)
|
|
188
|
+
|
|
189
|
+
// 범용 escape hatch
|
|
190
|
+
attribute.attribute("data-custom", "value")
|
|
191
|
+
|
|
192
|
+
// ref
|
|
193
|
+
attribute.ref(my_ref)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 4.7 이벤트 핸들러
|
|
197
|
+
|
|
198
|
+
```gleam
|
|
199
|
+
import redraw/dom/events
|
|
200
|
+
|
|
201
|
+
events.on_click(fn(e) { handle_click(e) })
|
|
202
|
+
events.on_change(fn(e) { set_name(/* ... */) })
|
|
203
|
+
events.on_input(fn(e) { Nil })
|
|
204
|
+
events.on_submit(fn(e) { Nil })
|
|
205
|
+
events.on_key_down(fn(e) { Nil })
|
|
206
|
+
events.on_focus(fn(e) { Nil })
|
|
207
|
+
events.on_blur(fn(e) { Nil })
|
|
208
|
+
|
|
209
|
+
// 캡처 단계
|
|
210
|
+
events.on_click_capture(fn(e) { Nil })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 4.8 Hooks
|
|
214
|
+
|
|
215
|
+
모든 훅은 `redraw` 메인 모듈에 있습니다:
|
|
216
|
+
|
|
217
|
+
```gleam
|
|
218
|
+
import redraw
|
|
219
|
+
|
|
220
|
+
// 상태
|
|
221
|
+
let #(count, set_count) = redraw.use_state(0)
|
|
222
|
+
let #(count, update_count) = redraw.use_state_(0) // 업데이터 함수 변형
|
|
223
|
+
let #(data, set_data) = redraw.use_lazy_state(fn() { expensive() })
|
|
224
|
+
|
|
225
|
+
// 이펙트
|
|
226
|
+
redraw.use_effect(fn() { Nil }, deps) // 의존성 지정
|
|
227
|
+
redraw.use_effect_(fn() { fn() { cleanup() } }, deps) // 클린업 포함
|
|
228
|
+
|
|
229
|
+
// Ref
|
|
230
|
+
let my_ref = redraw.use_ref() // Option(a)
|
|
231
|
+
let my_ref = redraw.use_ref_(initial) // 초기값 지정
|
|
232
|
+
|
|
233
|
+
// 메모이제이션
|
|
234
|
+
let result = redraw.use_memo(fn() { expensive(data) }, data)
|
|
235
|
+
let handler = redraw.use_callback(fn(e) { handle(e) }, deps)
|
|
236
|
+
|
|
237
|
+
// 리듀서
|
|
238
|
+
let #(state, dispatch) = redraw.use_reducer(reducer_fn, initial_state)
|
|
239
|
+
|
|
240
|
+
// Context
|
|
241
|
+
let value = redraw.use_context(my_context)
|
|
242
|
+
|
|
243
|
+
// 기타
|
|
244
|
+
let id = redraw.use_id()
|
|
245
|
+
let #(is_pending, start) = redraw.use_transition()
|
|
246
|
+
let deferred = redraw.use_deferred_value(value)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 4.9 컴포넌트 정의
|
|
250
|
+
|
|
251
|
+
```gleam
|
|
252
|
+
import redraw
|
|
253
|
+
|
|
254
|
+
// 이름 있는 컴포넌트 (DevTools에 표시)
|
|
255
|
+
let my_comp = redraw.component_("MyComponent", fn(props) {
|
|
256
|
+
html.div([], [html.text("Hello")])
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// React.memo (구조 동등성 기반 리렌더 방지)
|
|
260
|
+
let memoized = redraw.memoize_(my_comp)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### 4.10 Context API
|
|
264
|
+
|
|
265
|
+
```gleam
|
|
266
|
+
import redraw
|
|
267
|
+
|
|
268
|
+
let theme_ctx = redraw.create_context_("light")
|
|
269
|
+
|
|
270
|
+
// Provider
|
|
271
|
+
redraw.provider(theme_ctx, "dark", [child_elements])
|
|
272
|
+
|
|
273
|
+
// Consumer (Hook)
|
|
274
|
+
let theme = redraw.use_context(theme_ctx)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 4.11 SVG
|
|
278
|
+
|
|
279
|
+
```gleam
|
|
280
|
+
import redraw/dom/svg
|
|
281
|
+
import redraw/dom/attribute
|
|
282
|
+
|
|
283
|
+
svg.svg([attribute.attribute("viewBox", "0 0 100 100")], [
|
|
284
|
+
svg.circle([
|
|
285
|
+
attribute.attribute("cx", "50"),
|
|
286
|
+
attribute.attribute("cy", "50"),
|
|
287
|
+
attribute.attribute("r", "40"),
|
|
288
|
+
attribute.attribute("fill", "blue"),
|
|
289
|
+
], []),
|
|
290
|
+
])
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 5. lustre 렌더링 경로 — 레퍼런스
|
|
296
|
+
|
|
297
|
+
### 5.1 TEA 패턴 (use_tea)
|
|
298
|
+
|
|
299
|
+
`update`와 `view`는 표준 lustre 코드와 100% 동일합니다. 진입점만 `glendix/lustre.use_tea()`를 사용합니다.
|
|
300
|
+
|
|
301
|
+
```gleam
|
|
302
|
+
import gleam/int
|
|
303
|
+
import glendix/lustre as gl
|
|
304
|
+
import glendix/mendix.{type JsProps}
|
|
305
|
+
import lustre/effect
|
|
306
|
+
import lustre/element/html
|
|
307
|
+
import lustre/event
|
|
308
|
+
import redraw.{type Element}
|
|
309
|
+
|
|
310
|
+
// --- Model ---
|
|
311
|
+
type Model {
|
|
312
|
+
Model(count: Int)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Msg ---
|
|
316
|
+
type Msg {
|
|
317
|
+
Increment
|
|
318
|
+
Decrement
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- Update (순수 lustre 코드) ---
|
|
322
|
+
fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) {
|
|
323
|
+
case msg {
|
|
324
|
+
Increment -> #(Model(model.count + 1), effect.none())
|
|
325
|
+
Decrement -> #(Model(model.count - 1), effect.none())
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- View (순수 lustre 코드) ---
|
|
330
|
+
fn view(model: Model) {
|
|
331
|
+
html.div([], [
|
|
332
|
+
html.button([event.on_click(Decrement)], [html.text("-")]),
|
|
333
|
+
html.text(int.to_string(model.count)),
|
|
334
|
+
html.button([event.on_click(Increment)], [html.text("+")]),
|
|
335
|
+
])
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --- 위젯 진입점 ---
|
|
339
|
+
pub fn widget(_props: JsProps) -> Element {
|
|
340
|
+
gl.use_tea(#(Model(0), effect.none()), update, view)
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 5.2 Simple TEA (use_simple) — Effect 없음
|
|
345
|
+
|
|
346
|
+
```gleam
|
|
347
|
+
import glendix/lustre as gl
|
|
348
|
+
|
|
349
|
+
pub fn widget(_props: JsProps) -> Element {
|
|
350
|
+
gl.use_simple(Model(0), update_simple, view)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn update_simple(model: Model, msg: Msg) -> Model {
|
|
354
|
+
case msg {
|
|
355
|
+
Increment -> Model(model.count + 1)
|
|
356
|
+
Decrement -> Model(model.count - 1)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 5.3 Lustre Element를 수동으로 변환 (render)
|
|
362
|
+
|
|
363
|
+
lustre 뷰를 React 트리 안에 삽입할 때 사용합니다:
|
|
364
|
+
|
|
365
|
+
```gleam
|
|
366
|
+
import glendix/lustre as gl
|
|
367
|
+
|
|
368
|
+
let react_element = gl.render(lustre_element, dispatch_fn)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 5.4 redraw Element를 lustre 트리에 삽입 (embed)
|
|
372
|
+
|
|
373
|
+
lustre view 안에서 redraw 컴포넌트를 사용할 때 호출합니다:
|
|
374
|
+
|
|
375
|
+
```gleam
|
|
376
|
+
import glendix/lustre as gl
|
|
377
|
+
import lustre/element/html as lustre_html
|
|
378
|
+
import redraw/dom/attribute
|
|
379
|
+
import redraw/dom/html
|
|
380
|
+
|
|
381
|
+
fn view(model: Model) {
|
|
382
|
+
lustre_html.div([], [
|
|
383
|
+
lustre_html.text("lustre 영역"),
|
|
384
|
+
// redraw 엘리먼트를 lustre 트리에 삽입
|
|
385
|
+
gl.embed(
|
|
386
|
+
html.div([attribute.class("from-redraw")], [
|
|
387
|
+
html.text("redraw로 만든 엘리먼트"),
|
|
388
|
+
]),
|
|
389
|
+
),
|
|
390
|
+
])
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`gl.embed()`은 `redraw.Element` → `lustre/element.Element(msg)` 변환입니다. 변환 시 React 엘리먼트가 그대로 통과되며, lustre의 dispatch에는 참여하지 않습니다.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## 6. 외부 컴포넌트 통합
|
|
399
|
+
|
|
400
|
+
### 6.1 모듈 선택 가이드
|
|
401
|
+
|
|
402
|
+
| 컴포넌트 출처 | 사용 모듈 | 예시 |
|
|
403
|
+
|--------------|----------|------|
|
|
404
|
+
| npm 패키지 (React 컴포넌트) | `glendix/binding` + `glendix/interop` | recharts, @mui |
|
|
405
|
+
| `.mpk` Pluggable 위젯 | `glendix/widget` + `glendix/interop` | Switch.mpk, Badge.mpk |
|
|
406
|
+
| `.mpk` Classic (Dojo) 위젯 | `glendix/classic` | CameraWidget.mpk |
|
|
407
|
+
|
|
408
|
+
### 6.2 외부 React 컴포넌트 (binding + interop)
|
|
409
|
+
|
|
410
|
+
**설정:** `bindings.json` 작성 → `npm install 패키지명` → `gleam run -m glendix/install`
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
{
|
|
414
|
+
"recharts": {
|
|
415
|
+
"components": ["PieChart", "Pie", "Cell", "Tooltip"]
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Gleam 래퍼 작성:**
|
|
421
|
+
|
|
422
|
+
```gleam
|
|
423
|
+
import glendix/binding
|
|
424
|
+
import glendix/interop
|
|
425
|
+
import redraw.{type Element}
|
|
426
|
+
import redraw/dom/attribute.{type Attribute}
|
|
427
|
+
|
|
428
|
+
fn m() { binding.module("recharts") }
|
|
429
|
+
|
|
430
|
+
pub fn pie_chart(attrs: List(Attribute), children: List(Element)) -> Element {
|
|
431
|
+
interop.component_el(binding.resolve(m(), "PieChart"), attrs, children)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
pub fn tooltip(attrs: List(Attribute)) -> Element {
|
|
435
|
+
interop.void_component_el(binding.resolve(m(), "Tooltip"), attrs)
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**interop 함수 시그니처:**
|
|
440
|
+
|
|
441
|
+
| 함수 | 용도 |
|
|
442
|
+
|------|------|
|
|
443
|
+
| `interop.component_el(comp, attrs, children)` | 속성 + 자식 |
|
|
444
|
+
| `interop.component_el_(comp, children)` | 자식만 |
|
|
445
|
+
| `interop.void_component_el(comp, attrs)` | self-closing (자식 없음) |
|
|
446
|
+
|
|
447
|
+
### 6.3 .mpk Pluggable 위젯 (widget + interop)
|
|
448
|
+
|
|
449
|
+
**설정:** `.mpk`를 `widgets/`에 배치 → `gleam run -m glendix/install`
|
|
450
|
+
|
|
451
|
+
자동 생성되는 `src/widgets/*.gleam`:
|
|
452
|
+
|
|
453
|
+
```gleam
|
|
454
|
+
import glendix/interop
|
|
455
|
+
import glendix/mendix
|
|
456
|
+
import glendix/mendix.{type JsProps}
|
|
457
|
+
import glendix/widget
|
|
458
|
+
import redraw.{type Element}
|
|
459
|
+
import redraw/dom/attribute
|
|
460
|
+
|
|
461
|
+
pub fn render(props: JsProps) -> Element {
|
|
462
|
+
let boolean_attribute = mendix.get_prop_required(props, "booleanAttribute")
|
|
463
|
+
let comp = widget.component("Switch")
|
|
464
|
+
interop.component_el(comp, [
|
|
465
|
+
attribute.attribute("booleanAttribute", boolean_attribute),
|
|
466
|
+
], [])
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**위젯 prop 헬퍼:** 코드에서 직접 값을 생성하여 .mpk 위젯에 전달할 때 사용합니다.
|
|
471
|
+
|
|
472
|
+
| 함수 | Mendix 타입 | 용도 |
|
|
473
|
+
|------|------------|------|
|
|
474
|
+
| `widget.prop(key, value)` | DynamicValue | 읽기 전용 (expression, textTemplate) |
|
|
475
|
+
| `widget.editable_prop(key, value, display, set_value)` | EditableValue | 편집 가능한 속성 |
|
|
476
|
+
| `widget.action_prop(key, handler)` | ActionValue | 액션 콜백 (onClick 등) |
|
|
477
|
+
|
|
478
|
+
```gleam
|
|
479
|
+
import glendix/widget
|
|
480
|
+
import glendix/interop
|
|
481
|
+
|
|
482
|
+
let comp = widget.component("Badge button")
|
|
483
|
+
interop.component_el(comp, [
|
|
484
|
+
widget.prop("caption", "제목"),
|
|
485
|
+
widget.editable_prop("textAttr", model.text, model.text, set_text),
|
|
486
|
+
widget.action_prop("onClick", fn() { handle_click() }),
|
|
487
|
+
], [])
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
> Mendix에서 받은 prop (JsProps에서 꺼낸 값)은 이미 올바른 형식이므로 `attribute.attribute(key, value)`로 그대로 전달합니다.
|
|
491
|
+
|
|
492
|
+
### 6.4 Classic (Dojo) 위젯
|
|
493
|
+
|
|
494
|
+
```gleam
|
|
495
|
+
import gleam/dynamic
|
|
496
|
+
import glendix/classic
|
|
497
|
+
|
|
498
|
+
classic.render("CameraWidget.widget.CameraWidget", [
|
|
499
|
+
#("mfToExecute", classic.to_dynamic(mf_value)),
|
|
500
|
+
#("preferRearCamera", classic.to_dynamic(True)),
|
|
501
|
+
])
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
반환 타입: `redraw.Element`
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## 7. Mendix API 레퍼런스
|
|
509
|
+
|
|
510
|
+
### 7.1 Props 접근 (`glendix/mendix`)
|
|
511
|
+
|
|
512
|
+
`JsProps`는 opaque 타입입니다. 접근자 함수로만 읽습니다.
|
|
513
|
+
|
|
514
|
+
```gleam
|
|
515
|
+
import glendix/mendix
|
|
516
|
+
|
|
517
|
+
// Option 반환 (undefined → None)
|
|
518
|
+
mendix.get_prop(props, "myAttr") // Option(a)
|
|
519
|
+
|
|
520
|
+
// 항상 존재하는 prop
|
|
521
|
+
mendix.get_prop_required(props, "name") // a
|
|
522
|
+
|
|
523
|
+
// 문자열 (없으면 "")
|
|
524
|
+
mendix.get_string_prop(props, "caption") // String
|
|
525
|
+
|
|
526
|
+
// 존재 여부
|
|
527
|
+
mendix.has_prop(props, "onClick") // Bool
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 7.2 ValueStatus 확인
|
|
531
|
+
|
|
532
|
+
Mendix의 모든 동적 값은 상태를 가집니다:
|
|
533
|
+
|
|
534
|
+
```gleam
|
|
535
|
+
import glendix/mendix.{Available, Loading, Unavailable}
|
|
536
|
+
|
|
537
|
+
case mendix.get_status(some_value) {
|
|
538
|
+
Available -> // 값 사용 가능
|
|
539
|
+
Loading -> // 로딩 중
|
|
540
|
+
Unavailable -> // 사용 불가
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### 7.3 EditableValue (`glendix/mendix/editable_value`)
|
|
545
|
+
|
|
546
|
+
텍스트, 숫자, 날짜 등 편집 가능한 Mendix 속성:
|
|
547
|
+
|
|
548
|
+
```gleam
|
|
549
|
+
import glendix/mendix/editable_value as ev
|
|
550
|
+
|
|
551
|
+
// 읽기
|
|
552
|
+
ev.value(attr) // Option(a)
|
|
553
|
+
ev.display_value(attr) // String (포맷된 표시값)
|
|
554
|
+
ev.is_editable(attr) // Bool
|
|
555
|
+
ev.validation(attr) // Option(String) — 유효성 검사 메시지
|
|
556
|
+
|
|
557
|
+
// 쓰기
|
|
558
|
+
ev.set_value(attr, Some(new_value))
|
|
559
|
+
ev.set_value(attr, None) // 값 비우기
|
|
560
|
+
ev.set_text_value(attr, "2024-01-15") // 텍스트로 설정 (Mendix 파싱)
|
|
561
|
+
|
|
562
|
+
// 유효성 검사 함수 설정
|
|
563
|
+
ev.set_validator(attr, Some(fn(value) {
|
|
564
|
+
case value {
|
|
565
|
+
Some(v) if v == "" -> Some("값을 입력하세요")
|
|
566
|
+
_ -> None
|
|
567
|
+
}
|
|
568
|
+
}))
|
|
569
|
+
|
|
570
|
+
// 선택 가능한 값 목록 (Enum, Boolean 등)
|
|
571
|
+
ev.universe(attr) // Option(List(a))
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### 7.4 ActionValue (`glendix/mendix/action`)
|
|
575
|
+
|
|
576
|
+
Mendix 마이크로플로우/나노플로우 실행:
|
|
577
|
+
|
|
578
|
+
```gleam
|
|
579
|
+
import glendix/mendix/action
|
|
580
|
+
|
|
581
|
+
action.execute(my_action) // 직접 실행
|
|
582
|
+
action.execute_if_can(my_action) // can_execute가 True일 때만
|
|
583
|
+
action.execute_action(maybe_action) // Option(ActionValue)에서 안전 실행
|
|
584
|
+
|
|
585
|
+
action.can_execute(my_action) // Bool
|
|
586
|
+
action.is_executing(my_action) // Bool
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### 7.5 DynamicValue (`glendix/mendix/dynamic_value`)
|
|
590
|
+
|
|
591
|
+
읽기 전용 표현식 속성:
|
|
592
|
+
|
|
593
|
+
```gleam
|
|
594
|
+
import glendix/mendix/dynamic_value as dv
|
|
595
|
+
|
|
596
|
+
dv.value(expr) // Option(a)
|
|
597
|
+
dv.status(expr) // String
|
|
598
|
+
dv.is_available(expr) // Bool
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### 7.6 ListValue (`glendix/mendix/list_value`)
|
|
602
|
+
|
|
603
|
+
Mendix 데이터 소스 리스트:
|
|
604
|
+
|
|
605
|
+
```gleam
|
|
606
|
+
import glendix/mendix/list_value as lv
|
|
607
|
+
|
|
608
|
+
// 아이템 접근
|
|
609
|
+
lv.items(list_val) // Option(List(ObjectItem))
|
|
610
|
+
|
|
611
|
+
// 페이지네이션
|
|
612
|
+
lv.offset(list_val) // Int
|
|
613
|
+
lv.limit(list_val) // Int
|
|
614
|
+
lv.has_more_items(list_val) // Option(Bool)
|
|
615
|
+
lv.set_offset(list_val, new_offset)
|
|
616
|
+
lv.set_limit(list_val, 20)
|
|
617
|
+
lv.request_total_count(list_val, True)
|
|
618
|
+
lv.total_count(list_val) // Option(Int)
|
|
619
|
+
|
|
620
|
+
// 정렬
|
|
621
|
+
lv.set_sort_order(list_val, [
|
|
622
|
+
lv.sort("Name", lv.Asc),
|
|
623
|
+
lv.sort("CreatedDate", lv.Desc),
|
|
624
|
+
])
|
|
625
|
+
|
|
626
|
+
// 필터링
|
|
627
|
+
lv.set_filter(list_val, Some(filter_condition))
|
|
628
|
+
lv.set_filter(list_val, None) // 필터 해제
|
|
629
|
+
|
|
630
|
+
// 갱신
|
|
631
|
+
lv.reload(list_val)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### 7.7 ListAttribute (`glendix/mendix/list_attribute`)
|
|
635
|
+
|
|
636
|
+
리스트의 각 아이템에서 속성/액션/위젯 추출:
|
|
637
|
+
|
|
638
|
+
```gleam
|
|
639
|
+
import glendix/mendix/list_attribute as la
|
|
640
|
+
|
|
641
|
+
la.get_attribute(attr, item) // EditableValue 반환
|
|
642
|
+
la.get_action(action, item) // Option(ActionValue)
|
|
643
|
+
la.get_expression(expr, item) // DynamicValue
|
|
644
|
+
la.get_widget(widget, item) // Element (직접 렌더링)
|
|
645
|
+
|
|
646
|
+
// 메타데이터
|
|
647
|
+
la.attr_id(attr) // String
|
|
648
|
+
la.attr_sortable(attr) // Bool
|
|
649
|
+
la.attr_filterable(attr) // Bool
|
|
650
|
+
la.attr_type(attr) // "String", "Integer" 등
|
|
651
|
+
la.attr_formatter(attr) // ValueFormatter
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### 7.8 Selection (`glendix/mendix/selection`)
|
|
655
|
+
|
|
656
|
+
```gleam
|
|
657
|
+
import glendix/mendix/selection
|
|
658
|
+
|
|
659
|
+
// 단일 선택
|
|
660
|
+
selection.selection(single_sel) // Option(ObjectItem)
|
|
661
|
+
selection.set_selection(single_sel, Some(item))
|
|
662
|
+
selection.set_selection(single_sel, None)
|
|
663
|
+
|
|
664
|
+
// 다중 선택
|
|
665
|
+
selection.selections(multi_sel) // List(ObjectItem)
|
|
666
|
+
selection.set_selections(multi_sel, [item1, item2])
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### 7.9 Reference / ReferenceSet
|
|
670
|
+
|
|
671
|
+
```gleam
|
|
672
|
+
import glendix/mendix/reference as ref
|
|
673
|
+
import glendix/mendix/reference_set as ref_set
|
|
674
|
+
|
|
675
|
+
// 단일 참조
|
|
676
|
+
ref.value(my_ref) // Option(a)
|
|
677
|
+
ref.read_only(my_ref) // Bool
|
|
678
|
+
ref.validation(my_ref) // Option(String)
|
|
679
|
+
ref.set_value(my_ref, Some(item))
|
|
680
|
+
|
|
681
|
+
// 다중 참조
|
|
682
|
+
ref_set.value(my_ref_set) // Option(List(a))
|
|
683
|
+
ref_set.set_value(my_ref_set, Some([item1, item2]))
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 7.10 Filter (`glendix/mendix/filter`)
|
|
687
|
+
|
|
688
|
+
```gleam
|
|
689
|
+
import glendix/mendix/filter
|
|
690
|
+
|
|
691
|
+
// 비교 연산
|
|
692
|
+
filter.equals(filter.attribute("Status"), filter.literal("Active"))
|
|
693
|
+
filter.contains(filter.attribute("Name"), filter.literal("검색어"))
|
|
694
|
+
filter.greater_than(filter.attribute("Amount"), filter.literal(100))
|
|
695
|
+
// 그 외: not_equal, greater_than_or_equal, less_than, less_than_or_equal, starts_with, ends_with
|
|
696
|
+
|
|
697
|
+
// 날짜 비교
|
|
698
|
+
filter.day_equals(filter.attribute("Birthday"), filter.literal(date))
|
|
699
|
+
|
|
700
|
+
// 논리 조합
|
|
701
|
+
filter.and_([condition1, condition2])
|
|
702
|
+
filter.or_([condition1, condition2])
|
|
703
|
+
filter.not_(condition)
|
|
704
|
+
|
|
705
|
+
// 표현식
|
|
706
|
+
filter.attribute("AttrName") // 속성 참조
|
|
707
|
+
filter.association("AssocName") // 연관 관계
|
|
708
|
+
filter.literal(value) // 상수 값
|
|
709
|
+
filter.empty() // null 비교용
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### 7.11 날짜 (`glendix/mendix/date`)
|
|
713
|
+
|
|
714
|
+
> Gleam month는 1-based (1~12), JS는 0-based. glendix가 자동 변환합니다.
|
|
715
|
+
|
|
716
|
+
```gleam
|
|
717
|
+
import glendix/mendix/date
|
|
718
|
+
|
|
719
|
+
date.now()
|
|
720
|
+
date.from_iso("2024-03-15T10:30:00Z")
|
|
721
|
+
date.create(2024, 3, 15, 10, 30, 0, 0) // month: 1-12
|
|
722
|
+
|
|
723
|
+
date.year(d) // Int
|
|
724
|
+
date.month(d) // 1~12
|
|
725
|
+
date.day(d) // 1~31
|
|
726
|
+
date.hours(d) // 0~23
|
|
727
|
+
|
|
728
|
+
date.to_iso(d) // "2024-03-15T10:30:00.000Z"
|
|
729
|
+
date.to_timestamp(d) // Unix 밀리초
|
|
730
|
+
date.to_input_value(d) // "2024-03-15" (input[type="date"]용)
|
|
731
|
+
date.from_input_value(s) // Option(JsDate)
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### 7.12 Big (`glendix/mendix/big`)
|
|
735
|
+
|
|
736
|
+
Big.js 래퍼. Mendix Decimal 타입 처리용:
|
|
737
|
+
|
|
738
|
+
```gleam
|
|
739
|
+
import glendix/mendix/big
|
|
740
|
+
|
|
741
|
+
big.from_string("123.456")
|
|
742
|
+
big.from_int(100)
|
|
743
|
+
|
|
744
|
+
big.add(a, b) big.subtract(a, b)
|
|
745
|
+
big.multiply(a, b) big.divide(a, b)
|
|
746
|
+
big.absolute(a) big.negate(a)
|
|
747
|
+
|
|
748
|
+
big.compare(a, b) // gleam/order.Order
|
|
749
|
+
big.equal(a, b) // Bool
|
|
750
|
+
|
|
751
|
+
big.to_string(a) big.to_float(a)
|
|
752
|
+
big.to_int(a) big.to_fixed(a, 2)
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### 7.13 File, Icon, Formatter
|
|
756
|
+
|
|
757
|
+
```gleam
|
|
758
|
+
// FileValue / WebImage
|
|
759
|
+
import glendix/mendix/file
|
|
760
|
+
file.uri(file_val) // String
|
|
761
|
+
file.name(file_val) // Option(String)
|
|
762
|
+
file.image_uri(img) // String
|
|
763
|
+
file.alt_text(img) // Option(String)
|
|
764
|
+
|
|
765
|
+
// WebIcon
|
|
766
|
+
import glendix/mendix/icon
|
|
767
|
+
icon.icon_type(i) // Glyph | Image | IconFont
|
|
768
|
+
icon.icon_class(i) // String
|
|
769
|
+
icon.icon_url(i) // String
|
|
770
|
+
|
|
771
|
+
// ValueFormatter
|
|
772
|
+
import glendix/mendix/formatter
|
|
773
|
+
formatter.format(fmt, Some(value)) // String
|
|
774
|
+
formatter.parse(fmt, "123.45") // Result(Option(a), Nil)
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
## 8. Editor Configuration (`glendix/editor_config`)
|
|
780
|
+
|
|
781
|
+
Studio Pro의 editorConfig 로직을 Gleam으로 작성합니다.
|
|
782
|
+
|
|
783
|
+
> **Jint 제약**: Studio Pro는 Jint(.NET JS 엔진)으로 실행합니다. **Gleam List 사용 금지** — `["a", "b"]` 같은 리스트 리터럴은 Jint에서 크래시. 여러 키는 **콤마 구분 String**을 사용합니다.
|
|
784
|
+
|
|
785
|
+
```gleam
|
|
786
|
+
import glendix/editor_config.{type Properties}
|
|
787
|
+
import glendix/mendix
|
|
788
|
+
import glendix/mendix.{type JsProps}
|
|
789
|
+
|
|
790
|
+
const bar_keys = "barWidth,barColor"
|
|
791
|
+
const line_keys = "lineStyle,lineCurve"
|
|
792
|
+
|
|
793
|
+
pub fn get_properties(
|
|
794
|
+
values: JsProps,
|
|
795
|
+
default_properties: Properties,
|
|
796
|
+
platform: String,
|
|
797
|
+
) -> Properties {
|
|
798
|
+
let chart_type = mendix.get_string_prop(values, "chartType")
|
|
799
|
+
|
|
800
|
+
let props = case chart_type {
|
|
801
|
+
"line" -> editor_config.hide_properties(default_properties, bar_keys)
|
|
802
|
+
"bar" -> editor_config.hide_properties(default_properties, line_keys)
|
|
803
|
+
_ -> default_properties
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
case platform {
|
|
807
|
+
"web" -> editor_config.transform_groups_into_tabs(props)
|
|
808
|
+
_ -> props
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**함수 목록:**
|
|
814
|
+
|
|
815
|
+
| 함수 | 설명 |
|
|
816
|
+
|------|------|
|
|
817
|
+
| `hide_property(props, key)` | 단일 속성 숨기기 |
|
|
818
|
+
| `hide_properties(props, keys)` | 여러 속성 숨기기 (콤마 구분) |
|
|
819
|
+
| `hide_nested_property(props, key, index, nested_key)` | 중첩 속성 숨기기 |
|
|
820
|
+
| `hide_nested_properties(props, key, index, nested_keys)` | 여러 중첩 속성 (콤마 구분) |
|
|
821
|
+
| `transform_groups_into_tabs(props)` | 그룹 → 탭 변환 |
|
|
822
|
+
| `move_property(props, from_idx, to_idx)` | 속성 순서 변경 |
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
## 9. JS Interop Escape Hatch (`glendix/js/*`)
|
|
827
|
+
|
|
828
|
+
외부 JS 라이브러리(SpreadJS, Chart.js 등)와 직접 상호작용할 때 사용합니다. 모든 값은 `Dynamic` 타입. 가능하면 `glendix/binding`을 먼저 고려하세요.
|
|
829
|
+
|
|
830
|
+
```gleam
|
|
831
|
+
// 배열 변환
|
|
832
|
+
import glendix/js/array
|
|
833
|
+
array.from_list([1, 2, 3]) // Gleam List → JS Array (Dynamic)
|
|
834
|
+
array.to_list(js_arr) // JS Array → Gleam List
|
|
835
|
+
|
|
836
|
+
// 객체
|
|
837
|
+
import glendix/js/object
|
|
838
|
+
object.object([#("width", dynamic.from(800))])
|
|
839
|
+
object.get(obj, "key")
|
|
840
|
+
object.set(obj, "key", dynamic.from(val))
|
|
841
|
+
object.call_method(obj, "method", [arg1, arg2])
|
|
842
|
+
|
|
843
|
+
// JSON
|
|
844
|
+
import glendix/js/json
|
|
845
|
+
json.stringify(dynamic.from(data)) // String
|
|
846
|
+
json.parse("{\"k\":\"v\"}") // Result(Dynamic, String)
|
|
847
|
+
|
|
848
|
+
// Promise
|
|
849
|
+
import glendix/js/promise
|
|
850
|
+
import gleam/javascript/promise.{type Promise}
|
|
851
|
+
promise.resolve(42)
|
|
852
|
+
promise.then_(p, fn(v) { promise.resolve(transform(v)) })
|
|
853
|
+
promise.all([p1, p2])
|
|
854
|
+
promise.race([p1, p2])
|
|
855
|
+
|
|
856
|
+
// DOM
|
|
857
|
+
import glendix/js/dom
|
|
858
|
+
dom.focus(element)
|
|
859
|
+
dom.blur(element)
|
|
860
|
+
dom.scroll_into_view(element)
|
|
861
|
+
dom.query_selector(container, ".target") // Option(Dynamic)
|
|
862
|
+
|
|
863
|
+
// Timer
|
|
864
|
+
import glendix/js/timer
|
|
865
|
+
let id = timer.set_timeout(fn() { Nil }, 1000)
|
|
866
|
+
timer.clear_timeout(id)
|
|
867
|
+
let id = timer.set_interval(fn() { Nil }, 500)
|
|
868
|
+
timer.clear_interval(id)
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## 10. 빌드 & 도구
|
|
874
|
+
|
|
875
|
+
| 명령어 | 설명 |
|
|
876
|
+
|--------|------|
|
|
877
|
+
| `gleam build` | 컴파일 |
|
|
878
|
+
| `gleam run -m glendix/install` | 의존성 + 바인딩 + 위젯 .gleam 생성 |
|
|
879
|
+
| `gleam run -m glendix/dev` | 개발 서버 (HMR) |
|
|
880
|
+
| `gleam run -m glendix/build` | 프로덕션 빌드 (.mpk) |
|
|
881
|
+
| `gleam run -m glendix/start` | Mendix 테스트 프로젝트 연동 |
|
|
882
|
+
| `gleam run -m glendix/release` | 릴리즈 빌드 |
|
|
883
|
+
| `gleam run -m glendix/lint` | ESLint 검사 |
|
|
884
|
+
| `gleam run -m glendix/lint_fix` | ESLint 자동 수정 |
|
|
885
|
+
| `gleam run -m glendix/marketplace` | Marketplace 위젯 다운로드 (인터랙티브) |
|
|
886
|
+
| `gleam run -m glendix/define` | 위젯 프로퍼티 정의 TUI 에디터 |
|
|
887
|
+
|
|
888
|
+
**PM 자동 감지:** `pnpm-lock.yaml` → pnpm / `bun.lockb`·`bun.lock` → bun / 기본값 → npm
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## 11. 실전 패턴
|
|
893
|
+
|
|
894
|
+
### 11.1 폼 입력 위젯
|
|
895
|
+
|
|
896
|
+
```gleam
|
|
897
|
+
import gleam/option.{None, Some}
|
|
898
|
+
import glendix/mendix
|
|
899
|
+
import glendix/mendix.{type JsProps}
|
|
900
|
+
import glendix/mendix/action
|
|
901
|
+
import glendix/mendix/editable_value as ev
|
|
902
|
+
import redraw.{type Element}
|
|
903
|
+
import redraw/dom/attribute
|
|
904
|
+
import redraw/dom/events
|
|
905
|
+
import redraw/dom/html
|
|
906
|
+
|
|
907
|
+
pub fn text_input_widget(props: JsProps) -> Element {
|
|
908
|
+
let attr = mendix.get_prop(props, "textAttribute")
|
|
909
|
+
let on_enter = mendix.get_prop(props, "onEnterAction")
|
|
910
|
+
let placeholder = mendix.get_string_prop(props, "placeholder")
|
|
911
|
+
|
|
912
|
+
case attr {
|
|
913
|
+
Some(text_attr) -> {
|
|
914
|
+
let display = ev.display_value(text_attr)
|
|
915
|
+
let editable = ev.is_editable(text_attr)
|
|
916
|
+
let validation = ev.validation(text_attr)
|
|
917
|
+
|
|
918
|
+
html.div([attribute.class("form-group")], [
|
|
919
|
+
html.input([
|
|
920
|
+
attribute.class("form-control"),
|
|
921
|
+
attribute.value(display),
|
|
922
|
+
attribute.placeholder(placeholder),
|
|
923
|
+
attribute.readonly(!editable),
|
|
924
|
+
events.on_change(fn(_e) {
|
|
925
|
+
ev.set_text_value(text_attr, display)
|
|
926
|
+
}),
|
|
927
|
+
events.on_key_down(fn(_e) {
|
|
928
|
+
action.execute_action(on_enter)
|
|
929
|
+
}),
|
|
930
|
+
]),
|
|
931
|
+
case validation {
|
|
932
|
+
Some(msg) ->
|
|
933
|
+
html.div([attribute.class("alert alert-danger")], [
|
|
934
|
+
html.text(msg),
|
|
935
|
+
])
|
|
936
|
+
None -> html.none()
|
|
937
|
+
},
|
|
938
|
+
])
|
|
939
|
+
}
|
|
940
|
+
None -> html.none()
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### 11.2 데이터 테이블 위젯
|
|
946
|
+
|
|
947
|
+
```gleam
|
|
948
|
+
import gleam/list
|
|
949
|
+
import gleam/option.{None, Some}
|
|
950
|
+
import glendix/mendix
|
|
951
|
+
import glendix/mendix.{type JsProps}
|
|
952
|
+
import glendix/mendix/editable_value as ev
|
|
953
|
+
import glendix/mendix/list_attribute as la
|
|
954
|
+
import glendix/mendix/list_value as lv
|
|
955
|
+
import redraw.{type Element}
|
|
956
|
+
import redraw/dom/attribute
|
|
957
|
+
import redraw/dom/html
|
|
958
|
+
|
|
959
|
+
pub fn data_table(props: JsProps) -> Element {
|
|
960
|
+
let ds = mendix.get_prop_required(props, "dataSource")
|
|
961
|
+
let col_name = mendix.get_prop_required(props, "nameColumn")
|
|
962
|
+
|
|
963
|
+
html.table([attribute.class("table")], [
|
|
964
|
+
html.tbody([], case lv.items(ds) {
|
|
965
|
+
Some(items) ->
|
|
966
|
+
list.map(items, fn(item) {
|
|
967
|
+
let id = mendix.object_id(item)
|
|
968
|
+
let name = ev.display_value(la.get_attribute(col_name, item))
|
|
969
|
+
html.tr([attribute.key(id)], [
|
|
970
|
+
html.td([], [html.text(name)]),
|
|
971
|
+
])
|
|
972
|
+
})
|
|
973
|
+
None -> [html.tr([], [html.td([], [html.text("로딩 중...")])])]
|
|
974
|
+
}),
|
|
975
|
+
])
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### 11.3 검색 가능한 리스트
|
|
980
|
+
|
|
981
|
+
```gleam
|
|
982
|
+
import gleam/option.{None, Some}
|
|
983
|
+
import glendix/mendix
|
|
984
|
+
import glendix/mendix.{type JsProps}
|
|
985
|
+
import glendix/mendix/filter
|
|
986
|
+
import glendix/mendix/list_value as lv
|
|
987
|
+
import redraw.{type Element}
|
|
988
|
+
import redraw/dom/attribute
|
|
989
|
+
import redraw/dom/events
|
|
990
|
+
import redraw/dom/html
|
|
991
|
+
|
|
992
|
+
pub fn searchable_list(props: JsProps) -> Element {
|
|
993
|
+
let ds = mendix.get_prop_required(props, "dataSource")
|
|
994
|
+
let search_attr = mendix.get_string_prop(props, "searchAttribute")
|
|
995
|
+
let #(query, set_query) = redraw.use_state("")
|
|
996
|
+
|
|
997
|
+
redraw.use_effect(fn() {
|
|
998
|
+
case query {
|
|
999
|
+
"" -> lv.set_filter(ds, None)
|
|
1000
|
+
q -> lv.set_filter(ds, Some(
|
|
1001
|
+
filter.contains(filter.attribute(search_attr), filter.literal(q)),
|
|
1002
|
+
))
|
|
1003
|
+
}
|
|
1004
|
+
Nil
|
|
1005
|
+
}, query)
|
|
1006
|
+
|
|
1007
|
+
html.div([], [
|
|
1008
|
+
html.input([
|
|
1009
|
+
attribute.type_("search"),
|
|
1010
|
+
attribute.placeholder("검색..."),
|
|
1011
|
+
attribute.value(query),
|
|
1012
|
+
events.on_change(fn(_e) { set_query(query) }),
|
|
1013
|
+
]),
|
|
1014
|
+
// ... 결과 렌더링
|
|
1015
|
+
])
|
|
1016
|
+
}
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
## 12. 절대 하지 말 것
|
|
1022
|
+
|
|
1023
|
+
| 실수 | 올바른 방법 |
|
|
1024
|
+
|------|------------|
|
|
1025
|
+
| `import glendix/react` | **삭제됨.** `import redraw` 사용 |
|
|
1026
|
+
| 조건 안에서 Hook 호출 | Hook은 항상 함수 최상위에서 호출 |
|
|
1027
|
+
| `html.text("")`로 빈 렌더링 | `html.none()` 사용 |
|
|
1028
|
+
| `binding.resolve(m(), "pie_chart")` | JS 원본 이름 유지: `"PieChart"` |
|
|
1029
|
+
| 외부 React 컴포넌트용 `.mjs` 직접 작성 | `bindings.json` + `glendix/binding` 사용 |
|
|
1030
|
+
| `.mpk` 위젯용 `.mjs` 직접 작성 | `widgets/` + `glendix/widget` 사용 |
|
|
1031
|
+
| `date.month()`에 0-based 값 전달 | glendix가 1↔0 자동 변환 |
|
|
1032
|
+
| Editor config에서 Gleam List 사용 | 콤마 구분 String 사용 (Jint 호환) |
|
|
1033
|
+
| FFI `.mjs`에 비즈니스 로직 | `.gleam`에 작성. `.mjs`는 JS 런타임 접근만 |
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## 13. 트러블슈팅
|
|
1038
|
+
|
|
1039
|
+
| 문제 | 원인 | 해결 |
|
|
1040
|
+
|------|------|------|
|
|
1041
|
+
| `react is not defined` | peer dependency 미설치 | `gleam run -m glendix/install` |
|
|
1042
|
+
| `Cannot read property of undefined` | 없는 prop 접근 | `get_prop` (Option) 사용, prop 이름 확인 |
|
|
1043
|
+
| Hook 순서 에러 | 조건부 Hook 호출 | 항상 동일 순서로 호출 (React Rules) |
|
|
1044
|
+
| 바인딩 미생성 | `binding_ffi.mjs` 스텁 상태 | `gleam run -m glendix/install` |
|
|
1045
|
+
| 위젯 바인딩 미생성 | `widget_ffi.mjs` 스텁 상태 | `widgets/`에 `.mpk` 배치 후 install |
|
|
1046
|
+
| `could not be resolved` | npm 패키지 미설치 | `npm install <패키지명>` |
|
|
1047
|
+
| `.env` PAT 오류 | marketplace 인증 실패 | [Developer Settings](https://user-settings.mendix.com/link/developersettings)에서 PAT 재발급 |
|
|
1048
|
+
| Playwright 오류 | chromium 미설치 | `npx playwright install chromium` |
|
|
1049
|
+
|
|
1050
|
+
---
|