-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcomponentCore.mjs
More file actions
1234 lines (1074 loc) · 61 KB
/
componentCore.mjs
File metadata and controls
1234 lines (1074 loc) · 61 KB
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
56
57
58
59
60
61
62
63
64
65
66
67
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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
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
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
672
673
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
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
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { BGError } from './BGError.mjs';
import { ComponentParams } from './ComponentParams.mjs';
import { Component } from './component.mjs';
import { domTreeChanges } from './DomHooks.mjs';
import icons from '@primer/octicons';
// make aliases for some icon names
icons.close = icons.x;
// Library componentCore
// This library implements the core bgdom component functionality that is used by the Component and Button (and other?) class
// hierarchies.
//
// bgComponents extend DOM nodes by creating a JS object with an 'el' property that points to its corresponding DOM node. A GC
// friendly form of backlink from the DOM node to the JS object is provided by a WeakMap. This library provides functions that
// miorror the DOM functions to build the hierarchy that will accept either a native DOMNode (ie. Element) or a java object linked
// to a native DOMNode. Many DOMNodes will not have corresponding 'extra' objects but every 'extra' object (aka BGNode) will be linked
// to a DOMNode.
//
// A major motivation is to allow bgComponents to have named children to make writing functions and methods more natural. For eample,
// a view bgComponent could have named members for the various children that make up the view so that they can easily be scripted
// to provide functionality. myView.expandBtn.click().
//
// Terminology:
// DOMNode : the native JS object used by the browser. i.e. document.createElement('div') returns a DOMNode.
// BGNode : a JS object that extends a DOMNode. Its 'el' property points to the DOMNode. a weakmap allows ComponentToBG to
// navigate from a DOMNode to its BGNode if one exists.
// The BGNode objects created by this library additionally has the bgComponent Symbol set which indicates that it
// opts into the full navigation system.
// BGComp : can be either a BGNode or a DOMNode. Its imporatant for the user of this API to be able to work with nodes logically
// without having to constantly deal with two objects so functions are typically written to receive and return BGComp
// objects, and code that receives a BGComp typically uses [myBGNode,myDOMNode,myBGComp] = ComponentNormalize(myBGComp)
// when it needs to access features specific to the DOM or internal BG system members.
// You can think of it this way. Either a BGNode or DOMNode JS object identifies the logical node but when you want
// to use a DOM feature or member variables that are only in the BGNode, you need to work with one specific JS Object
// or the other.
//
// Hierarchies:
// DOMNodes form a well understood tree hierachy. BGNodes form a sparse reflection of the DOM tree hierarchy. Its sparse in two ways.
// First, in the horizontal dimension, when traversing the children of a BGNode, its DOMNode children that do not have BGNodes will
// not be traversed. Second, in the vertical dimension, when traversing the direct children of a BGNode, the DOMNode of a child
// might be an arbitrarily deep descendant of the DOMNode of the parent. i.e. the BGNode child can skip over some of the parent
// chain of the DOMNodes.
//
// The significance of the difference between the DOMNode hierarchy and the BGNode hierarchy is that the DOM hierarchy necessarily
// reflects the nuances of presentation whereas the BGNode hierarchy is free to more directly represent the logical structure of
// the data.
// Example -- A view component node has a checkbox input to toggle some state. Logically, the input is a direct descendant of the
// view but in order to display it correctly, with ARIA support, we decide to nest the <input> DOMNode within a <label> DOM node.
// furthermore, we may decide that we have to put the <label> node inside a <div> or <span> to achieve some presentation objective.
// The code that scripts the view should not have to change based on how we structure the DOMNode for presentation.
//
// We often think about HTML defining the structure of the DOM tree and CSS defines the presentation but any web designer knows that
// that separation is not actually independent. This allows another level of separation so that the BGNode hierarchy can more
// precisely represent the logical structure.
//
// BGNode <--> DOMNode Navigation:
// Given a BGNode we can get to its corresponding DOMNode (aka element) via its 'el' member variable. A BGNode must have an 'el'.
// The function 'DOMNode ComponentToEl(BGComp)' provides an abstraction for this navigation that also provides error checking and
// the property of idempotency, meaning that if BGComp has already been converted to its DOMNode it will return it unchanged.
//
// Given a DOMNode we can navigate to to its corresponding BGNode using a WeakMap. This allows us to leave the browser's DOMNode
// object untouched and also will not prevent Garbage Collection (GC) from recovering the pair of objects if nothing else references
// them.
//
// Duck Typing and Opting into the Full Navigation:
// A BGNode JS object does not have to be any particular type of object. This library duck types them. Many functions of this library
// will use the 'el' member variable of an object even if that object was not created by this library.
// However, features that modify the object passed in will not do so unless the object has the bgComponent Symbol set. You can allow
// third party objects to fully participat in the features of this library by assigning is bgComponent Symbol property.
// e.g. myObj[bgComponent] = true
//
// DOM Modification:
// Even though this library does not modify each individual DOMObject that it works with, the library does modify the DOM's Node
// and Element prototypes. It does this because it is the only way I can think of to generate efficient triggers for lifecyle
// methods (like onMount, onUnmount, etc...)
//
// Adding Properties and Attributes:
// Attributes are the HTML side of member variables and properties are the Javascript side. Properties are normal javascript member
// variables. Attributes are accessed with <DOMNode>.(has|get|set|remove)Attibutes and the <DOMNode>.attributes array. Attributes
// are always strings and properties can be any javascript type.
//
// We can think of each DOMNode having a fixed set of standard attributes based on it type (aka nodeName). Those attributes (with at
// least one exception) are automatically kept in sync for us by the DOM. We can read or write either way and they both will have
// the same value (except the attribute is always the string representation of the value).
//
// The 'value' attribute of <input> elements is one exception. Changes only flow from attribute to java object.
//
// We can add both custom attributes and custom java object properties but they will not be kept in sync by the DOM. Since this library
// links an additional javascript object to the DOMNode javascript object, we can put our custom properties there. We typically do not
// sync those with either the DOMNode javascript object nor its attributes because its easy and efficient to get the BGNode (the
// extra object) for a DOMNode.
//
// There are two reasons I know of that we might want to add a custom attribute. If the component is being created from html text,
// only custom attributes can be added in the tag text. (e.g. <div myCustomName="foo"). Second, custom attributes can be accessed
// in css via selectors like div[myCustomName="foo"]. Using a custom attribute like this is similar to using a class but whereas
// classes are single tags, attributes are name value pairs.
//
// Custom attribute names that start with 'data-' are treated specially by the DOM and are sync'd between corresponding javascript
// properties in e.dataset.<name> where name is the attribute name with 'data-' removed. Note that attribute names are case
// insensitive. You can prefix a character in the dataset attribute name with '-' to make its corresponding character in el.dataset.<name>
// Capital. For example, the attribute 'data-my-var' would be el.dataset.myVar
// Symbols that we use to identify participating objects and to record their non-DOM parent and the name that that parent uses for it
export const bgComponent =Symbol.for('bgComponent');
export const bgComponentName =Symbol.for('bgComponentName');
export const bgComponentParent =Symbol.for('bgComponentParent');
// compiled common Regular Expressions
export const reEmpty = /^\s*$/;
export const reHTMLContent = /^\s*<[^>]+>/;
export const reVarName = /^[_a-zA-Z][_a-zA-Z0-9]*(\[.*\])?$/;
const xlinkns = 'http://www.w3.org/1999/xlink';
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Navigating the DOM tree.
// usage: <DOMNode> ComponentToEl(<BGComp> bgComp)
// return the DOMNode associated with the object passed in. Use this instead of <bgComp>.el when <bgComp> is of unknown type and
// might already be a DOMNode.
// Note that the BGNodes created by this library can be identified by having the bgComponent property set but this function does
// not care if bgComp is one of ours. If it has an el that is a DOMNode, we're good.
// Params:
// <BGComp> : this can be either a DOMNode already or an object with an <bgComp>.el which is a DOMNode
export function ComponentToEl(bgComp)
{
if (bgComp) {
if (bgComp && bgComp.nodeType) return bgComp;
if (bgComp && bgComp.el && bgComp.el.nodeType) return bgComp.el;
}
throw new BGError("bgComp could not be identified as anything that leads to a DOMNode", {bgComp});
}
// usage: BGNode ComponentToBG(<bgComp>, <forceFlag>)
// return the BGNode object associated with <el> (the DOMNode). If there is no BGNode object for this DOMNode, the <forceFlag>
// determines what to return.
//
// How We Associate BGNodes With DOMNodes:
// To be able to navigate from a DOMNode to its corresponding BGNode, we have to extend the DOMNode. Tradititionally it is problematic
// to extend the browser's objects but JS now has several facilities that make it more robust.
// A bgComponent can be a DOM node/element or an object that has an 'el' property that is the DOM node/element. Given an Object with
// an 'el' property, the el property navigates to the DOM node/element. To go the other way, we use a WeakMap instead of extending the
// DOM node. An alternate implementation would be to add a WeakRef property to the DOM node/element that points back to the associated
// object but WeakRef is not yet available in Atom's nodejs version.
//
// Params:
// <bgComp> : typically a DOMNode but could also be a BGNode. If its not either of these it will throw an exeception regardless
// of the value of <forceFlag>
// <forceFlag> : indicate what to return if there is no BGNode for a DOMNode passed in
// (default) : throw exception
// 'null' : return null
// 'force' : return <bgComp> even though its a DOMNode and not a BGNode
// 'climb' : climb up the parent chain of bgComp to find a BGNode
// 'create' : create a new BGNode for this DOMNode
export function ComponentToBG(bgComp, forceFlag) {
// we cant do nothin with nothin
if (!bgComp)
throw new BGError("the bgComp passed in is null or undefined", {bgComp});
// its already a BGNode
if (('el' in bgComp) && ('nodeType' in bgComp.el))
return bgComp;
// this is the typical, direct link
var ret=ComponentMap.get(bgComp);
if (ret)
return ret;
// as long as bcomp is a DOMNode, its ok that we did not find a BGNode
if (!('nodeType' in bgComp))
throw new BGError("the bgComp passed in is neither a BGNode nor DOMNode", {bgComp});
// now, what do do if there is no direct link
switch (forceFlag) {
case 'climb':
if (!('parentElement' in bgComp))
throw Error("bgComp is not a DOM Node because it has no parentElement member");
console.assert(false, "please confirm that the 'climb' option to ComponentToBG is working and then remove this assert and the error msg below it");
var p=bgComp;
while ((p=p.parentElement) && !(ret=ComponentMap.get(p)));
console.warn("check this. ComponentToBG()", {passedIn:bgComp, foundViaClimb:ret});
return ret;
case 'force':
return bgComp;
case 'null':
return null;
case 'create':
ret = new Component(Component.wrapNode, bgComp);
return ret;
default:
throw new BGError("the bgComp does not have a corresponding BGNode object", {bgComp});
}
}
export const ComponentMap = new WeakMap();
// usage: <BGComp> ComponentGetParent(bgComp)
// returns the best bgComp parent of bgComp
// it tries to return a BGNode but will return a DOMNode if it has to.
// if bgComp is null it throws an exception
// if no parent is found it returns null because unmounted BGComps do not have parents
export function ComponentGetParent(bgComp) {
if (!bgComp)
throw new BGError("the bgComp passed in is null or undefined", {bgComp});
const [bgObj, bgEl] = ComponentNormalize(bgComp);
if (bgObj && (bgComponentParent in bgObj)) return bgObj[bgComponentParent];
if (bgObj && bgObj.parent) return bgObj.parent;
if (bgEl && bgEl.parentElement) return ComponentToBG(bgEl.parentElement, "force");
return null;
}
// pass in bgComp as either the extra object associated with a DOM object or as the DOM object itself and it returns
// [<obj>,<el>,<either>] where <obj> or <el> may be null but <either> will never be null. If both exist, <either> will be <obj>
//
// Params:
// <bgComp> : a reference to a bgComponent which can be the DOM element or the business logic object associated with it
// <forceFlag> : default false. If true the <obj> returned in the first array position will be <either> instead of <obj>
// Return Value:
// [obj,el,either] : where....
// <obj> : is the extra object associated with the DOM object (or null if there is none)
// <el> : is the DOM object itself.
// <either> : is <obj> if it exists or <el> otherwise so that <either> is non-null unless <bgComp> is null
export function ComponentNormalize(bgComp) {
// ComponentNormalize is idempotent so if an array is passed in, we can quickly return it. If the array only has two elements,
// fill in the thrid <either> element so that ComponentNormalize always returns an rray with 3 elements.
if (bgComp && Array.isArray(bgComp)) {
if (bgComp.length == 2)
bgComp.push(bgComp[0] || bgComp[1]);
return bgComp;
}
if (bgComp && (bgComponent in bgComp))
return [bgComp, bgComp.el, bgComp]
const el = ComponentToEl(bgComp);
const obj = ComponentToBG(el, 'null');
return [obj, el, obj||el]
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Get the ID/name of a bgComp (which can be either a DOMNode or a BGNode).
// usage: <string> NodeTraceName(node)
// return a human friendly name for <node>.
// Params:
// <node> : can be either a DOMNode or the extra obj (bgNode) that this library maintains for some DOMNodes
export function NodeTraceName(node)
{
if (!node)
return typeof node // null or undefined
if (typeof node == 'string')
return "(name:"+node+")"
if (bgComponent in node)
return "" +(node.constructor.name)+ ":" +(node[bgComponentName] || node.label)+ ":ch:" +node.mounted.length+ "/" +node.mountedUnamed.length+ "";
if (typeof node == 'object')
return ""+ (node.constructor.name)+ ":" +node.tagName+ "." +node.className;
else
return "(unexpected type)" + typeof node + ": " + node;
}
// usage: <string> ComponentGetName(<BGCComp> bgComp, <boolean> options)
// return the best name for the bgComp. If is has a name field, use that, otherwise use the makeTagIDClasses algorithm
export function ComponentGetName(bgComp, options={})
{
if (!bgComp)
return '';
const [bgObj, bgEl] = ComponentNormalize(bgComp);
// if there are both bgEl.id and bgObj[bgComponentName], they should be the same so it does not matter which we choose
if (bgObj && (bgComponentName in bgObj) && bgObj[bgComponentName])
return bgObj[bgComponentName];
else if (bgEl && bgEl.id)
return bgEl.id;
else if (bgObj && typeof bgObj.name=='string' && bgObj.name)
return bgObj.name;
else if (options.bgnameOnly)
return null;
else
return ComponentParams.makeTagIDClasses(bgEl, options);
}
// return the hierarchical name of this bgComp in the DOM
// The idea is that this name should be enough to look up the bgComp in the DOM.
// The root of the hierarchical name will either be the $html (the root node of the document) node or a node that has an #id since
// #id is unique in the document. Either way, the mounted name is an identifier that can be used to retrieve the node/element
export function ComponentGetMountedName(bgComp, options={})
{
var mountedName = ComponentGetName(bgComp, options);
var p=ComponentGetParent(bgComp);
while (p && ! mountedName.startsWith("#")) {
var pEl = ComponentToEl(p);
if (pEl && pEl.id)
mountedName = "#"+pEl.id + '/' + mountedName;
else
mountedName = ComponentGetName(p, options) + '/' + mountedName;
p=ComponentGetParent(p);
}
return mountedName;
}
export function ComponentGet(mountedName)
{
var parts = mountedName.split('/');
var startPart = parts.shift();
var resultObj = null, resultEl = null, $result = null;
if (startPart == "$html")
[resultObj, resultEl, $result] = ComponentNormalize(document.getElementsByTagName('html')[0]);
else if (startPart.startsWith("#"))
[resultObj, resultEl, $result] = ComponentNormalize(document.getElementById(startPart.replace(/^#/,"")));
// TODO: consider crafting a selector to pass to resultEl.querySelector() instead of iterating.
while (parts.length > 0) {
var part = parts.shift();
if (resultObj && (part in resultObj))
[resultObj, resultEl, $result] = ComponentNormalize(resultObj[part]);
else if (part.startsWith('$'))
[resultObj, resultEl, $result] = ComponentNormalize(resultEl.getElementsByTagName(part.replace(/^\$/,""))[0]);
else
[resultObj, resultEl, $result] = ComponentNormalize(resultEl.getElementsByClassName(part)[0]);
}
return $result;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Mounting and Unmounting
// To be mounted, means that the bi directional link is established between parent and child BGComps.
// The DOM makes a link between the DOMNode of the parent and the DOMNode of the child.
// In addition to that a link is made betwen the BGNodes of parent and child.
// usage: ComponentMount(<parent>, <name>, <childContent> [,<insertBefore>])
// usage: ComponentMount(<parent>, <childContent> [,<insertBefore>])
// Form a parent<->child relationship between DOM Elements.
// This is a wrapper over the <domNode>.appendChild/insertBefore methods. It adds two features.
// 1. The child content can be specified in more flexible ways
// 2. It maintains named links in the parent to the child under these circumstances
// * If a name is available for a child node
// * the parent has the [bgComponent] key (indicating that it is opting into this behavior)
// 3. if a child is not named, it forms an unnamed relationship
//
// ChildContent Types:
// Several types of children content are supported.
// component : object(w/.el) : any JS object with a 'el' property (el should be a DOMNode)
// DOMNode : object(w/.nodeType) : DOMNodes are identified by having a 'nodeType' property
// plain text: string(s[0]!="<") : Plain text will be appended as a text node.
// Prepend string with '@TEXT' to force it to be treated as plain text and not html
// html text : string(s[0]=="<") : text starting with '<' will be converted to a component whose outerHTML is the provided text
// Prepend string with '@HTML' to force it to be treated as html and not plain text
// multiple Children : array : multiple children can be given in an array. Each array element can be any of the
// supported types including a nested array. Array nesting will not affect how the child
// hiearchy is built -- all children will be traversed and added to this component directly.
// The one difference is if name is specified and content is an array, the <name> property
// created in the parent will be an array with elements pointing to the children. Any
// children in the array that have a name property will have a reference added as that
// name regardless of whether the array itself is named. Typically, arrays will not be named
// and there is no difference between adding the children individually or within an array.
// ComponentUnmount:
// To avoid memory leaks, ComponentMount and ComponentUnmount should be called in matching pairs. If you call ComponentMount
// then you should call ComponentUnmount to undo the cyclic references when the dom element is no longer needed.
//
// Params:
// <parent> : the parent to mount the children to
// <name> : the variable-like name of the child being added in the context of the parent.
// If not provided, <childContent>.name will be used. Normaly names like this are a property of the parent and
// not the child but in the case of the DOM tree, there is a single hierarchy so the usual problem is less of a
// problem and it makes for a much nicer syntax to create content trees b/c the children can be specified in
// arrays with their names embedded in their state.
// If no name exist, the childContent will be unamed with regard to its parent.
// The special name 'unnamed' is recognized as no name being passed. This is useful in overriding a child's name property
// <childContent> : the children to be mounted to this component. It can be given in any of the types described above.
// <insertBefore> : (optional) the existing child to insert childContent before as a DOMNode or component object.
// Default is append to end of $parent. Unlike the plain DOM API, this can be a non-direct descendant of $parent.
// If you want to mount the child at the end of a descendant node, pass in <insertBefore> as an object with
// the property "appendTo" like... , {appendTo:<descendant node>}).
// Usage:
// The name parameter is optional but for readability, it must be in the p1 position if provided.
// Note that if the p1 param is a single word content and the insertBefore is specified it will incorrectly be interpreted as
// Form1. You can pass 'unnamed' as the first paramter to avoid this ambiguity and still result in an unnamed child.
// Form1: ComponentMount(<parent>, <name>, <childContent> [,<insertBefore>])
// Form2: ComponentMount(<parent>, <childContent> [,<insertBefore>])
export function ComponentMount($parent, p1, p2, p3, trace)
{
// detect form1 and form2
var name, childContent, insertBefore;
// if p3 is specified the user must have called with 3 params so it must be form 1
// The only other form 1 cases is when p2 is specified and p1 is a valid name
// When p1 is content that happens to also be a valid name and insertBefore is specified, it will be incorrectly classified.
const p1Specified = (!!p1);
const p2Specified = (typeof p2 != 'undefined');
const p3Specified = (typeof p3 != 'undefined');
const p1CanBeAName= (typeof p1 == 'string' && reVarName.test(p1));
if ((!p1Specified) || (p3Specified) || (p2Specified && p1CanBeAName)) {
name = p1; if (name == "unnamed") name='';
childContent = p2
insertBefore = p3
} else {
childContent = p1
insertBefore = p2
}
console.assert($parent, "ComponentMount called with null parent", {$parent, name, childContent,insertBefore})
const [parentObj, parentEl, parent] = ComponentNormalize($parent);
// when specifying children content, sometimes its convenient to allow the expression to result null, so just ignore this case
if (childContent == null)
return;
// check the content type. some types need special treatment and some are invalid.
switch (typeof childContent) {
// ChildContent can be null but not undefined. This is either a logic error in Form1/Form2 detection or the caller explicitly
// passed 'undefined' as the content
case 'undefined':
console.assert(false, "encounted undefined ChildContent. null is ok but not undefined.");
// string content can be either plain text content or the html describing a subtree (determined by the reHTMLContent RegExp)
case 'string':
var element;
if (/^@HTML/.test(childContent.trim()) || reHTMLContent.test(childContent)) {
element = document.createElement('div');
element.innerHTML = childContent.trim().replace(/^@HTML/,"");
element = element.firstChild;
}
else
element = document.createTextNode(childContent.replace(/^@TEXT/,""));
childContent = element;
break;
case 'object': if (Array.isArray(childContent)) {
// iterate an array of children and recursively add them
for (var i =0; i<childContent.length; i++) {
var aryElement = childContent[i]
// if its an array, since its nested inside the childContent array (this block) treat it as construction
// parameters to ComponentConstruct. If the parent has a defaultChildConstructor property it will be added to the
// params as the 'defaultConstructor' so that it will be used for the 'Constructor' if the params do not
// specify one. Note that the defaultChildConstructor may have already been added to this construction array by
// the ComponentParams class but it is ok to add it twice because 1) they are probably the same value (because
// the parent often sets it defaultChildConstructor from params.defaultChildConstructor) so it will just override
// the existing value with the same value, and 2) if they are not the same, we it will prefer the more dynamic one
// in the parent.
if (Array.isArray(aryElement)) {
aryElement = ComponentConstruct(
(parent.defaultChildConstructor) ? {defaultConstructor:parent.defaultChildConstructor} : null,
...aryElement
);
}
var aryElementType = typeof aryElement;
var childName = ((aryElementType == 'object') && bgComponentName in aryElement)
? aryElement[bgComponentName]
: ((aryElementType == 'object') && name in aryElement)
? aryElement[name]
: (!name || name.endsWith("[]"))
?name
:name+"[]";
if (childName && parentObj && (bgComponent in parentObj) && (childName in parentObj))
throw new BGError("can not mount this child component because its name is already taken in the parent", {childName,parent, child:aryElement})
// call ComponentMount explicitly with all the params to avoid any ambiguity -- if insertBefore is undefined, pass null
var mountedChild = ComponentMount([parentObj, parentEl, parent], childName, aryElement, insertBefore || null, trace);
}
return childContent;
} break; // the Object/Array case
default:
console.assert(false, "Invalid arguments. ChildContent needs to be an object, array or string", {childContent:childContent,p1:p1,p2:p2,p3:p3, type:typeof childContent});
}
if (typeof childContent.onPreMount == 'function')
childContent.onPreMount()
const [childObj, childEl, child ] = ComponentNormalize(childContent);
const isConnecting = parentEl.isConnected && ! childEl.isConnected;
if (!name)
name = ComponentGetName([childObj, childEl, child ], {bgnameOnly:true});
if (name && parentObj && (bgComponent in parentObj) && (name in parentObj))
throw new BGError("can not mount this child component because its name is already taken in the parent", {childName:name,parent, child:aryElement})
if (isConnecting)
FireDOMTreeEvent(child, bgePreConnected);
// do the work
if (insertBefore) {
if ("appendTo" in insertBefore) {
const appendToEl = ComponentToEl(insertBefore["appendTo"]);
appendToEl.appendChild(childEl);
} else {
const insertBeforeEl = ComponentToEl(insertBefore);
if (!insertBeforeEl || !('parentNode' in insertBeforeEl) || !insertBeforeEl.parentNode)
throw new BGError("insertBefore could not be converted to a DOM node", {insertBefore, parent,p1,p2,p3})
// We allow insertBeforeEl to be a non-direct descendant. Maybe we should check and assert that insertParent is a descendant
// of parentEl.
const insertParent = insertBeforeEl.parentNode
insertParent.insertBefore(childEl, insertBeforeEl);
}
} else {
parentEl.appendChild(childEl);
}
if (isConnecting)
FireDOMTreeEvent(child, bgeConnected);
// since we just added child's el to parent's el, we can use ComponentMountAdd to for the link relationships
ComponentMountAdd([parentObj, parentEl, parent], name, [childObj, childEl, child ], trace);
return child;
}
// This mounts a child that is already a DOM node descendant of $parent into a named child
// Component Parent/Child Relationships:
// This function is the definitive location for bulding the parent/child. ComponentMount calls this after it joind the DOM elements
export function ComponentMountAdd($parent, name, $child, trace)
{
const [parentObj, parentEl, parent] = ComponentNormalize($parent);
const [childObj, childEl, child ] = ComponentNormalize($child);
// if name was not explicitly passed in, see if we can get it from the content
//BGDomLinks
if (!name)
name = ComponentGetName([childObj, childEl, child ], {bgnameOnly:true});
// make the obj level parent/child relationship link
// add a member in the parent with the child's name pointing to the child
if (bgComponent in parent) {
if (name) {
var match
if (match=/^(?<basename>[^[]+)\[(?<index>.*)\]$/.exec(name)) {
if (!Array.isArray(parent[match.groups.basename]))
parent[match.groups.basename] = [];
if (!match.groups.index) {
match.groups.index = parent[match.groups.basename].length;
name = match.groups.basename+"["+match.groups.index+"]"
}
if (parent[match.groups.basename][match.groups.index] && parent[match.groups.basename][match.groups.index]!=child)
throw new BGError("the child's name in the parent is being used by something else",{childName:name, inParent:parent[match.groups.basename][match.groups.index], parent});
parent[match.groups.basename][match.groups.index] = child;
console.assert(parent[match.groups.basename][match.groups.index]);
} else {
console.assert(! /\[/.test(name),{name})
if (parent[name] && parent[name]!=child)
throw new BGError("the child's name in the parent is being used by something else",{childName:name, ParentAlreadySetWith:parent[name], parent, child});
parent[name] = child;
}
if (!parent.mounted)
parent.mounted = [];
parent.mounted.push(name);
if (childEl && childEl.classList) childEl.classList.add(name);
} else {
if (!parent.mountedUnamed)
parent.mountedUnamed = [];
parent.mountedUnamed.push(child);
// 2022-11 bobg: removing the pullup mechanism because I could not find any code in the older packages that rely on it and its too
// problematic. We should replace it with a mechanism to specify containingContent and store the link ('containingEl')
// to the higher node explicitly so that we manage children in el and containingEl can participate in the parent link (when needed)
// // this block pulls named bgComp components up through unamed components. For example, in a view component you create
// // a bunch of content, some named so that you can interact with them in the view's methods but some of the named components
// // are grouped in unamed panels for display organization. This block makes those unamed panels transparent wrt naming
// // so that the named components are accesible in the view as if they were not in the nested panels.
// // 2022-11 bobg: this caused problems when writing the VariablesView of the bg-atom-bash-debugger plugin.
// // BGDBVarSimple has named children (vname,vtype,vvalue). We add many BGDBVarSimple nodes to
// // the table and since they were unnamed, this block would pullup (vname,vtype,vvalue) into the table
// // overwriting them with each new BGDBVarSimple and then destroying the view led to errors for the unmatched.
// // This also revealed that deleting the named children from this unnamed obj is a bad idea because
// // The BGDBVarSimple class my have logic that uses them.
// // Changes:
// // 1) I changed this code to throw an error if it would have overwritten something
// // 2) I changed VariablesView to name the BGDBVarSimple rows (I added better support for name child arrays)
// // TODO: the .root mechanism is an opt out mechanism to prevent pullups. Maybe we should flip that to an opt in mechanism.
// // I am not doing that now because I dont want to stop and find the places that rely on this pullup
// // Also, maybe a better solution is to create another class like LightComponent() that does not have the bgComponent
// // symbol. Then when a LightComponent is being mounted, this code can pullup the bgComponents that it contains
// // because LightComponent's would never have logic.
// if ((bgComponent in child) && Array.isArray(child.mounted) && !child.root) {
// var cName;
// while (cName=child.mounted.shift()) {
// console.log("################################### pullup ",cName);
// var cChild
// if (match=/^(?<basename>[^[]+)\[(?<index>.*)\]$/.exec(cName))
// cChild = child[match.groups.basename][match.groups.index];
// else
// cChild = child[cName];
// console.assert(cChild);
//
// // the index may change so we need to keep track of both old and new just in case
// var newCName = cName;
//
// if (match) {
// // init newIndex with the one used in the child's named array but if its all numeric, reset it to "" so that
// // the following code will re-number it in the new parent
// var newIndex = match.groups.index;
// newCName = cName;
// if (/^[0-9]+$/.test(newIndex))
// newIndex = "";
//
// if (!Array.isArray(parent[match.groups.basename]))
// parent[match.groups.basename] = [];
// if (!newIndex) {
// newIndex = parent[match.groups.basename].length;
// newCName = match.groups.basename+"["+newIndex+"]"
// }
// if (parent[match.groups.basename][newIndex] && parent[match.groups.basename][newIndex]!=child)
// throw new BGError("the pullup mechanism (a work in progress) hit a name conflict while re-parenting child's named mounted child to parent. cName already exists in the parent", {cName, newCName, parent, child});
// parent[match.groups.basename][newIndex] = child;
// console.assert(parent[match.groups.basename][newIndex]);
// }
// else {
// if (parent[cName] && parent[cName]!=child[cName])
// throw new BGError("the pullup mechanism (a work in progress) hit a name conflict while re-parenting child's named mounted child to parent. cName already exists in the parent", {cName, parent, child});
// parent[cName]=child[cName];
// }
//
// if (!parent.mounted)
// parent.mounted = [];
// parent.mounted.push(newCName);
// if (childEl && childEl.classList) {
// childEl.classList.add(newCName);
// childEl.classList.remove(cName);
// }
//
// // remove the cName from child.mounted, but leave child[cName] in case child has logic that uses it.
// child.mounted = child.mounted.filter((c)=>{return c!=cName});
//
// // we already checked that (bgComponent in child) before entering this block
// child.parent=parent;
// child[bgComponentParent]=parent;
// child.name = newCName;
// }
// }
}
}
// add a 'parent' member in the child pointing to the parent
//BGDomLinks
if (bgComponent in child) {
if (child.parent && child.parent!==parent)
throw new BGError("the child being mounted into a parent already has a member named 'parent' that points to something else", {parent, child, existingParent:child.parent });
child.parent = parent;
child[bgComponentParent] = parent;
child.name = name;
child[bgComponentName] = name;
}
lifeCycleChecker && lifeCycleChecker.mark(child, 'onMounted');
if (typeof child.onMount == 'function')
child.onMount()
}
// usage: <child> ComponentUnmount($parent, $child)
// Tear down the parent<->child relationship that ComponentMount created
// The difference between ComponentUnmount and ComponentDestroyChild is that whereas they both unmount the child, ComponentDestroyChild
// will also destroy it so that its no longer usable.
// Params:
// <$parent> : the parent bgComp of the relationship
// <$child> : the child being unmounted. Can be the bgComp or the name in the context of $parent
export function ComponentUnmount($parent, $child)
{
const [parentObj, parentEl, parent] = ComponentNormalize($parent);
var name
if (typeof $child == 'string') {
name = $child;
if ( (name.endsWith("]")) && (match=/^(?<basename>[^[]+)\[(?<index>.*)\]$/.exec(name)) )
$child = parent[match.groups.basename][match.groups.index];
else
$child = parent[name];
if (!$child)
throw new BGError("ComponentUnmount: unmounting a child by name that does not exist in the parent", {parent, name});
}
const [childObj, childEl, child ] = ComponentNormalize($child);
if (parent.trace) console.log("t> ComponentUnmount(p:%s, c:%s) details%O", NodeTraceName(parent), NodeTraceName(child), {childName:child[bgComponentName], parent:parent, child:child, parentNamed:parent.mounted.join(),parentUnnamed:parent.mountedUnamed.join()});
console.assert(child);
if (typeof child.onPreUnmount == 'function')
child.onPreUnmount()
const isDisconnecting = childEl.isConnected;
if (isDisconnecting)
FireDOMTreeEvent(child, bgePreDisconnected);
// assert that we are dettaching it from the DOM at the correct place for this component
//TODO: to do this check right, we need to walk the parent chain to find parentEl b/c it might not be a direct child
//console.assert(!!parentEl && !!childEl && childEl.parentNode===parentEl, "ComponentUnmount ill-specified relation to tear down", {parent:parent, name, child:child, namedChild:parent&&parent[name], parentEl, childEl})
// remove from the DOM relation
if (childEl) {
childEl.remove();
}
if (isDisconnecting)
FireDOMTreeEvent(child, bgeDisconnected);
// remove the BG Node relation from the parent
//BGDomLinks
if (parent[bgComponent]) {
// try 3 ways to get the name from the child
if (!name && (bgComponentName in child))
name=child[bgComponentName];
if (!name && (bgComponent in child))
name=child.name;
// remove an unnamed relation from the parent if the parent supports it
if (!name && Array.isArray(parent.mountedUnamed)) {
var i = parent.mountedUnamed.indexOf(child);
if (i == -1)
i = parent.mountedUnamed.indexOf(childEl);
if (i != -1) {
parent.mountedUnamed.splice(i,1);
} else if (childObj) {
// if childObj exists, its one of ours, and we are only in this block if parent is one of ours so we should insist
// that the mount relationship that we are undoing should exist
throw new BGError("could not find (unnamed) child in parent.mountedUnamed", {parent:parent, child:child, mountedUnamedCopy:parent.mountedUnamed.slice(), name});
}
}
// remove a named relation from the parent if the parent supports it
if (name && Array.isArray(parent.mounted)) {
const i = parent.mounted.indexOf(name);
if (i != -1) {
parent.mounted.splice(i,1);
var match;
if ( (name.endsWith("]")) && (match=/^(?<basename>[^[]+)\[(?<index>.*)\]$/.exec(name)) )
(function(a,i) {return delete a[i]})(parent[match.groups.basename], match.groups.index);
else
delete parent[name];
}
// if childObj exists, its one of ours, and we are only in this block if parent is one of ours so we should insist
// that the mount relationship that we are undoing should exist
else if (childObj)
throw new BGError("could not find (named) child in parent.mountedUnamed", {parent:parent, child:child, mountedCopy:parent.mounted.slice(), name});
}
}
// remove the BG Node relation from the child if the child supports it
if (child[bgComponent] && ("parent" in child))
delete child["parent"];
lifeCycleChecker && lifeCycleChecker.mark(child, 'onUnmounted');
if (typeof child.onUnmount == 'function')
child.onUnmount()
return child;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Lifecycle Functions
// Create or destroy BGComp objects
// usage: <DOMNode> Html(tagIDClasses, ...p)
// This creates a plain DOMNode by using ComponentParams to process the arguments. This allows the same syntax for Component's
// constructor to be used to create DOMNodes.
export function Html(tagIDClasses, ...p)
{
var componentParams = new ComponentParams(tagIDClasses, ...p);
return ComponentMakeDOMNode(componentParams);
}
// usage: <DOMNode> ComponentMakeDOMNode(<ComponentParams>, [<obj>])
// Create and return a DOMNode/element that reflects the data in this ComponentParams instance and optionally turn <obj> into a
// BGNode object linked to the new DOMNode.
//
// If an <obj> is passed in, the DOMnode will be bi-directionally linked with it and the required state of a BGNode will be added
// to it. After this call <obj> will be a valid BGNode
//
// If bgComp is not provided, the new DOMNode will not be connected to any BGBone. Note that any DOMNode is also a valid BGComp even
// if it does not have a BGNode linked to it.
//
// Member Variables:
// When the <bgComp> parameter is passed in, these properties are set in it
// <bgComponent> : bgComponent is a Symbol that identifies bgComp as being a bgComponent full object
// <name> : the name of the node relative to its parent. Can be '' (unnamed)
// <mounted> : array of names (strings) of the named children of this component
// <mountedUnamed> : array of bgComponents objects of any unnamed children
// <el> : reference to the DOM node/element associated with this bgComponent
export function ComponentMakeDOMNode(componentParams, bgComp)
{
// if the ctor params indicated that we are wrapping an existing node, use it, otherwise create a new one
var el = componentParams.wrapNode;
if (!el && (componentParams.tagName == 'icon' )) {
var iconName = componentParams.optParams.icon || componentParams.optParams.label;
var iconObj = icons[iconName];
if (!iconObj) {
iconObj = icons["alert"];
console.warn(`icon (${iconName}) not found. Used alert icon as standin`);
}
el = document.createElement('div');
el.innerHTML = iconObj.toSVG();
el = el.firstChild;
}
else if (!el) {
el = document.createElement(componentParams.tagName || 'div');
if (componentParams.idName)
el.id = componentParams.idName;
if (componentParams.className || componentParams.name)
el.className = componentParams.getClassNames();
var isSVG = el instanceof SVGElement;
for (var propName in componentParams.props) {
var propValue = componentParams.props[propName];
var isFunc = typeof propValue === 'function';
if (isSVG && isFunc) {
el[propName] = propValue;
} else if (propName === 'dataset') {
for (var key in propValue||{})
if (propValue[key] != null)
el.dataset[key] = propValue[key];
else
delete el.dataset[key];
} else if (isSVG && (propName === 'xlink')) {
for (var key in propValue||{})
if (propValue[key] != null)
el.setAttributeNS(xlinkns, key, propValue[key]);
else
el.removeAttributeNS(xlinkns, key, propValue[key]);
} else if (!isSVG && (propName in el || isFunc) && (propName !== 'list')) {
el[propName] = propValue;
} else {
if (propValue == null) {
el.removeAttribute(propName);
} else {
el.setAttribute(propName, propValue);
}
}
}
for (var styleName in componentParams.styles)
el.style[styleName] = componentParams.styles[styleName] == null ? '' : componentParams.styles[styleName];
}
global.lifeCycleChecker && lifeCycleChecker.mark(bgComp||el, 'ctor');
domTreeChanges.addToWatch(el);
// if bgComp is given, create a two way link between it and the el
//BGDomLinks
if (bgComp) {
bgComp[bgComponent] = true;
bgComp[bgComponentName] = componentParams.name;
bgComp.name = componentParams.name;
bgComp.mounted = [];
bgComp.mountedUnamed = [];
bgComp.el = el;
bgComp.root = componentParams.root;
ComponentMap.set(el,bgComp);
}
return el;
}
// usage: void ComponentDestroyDOMNode($bgComp)
// This is the complement to ComponentMakeDOMNode. A DOMNode created by ComponentMakeDOMNode typically should be destroyed by this
// function. If a BGNode was passed to ComponentMakeDOMNode to be linked with the new DOMNode, either that BGNode or the DOMNode
// returned by ComponentMakeDOMNode can be passed to this function.
// TODO: this assumes that $bgComp is not mounted. consider if we need to check and unmount it but I thikn its OK not to b/c
// the component family seems to be focused on the children so we usually are destroying a node from its parent with
// ComponentDestroyChild. When we do have a top level node, its natural to unmount it when we are done and may not even destroy it
// 2022-12 bobg: update: I added the block that unmounts $bgComp if its mounted. Newer DOM interfaces allow manipulating the
// DOM tree from the child's perspective and well as from the parent's. When writing Tabs, I called tab.destroy() and it was
// surprising that this function did not unmount it even though it destroyed the contents.
export function ComponentDestroyDOMNode($bgComp)
{
const [bgCompObj,bgCompEl,bgComp] = ComponentNormalize($bgComp);
// conditional component tracing
if (bgComp.trace) console.log("t> ComponentDestroyDOMNode(%s) details%O", NodeTraceName(bgComp), {bgComp})
// unmount it if its connected to the DOM or an unconnected DOM fragment. (so connected would not be a good test)
if (bgCompObj && bgCompObj.parent) {
ComponentUnmount(bgCompObj.parent, bgCompObj);
} else if (bgCompEl && (bgCompEl.parentNode || bgCompEl.parentElement) ) {
bgCompEl.remove();
}
if (bgCompObj) {
var namedChildren = bgCompObj.mounted.slice();
for (var childName of namedChildren) {
ComponentDestroyChild(bgCompObj,childName);
}
var unnamedChildren = bgCompObj.mountedUnamed.slice();
for (var child of unnamedChildren) {
ComponentDestroyChild(bgCompObj,child);
}
console.assert(bgCompObj.mounted.length==0 && bgCompObj.mountedUnamed.length==0, {parent:[bgCompObj,bgCompEl,bgComp]});
bgCompObj[bgComponent] = 'destroyed'
ComponentMap.delete(bgCompEl);
bgCompObj.el = null;
}
domTreeChanges.removeFromWatch(bgCompEl);
global.lifeCycleChecker && lifeCycleChecker.mark(bgComp, 'destroyed');
}
// usage: <void> ComponentDestroyChild($parent, nameOrChild)
// This calls ComponentUnmount to break down the parent/child relationship and then calls destroy on the child
// The difference between ComponentUnmount and ComponentDestroyChild is that whereas they both unmount the child, ComponentDestroyChild
// will also destroy it so that its no longer usable.
// Params:
// <parent> : the parent bgComponent of the relationship
// <name> : the name of the child in the context of the parent or undefined if the child is unnamed
// <child> : if <name> is undefined, the child object must be passed in.
export function ComponentDestroyChild($parent, nameOrChild)
{
if ($parent.trace) console.log("t> ComponentDestroyChild(p:%s,c:%s) details%O", NodeTraceName($parent), NodeTraceName(nameOrChild), {$parent, nameOrChild})
const childObj = ComponentUnmount($parent, nameOrChild);
if (typeof childObj.destroy == 'function')
childObj.destroy();
else
ComponentDestroyDOMNode(childObj);
return childObj;
}
// usage: <void> ComponentDestroyChildren($parent)
// Unmounts all children from $parent and call their .destroy() methods to recover their resources
export function ComponentDestroyChildren($parent)
{
const [parentObj, parentEl, parent] = ComponentNormalize($parent);
while (parentEl.firstChild)
ComponentDestroyChild([parentObj, parentEl, parent], parentEl.lastChild);
if (parentObj)
console.assert(parentObj.mounted.length==0 && parentObj.mountedUnamed.length==0);
}
// usage: <BGNode> ComponentConstruct(...p)
// This supports constructing a specific class of BGNode given an array of contruction params. This is typically done when adding
// child content to either constructing a Component class or calling ComponentMount (or something that calls ComponentMount).
// The type of BGNode returned is determined by the constructor obtained from the parameters themselves or the global.bg.Component
// constructor as a default.
// Override or Provide a Deafult The Constructor in the Params:
// The caller of this function can prepend an argument to the list that will override same named parameters in the list passed in.
// ComponentConstruct({Constructor:Html}, ...p) // this will ensure that the Html constructor function is used
// ComponentConstruct({defaultConstructor:Html}, ...p) // this will use the Html constructor function only if Constructor is
// // not specified in the remaining parameters
// ComponentConstruct({defaultChildConstructor:ListItem}, ...p) // this will not affect the constructor that this invocation
// // uses, but if the params specify any children by constrction params,
// // ListItem will become the defaultConstructor for them.
// Example:
// this.mount([
// myChild, // <myChild> is an existing BGComp
// new <ClassName>(), // <ClassName> is a class that uses this library (Component, Button, InputField, etc...)
// ["$div", ...], // construct the default type of component for the Parent (the parent is 'this' in this example)
// [{Constructor:<classVariable>}, ...] // where <classVariable> is a variable that conatins a constructor
// ])
// Params:
// <p> : is an arbitrarily long list of parameters as described in 'man ComponentParams'. This function determines how to construct
// the BGComp from the parameter themselves.
// * if and parameter name 'Constructor' is present it will be invoked to create the BGComp.
// * otherwise, if 'defaultConstructor' is present it will be used.
// * otherwise if global.bg.Component exists, it will be used.
// * otherwise and exception is thrown.
export function ComponentConstruct(...p) {
//TODO: support dynamic construction of a specific Component type, based on the $<type> string field, maybe use customElements.define (CustomElementRegistry)
const componentParams = new ComponentParams(...p);
if (componentParams.Constructor) {
return new componentParams.Constructor(...p);
} else if (componentParams.defaultConstructor) {
return new componentParams.defaultConstructor(...p);
} else if (global.bg.Component) {