@twobirds/microcomponents 0.0.59 → 0.0.61

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/public/src/MC.ts CHANGED
@@ -410,8 +410,6 @@ class DC extends McBase {
410
410
  }
411
411
  }
412
412
 
413
- let observer: MutationObserver | null = null;
414
-
415
413
  /*
416
414
  unresolvedCEs (code below this comment):
417
415
 
@@ -430,10 +428,10 @@ In general this behaves like a custom runtime import for custom elements in the
430
428
  The only difference is that the micro component is loaded only when needed, and not on page load.
431
429
  */
432
430
 
433
- const unresolvedCEs = new Map();
431
+ const CEs = new Map();
434
432
 
435
433
  /*
436
- MCs (code below this comment):
434
+ loadedMCs (code below this comment):
437
435
  a repository (a hashmap) of micro components:
438
436
  the name is the name of the micro component (like "my-mc")
439
437
  if they are loading, the value is a script element
@@ -449,28 +447,88 @@ The only difference is that the micro component is loaded only when needed, and
449
447
  */
450
448
  const MCs = new Map();
451
449
 
450
+ function makeLoadScript( element: string ): HTMLScriptElement {
451
+
452
+ const fileName: string = '/' + element.split('-').join('/')+'.js';
453
+
454
+ const se: HTMLScriptElement = document.createElement('script');
455
+
456
+ se.setAttribute('src', fileName);
457
+ se.setAttribute('blocking', 'render');
458
+ se.async = true;
459
+ se.setAttribute('type', 'module');
460
+ se.setAttribute( 'name', element);
461
+ se.setAttribute( 'loading', '');
462
+
463
+ addListener( se, 'load', (ev) => {
464
+ // console.info('loaded MC', elementName);
465
+ se.removeAttribute( 'loading' );
466
+ se.setAttribute( 'loaded', '');
467
+ });
468
+
469
+ addListener( se, 'error', (ev) => {
470
+ console.error('could not load Custom Element code from', fileName);
471
+ });
472
+
473
+ return se;
474
+ }
475
+
476
+ function onLoadCallback( script: HTMLScriptElement ) {
477
+ // create callback script tag
478
+ // it is essential to do this in a timeout and via another script tag,
479
+ // to force it to be a top level module allowing for the use of "import"
480
+ setTimeout( function(){
481
+ // create callback script tag
482
+ const importFileName = script.getAttribute('src');
483
+ const elementName = script.getAttribute('name');
484
+
485
+ const se: HTMLScriptElement = document.createElement('script');
486
+
487
+ se.async = true;
488
+ se.setAttribute( 'type', 'module');
489
+
490
+ // --- autoload script
491
+ const code = /*javascript*/`
492
+ import _ from "${importFileName}";
493
+ import {DC} from "./microcomponents.js";
494
+
495
+ const elements = Array.from(document.querySelectorAll('[_mc*="${elementName}"]')).filter( element => element instanceof HTMLElement );
496
+
497
+ elements.forEach((element) => {
498
+ autoload.state.MCs.set( '${elementName}', _ ); // add class to MCs repository
499
+ DC.add(element, _.name, new _(element)); // add instance to DOM
500
+ element.setAttribute('_mc', element.getAttribute('_mc').replace('${elementName}', '').replace( /\w\w/g, ' ').trim()); // remove name from elements "_mc" attribute
501
+ });
502
+
503
+ setTimeout( () => {
504
+ document.body.querySelector('script[name="${elementName}]"')?.remove();
505
+ },0);`;
506
+ // --- end autoload script
507
+
508
+ se.innerHTML = code;
509
+ document.body.append(se);
510
+ }, 0);
511
+ }
512
+
452
513
  function loadUndefinedMicroComponents() {
453
514
 
454
515
  // load unresolved custom eleents
455
516
  [ ...document.querySelectorAll(':not(:defined)') ] // undefined custom elements
456
- .filter( (element) => !!element && !document.head.querySelector(`script[${element.tagName.toLowerCase()}]`) )
517
+ .filter( (element) => !!element
518
+ && !customElements.get(element.tagName.toLowerCase()) // not in CustomElementsRegistry
519
+ && !CEs.has(element.tagName.toLowerCase()) // not in loading list
520
+ )
457
521
  .forEach((el) => {
458
522
  const elementName = el.tagName.toLowerCase(),
459
523
  fileName: string = '/'+elementName.split('-').join('/')+'.js';
460
- //console.log('load undefined custom element',el);
461
524
 
462
- const se: HTMLScriptElement = document.createElement('script');
525
+ const se: HTMLScriptElement = makeLoadScript( elementName );
463
526
 
464
- se.setAttribute('src', fileName);
465
- se.setAttribute('blocking', 'render');
466
- se.setAttribute('type', 'module');
467
- se.setAttribute( elementName, '');
468
-
469
- se.onerror = () => {
470
- console.error('could not load Custom Element code from', fileName);
471
- }
527
+ addListener( se, 'load', () => {
528
+ CEs.set( elementName, customElements.get(el.tagName.toLowerCase()) );
529
+ });
472
530
 
473
- //console.info('load Custom Element', elementName);
531
+ CEs.set( elementName, 'loading');
474
532
  document.head.append(se);
475
533
 
476
534
  // instanciation of the micro component will be handled by the browser automatically
@@ -480,74 +538,41 @@ function loadUndefinedMicroComponents() {
480
538
  [ ...document.querySelectorAll('[_mc]:not([_mc=""]') ] // undefined DOM components
481
539
  .filter( (element) => !!element )
482
540
  .forEach((el) => {
483
- const toBeLoaded = el.getAttribute('_mc')?.split(' ').filter( m => !!m && !document.head.querySelector(`script[${m}]`) );
484
-
485
-
486
- //console.log('load undefined DOM components',toBeLoaded);
487
-
488
- toBeLoaded?.forEach( elementName => {
489
- const se: HTMLScriptElement = document.createElement('script'),
490
- fileName: string = '/'+elementName.split('-').join('/')+'.js';
491
-
492
- se.setAttribute('src', fileName);
493
- se.setAttribute('blocking', 'render');
494
- se.async = true;
495
- se.setAttribute('type', 'module');
496
- se.setAttribute( elementName, '');
541
+ const elements = el.getAttribute('_mc')?.split(' ').filter( m => !!m ) || [];
542
+ const elementsToBeLoaded = elements.filter( m => !MCs.has( m ) );
497
543
 
498
- se.onload = () => {
499
- // console.info('loaded MC', elementName);
544
+ // load MC components that do not exist yet
545
+ elementsToBeLoaded?.forEach( elementName => {
546
+ const se: HTMLScriptElement = makeLoadScript( elementName );
500
547
 
501
- // create callback script tag
502
- setTimeout( function(){
503
- // create callback script tag
504
- const se: HTMLScriptElement = document.createElement('script');
505
- const importFileName = fileName;
548
+ addListener( se, 'load', () => onLoadCallback( document.head.querySelector(`script[name="${elementName}"]`) as HTMLScriptElement) );
549
+
550
+ MCs.set( elementName, 'loading');
506
551
 
507
- se.async = true;
508
- se.setAttribute( 'type', 'module');
509
-
510
- const code = /*javascript*/`
511
- import _ from "${importFileName}";
512
- import {DC} from "./microcomponents.js";
513
-
514
- //console.log('use importFileName', '${importFileName}');
515
- // console.log('use elementName', '${elementName}');
516
-
517
- const elements = Array.from(document.querySelectorAll('[_mc*="${elementName}"]')).filter( element => element instanceof HTMLElement );
518
-
519
- // console.log('elements', elements);
520
- elements.forEach((element) => {
521
- // console.log('DC, element,class', DC, element, _);
522
- DC.add(element, _.name, new _(element));
523
- element.setAttribute('_mc', element.getAttribute('_mc').replace('${elementName}', '').replace( /\w\w/g, ' ').trim());
524
- });
525
- setTimeout( () => {
526
- // console.log('remove callback script tag', '${elementName}');
527
- document.body.querySelector('script[${elementName}]')?.remove();
528
- },0);
529
- `;
530
-
531
- //console.log('before innerHTML');
532
- se.innerHTML = code;
533
- //console.log('after innerHTML');
534
- //console.log( 'callback script tag', se, code );
535
- document.body.append(se);
536
- }, 0);
537
-
538
- }
539
-
540
- se.onerror = () => {
541
- console.error('could not load DOM Component code from', fileName);
542
- }
543
-
544
- // console.info('load DOM Micro Component', elementName);
545
552
  document.head.append(se);
546
553
  });
554
+
555
+ // loading MC components already have callbacks attached
556
+ const elementsAlreadyLoaded = elements.filter( m => MCs.has( m ) && !MCs.get( m )!.length);
557
+
558
+ // MCs that are already loaded will trigger the onLoadCallback manually
559
+ elementsAlreadyLoaded?.forEach( elementName => {
560
+ setTimeout( ()=>{
561
+ console.log('attaching', elementName, (autoload as any).state.MCs.get(elementName));
562
+ DC.add(el, elementName, new ( (autoload as any).state.MCs.get(elementName) as any)(el)); // add instance to DOM
563
+ el.setAttribute('_mc', (el as any)
564
+ .getAttribute('_mc')
565
+ .replace('${elementName}', '')
566
+ .replace( /\w\w/g, ' ')
567
+ .trim()
568
+ ); // remove name from elements "_mc" attribute
569
+ }, 5);
570
+ });
547
571
  });
548
572
  }
549
573
 
550
574
  let autoloadEnabled: boolean = false;
575
+ let observer: MutationObserver | null = null;
551
576
 
552
577
  export function autoload(enable: boolean = true): boolean {
553
578
  if (!enable) {
@@ -567,5 +592,7 @@ export function autoload(enable: boolean = true): boolean {
567
592
  return autoloadEnabled;
568
593
  }
569
594
 
595
+ (autoload as LooseObject).state = { CEs, MCs };
596
+
570
597
  export { addListener, removeListener, McBase, MC, DC, McEvent };
571
598
 
package/src/MC.ts CHANGED
@@ -410,8 +410,6 @@ class DC extends McBase {
410
410
  }
411
411
  }
412
412
 
413
- let observer: MutationObserver | null = null;
414
-
415
413
  /*
416
414
  unresolvedCEs (code below this comment):
417
415
 
@@ -430,10 +428,10 @@ In general this behaves like a custom runtime import for custom elements in the
430
428
  The only difference is that the micro component is loaded only when needed, and not on page load.
431
429
  */
432
430
 
433
- const unresolvedCEs = new Map();
431
+ const CEs = new Map();
434
432
 
435
433
  /*
436
- MCs (code below this comment):
434
+ loadedMCs (code below this comment):
437
435
  a repository (a hashmap) of micro components:
438
436
  the name is the name of the micro component (like "my-mc")
439
437
  if they are loading, the value is a script element
@@ -449,28 +447,88 @@ The only difference is that the micro component is loaded only when needed, and
449
447
  */
450
448
  const MCs = new Map();
451
449
 
450
+ function makeLoadScript( element: string ): HTMLScriptElement {
451
+
452
+ const fileName: string = '/' + element.split('-').join('/')+'.js';
453
+
454
+ const se: HTMLScriptElement = document.createElement('script');
455
+
456
+ se.setAttribute('src', fileName);
457
+ se.setAttribute('blocking', 'render');
458
+ se.async = true;
459
+ se.setAttribute('type', 'module');
460
+ se.setAttribute( 'name', element);
461
+ se.setAttribute( 'loading', '');
462
+
463
+ addListener( se, 'load', (ev) => {
464
+ // console.info('loaded MC', elementName);
465
+ se.removeAttribute( 'loading' );
466
+ se.setAttribute( 'loaded', '');
467
+ });
468
+
469
+ addListener( se, 'error', (ev) => {
470
+ console.error('could not load Custom Element code from', fileName);
471
+ });
472
+
473
+ return se;
474
+ }
475
+
476
+ function onLoadCallback( script: HTMLScriptElement ) {
477
+ // create callback script tag
478
+ // it is essential to do this in a timeout and via another script tag,
479
+ // to force it to be a top level module allowing for the use of "import"
480
+ setTimeout( function(){
481
+ // create callback script tag
482
+ const importFileName = script.getAttribute('src');
483
+ const elementName = script.getAttribute('name');
484
+
485
+ const se: HTMLScriptElement = document.createElement('script');
486
+
487
+ se.async = true;
488
+ se.setAttribute( 'type', 'module');
489
+
490
+ // --- autoload script
491
+ const code = /*javascript*/`
492
+ import _ from "${importFileName}";
493
+ import {DC} from "./microcomponents.js";
494
+
495
+ const elements = Array.from(document.querySelectorAll('[_mc*="${elementName}"]')).filter( element => element instanceof HTMLElement );
496
+
497
+ elements.forEach((element) => {
498
+ autoload.state.MCs.set( '${elementName}', _ ); // add class to MCs repository
499
+ DC.add(element, _.name, new _(element)); // add instance to DOM
500
+ element.setAttribute('_mc', element.getAttribute('_mc').replace('${elementName}', '').replace( /\w\w/g, ' ').trim()); // remove name from elements "_mc" attribute
501
+ });
502
+
503
+ setTimeout( () => {
504
+ document.body.querySelector('script[name="${elementName}]"')?.remove();
505
+ },0);`;
506
+ // --- end autoload script
507
+
508
+ se.innerHTML = code;
509
+ document.body.append(se);
510
+ }, 0);
511
+ }
512
+
452
513
  function loadUndefinedMicroComponents() {
453
514
 
454
515
  // load unresolved custom eleents
455
516
  [ ...document.querySelectorAll(':not(:defined)') ] // undefined custom elements
456
- .filter( (element) => !!element && !document.head.querySelector(`script[${element.tagName.toLowerCase()}]`) )
517
+ .filter( (element) => !!element
518
+ && !customElements.get(element.tagName.toLowerCase()) // not in CustomElementsRegistry
519
+ && !CEs.has(element.tagName.toLowerCase()) // not in loading list
520
+ )
457
521
  .forEach((el) => {
458
522
  const elementName = el.tagName.toLowerCase(),
459
523
  fileName: string = '/'+elementName.split('-').join('/')+'.js';
460
- //console.log('load undefined custom element',el);
461
524
 
462
- const se: HTMLScriptElement = document.createElement('script');
525
+ const se: HTMLScriptElement = makeLoadScript( elementName );
463
526
 
464
- se.setAttribute('src', fileName);
465
- se.setAttribute('blocking', 'render');
466
- se.setAttribute('type', 'module');
467
- se.setAttribute( elementName, '');
468
-
469
- se.onerror = () => {
470
- console.error('could not load Custom Element code from', fileName);
471
- }
527
+ addListener( se, 'load', () => {
528
+ CEs.set( elementName, customElements.get(el.tagName.toLowerCase()) );
529
+ });
472
530
 
473
- //console.info('load Custom Element', elementName);
531
+ CEs.set( elementName, 'loading');
474
532
  document.head.append(se);
475
533
 
476
534
  // instanciation of the micro component will be handled by the browser automatically
@@ -480,74 +538,41 @@ function loadUndefinedMicroComponents() {
480
538
  [ ...document.querySelectorAll('[_mc]:not([_mc=""]') ] // undefined DOM components
481
539
  .filter( (element) => !!element )
482
540
  .forEach((el) => {
483
- const toBeLoaded = el.getAttribute('_mc')?.split(' ').filter( m => !!m && !document.head.querySelector(`script[${m}]`) );
484
-
485
-
486
- //console.log('load undefined DOM components',toBeLoaded);
487
-
488
- toBeLoaded?.forEach( elementName => {
489
- const se: HTMLScriptElement = document.createElement('script'),
490
- fileName: string = '/'+elementName.split('-').join('/')+'.js';
491
-
492
- se.setAttribute('src', fileName);
493
- se.setAttribute('blocking', 'render');
494
- se.async = true;
495
- se.setAttribute('type', 'module');
496
- se.setAttribute( elementName, '');
541
+ const elements = el.getAttribute('_mc')?.split(' ').filter( m => !!m ) || [];
542
+ const elementsToBeLoaded = elements.filter( m => !MCs.has( m ) );
497
543
 
498
- se.onload = () => {
499
- // console.info('loaded MC', elementName);
544
+ // load MC components that do not exist yet
545
+ elementsToBeLoaded?.forEach( elementName => {
546
+ const se: HTMLScriptElement = makeLoadScript( elementName );
500
547
 
501
- // create callback script tag
502
- setTimeout( function(){
503
- // create callback script tag
504
- const se: HTMLScriptElement = document.createElement('script');
505
- const importFileName = fileName;
548
+ addListener( se, 'load', () => onLoadCallback( document.head.querySelector(`script[name="${elementName}"]`) as HTMLScriptElement) );
549
+
550
+ MCs.set( elementName, 'loading');
506
551
 
507
- se.async = true;
508
- se.setAttribute( 'type', 'module');
509
-
510
- const code = /*javascript*/`
511
- import _ from "${importFileName}";
512
- import {DC} from "./microcomponents.js";
513
-
514
- //console.log('use importFileName', '${importFileName}');
515
- // console.log('use elementName', '${elementName}');
516
-
517
- const elements = Array.from(document.querySelectorAll('[_mc*="${elementName}"]')).filter( element => element instanceof HTMLElement );
518
-
519
- // console.log('elements', elements);
520
- elements.forEach((element) => {
521
- // console.log('DC, element,class', DC, element, _);
522
- DC.add(element, _.name, new _(element));
523
- element.setAttribute('_mc', element.getAttribute('_mc').replace('${elementName}', '').replace( /\w\w/g, ' ').trim());
524
- });
525
- setTimeout( () => {
526
- // console.log('remove callback script tag', '${elementName}');
527
- document.body.querySelector('script[${elementName}]')?.remove();
528
- },0);
529
- `;
530
-
531
- //console.log('before innerHTML');
532
- se.innerHTML = code;
533
- //console.log('after innerHTML');
534
- //console.log( 'callback script tag', se, code );
535
- document.body.append(se);
536
- }, 0);
537
-
538
- }
539
-
540
- se.onerror = () => {
541
- console.error('could not load DOM Component code from', fileName);
542
- }
543
-
544
- // console.info('load DOM Micro Component', elementName);
545
552
  document.head.append(se);
546
553
  });
554
+
555
+ // loading MC components already have callbacks attached
556
+ const elementsAlreadyLoaded = elements.filter( m => MCs.has( m ) && !MCs.get( m )!.length);
557
+
558
+ // MCs that are already loaded will trigger the onLoadCallback manually
559
+ elementsAlreadyLoaded?.forEach( elementName => {
560
+ setTimeout( ()=>{
561
+ console.log('attaching', elementName, (autoload as any).state.MCs.get(elementName));
562
+ DC.add(el, elementName, new ( (autoload as any).state.MCs.get(elementName) as any)(el)); // add instance to DOM
563
+ el.setAttribute('_mc', (el as any)
564
+ .getAttribute('_mc')
565
+ .replace('${elementName}', '')
566
+ .replace( /\w\w/g, ' ')
567
+ .trim()
568
+ ); // remove name from elements "_mc" attribute
569
+ }, 5);
570
+ });
547
571
  });
548
572
  }
549
573
 
550
574
  let autoloadEnabled: boolean = false;
575
+ let observer: MutationObserver | null = null;
551
576
 
552
577
  export function autoload(enable: boolean = true): boolean {
553
578
  if (!enable) {
@@ -567,5 +592,7 @@ export function autoload(enable: boolean = true): boolean {
567
592
  return autoloadEnabled;
568
593
  }
569
594
 
595
+ (autoload as LooseObject).state = { CEs, MCs };
596
+
570
597
  export { addListener, removeListener, McBase, MC, DC, McEvent };
571
598