Skip to content

Commit fad47ed

Browse files
committed
Refactor: Added simpler way to configure lazily-loading components by using a function that return the component class directly
1 parent dcb0df9 commit fad47ed

File tree

6 files changed

+144
-133
lines changed

6 files changed

+144
-133
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44

55
## [Unreleased]
66

7-
## [3.0.1] - 2023-03-15
7+
## [3.0.2] - 2024-09-04
88
### Maintenance
9-
- Fix: Added manual toggle in SelectorHookParserConfig to revert to regex-based parsing for selector hooks to improved backwards-compatibility
9+
- Refactor: Added simpler way to configure lazily-loading components by using a function that return the component class directly
10+
11+
## [3.0.1] - 2024-08-30
12+
### Maintenance
13+
- Fix: Added manual toggle in SelectorHookParserConfig to revert to regex-based parsing for selector hooks to improve backwards-compatibility
1014

1115
## [3.0.0] - 2024-08-21
1216
### Added

docs/.gitignore

Lines changed: 0 additions & 14 deletions
This file was deleted.

projects/ngx-dynamic-hooks/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ngx-dynamic-hooks",
3-
"version": "3.0.1",
3+
"version": "3.0.2",
44
"description": "Automatically insert live Angular components into a dynamic string of content (based on their selector or any pattern of your choice) and render the result in the DOM.",
55
"person": "Marvin Tobisch <[email protected]>",
66
"license": "MIT",

projects/ngx-dynamic-hooks/src/lib/interfacesPublic.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,27 @@ export interface HookComponentData {
137137
/**
138138
* A config object describing the component that is supposed to be loaded for this Hook
139139
*
140-
* Can be either the component class itself or a LazyLoadComponentConfig, if the component
141-
* should be lazy-loaded (Ivy-feature)
140+
* Can be either:
141+
* - The component class itself
142+
* - A function that returns a promise with the component class
143+
* - An explicit LazyLoadComponentConfig
142144
*/
143-
export type ComponentConfig = (new(...args: any[]) => any) | LazyLoadComponentConfig;
145+
export type ComponentConfig = (new(...args: any[]) => any) | (() => Promise<(new(...args: any[]) => any)>) | LazyLoadComponentConfig;
144146

145147
/**
146-
* A config object for a component that is supposed to be lazy-loaded (Ivy-feature)
148+
* An explicit config object for a component that is supposed to be lazy-loaded.
147149
*
148-
* importPromise has to be a function that returns the import promise for the component file (not the import promise itself!)
149-
* importName has to be the name of the component class to be imported
150+
* - importPromise has to be a function that returns the import promise for the component file (not the import promise itself!)
151+
* - importName has to be the name of the component class to be imported
150152
*
151153
* Example:
152154
* {
153155
* importPromise: () => import('./someComponent/someComponent.c'),
154156
* importName: 'SomeComponent'
155157
* }
158+
*
159+
* Note: This mostly exists for backwards-compatibility. Lazy-loading components is easier accomplished by using a function
160+
* that returns a promise with the component class in the component field of HookComponentData
156161
*/
157162
export interface LazyLoadComponentConfig {
158163
importPromise: () => Promise<any>;

projects/ngx-dynamic-hooks/src/lib/services/core/componentCreator.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,25 +277,31 @@ export class ComponentCreator {
277277
loadComponentClass(componentConfig: ComponentConfig): ReplaySubject<new(...args: any[]) => any> {
278278
const componentClassLoaded: ReplaySubject<new(...args: any[]) => any> = new ReplaySubject(1);
279279

280-
// a) If is normal class
280+
// a) If is component class
281281
if (componentConfig.hasOwnProperty('prototype')) {
282282
componentClassLoaded.next(componentConfig as (new(...args: any[]) => any));
283283

284-
// b) If is LazyLoadComponentConfig
284+
// c) If is function that returns promise with component class
285+
} else if (typeof componentConfig === 'function') {
286+
(componentConfig as (() => Promise<(new(...args: any[]) => any)>))().then(compClass => {
287+
componentClassLoaded.next(compClass);
288+
})
289+
290+
// c) If is LazyLoadComponentConfig
285291
} else if (componentConfig.hasOwnProperty('importPromise') && componentConfig.hasOwnProperty('importName')) {
286292
// Catch typical importPromise error
287293
if ((componentConfig as LazyLoadComponentConfig).importPromise instanceof Promise) {
288294
throw Error(`When lazy-loading a component, the "importPromise"-field must contain a function returning the import-promise, but it contained the promise itself.`);
289295
}
290296

291-
(componentConfig as LazyLoadComponentConfig).importPromise().then((m) => {
297+
(componentConfig as LazyLoadComponentConfig).importPromise().then((m) => {
292298
const importName = (componentConfig as LazyLoadComponentConfig).importName;
293299
const compClass = Object.prototype.hasOwnProperty.call(m, importName) ? m[importName] : m['default'];
294300
componentClassLoaded.next(compClass);
295301
});
296302

297303
} else {
298-
throw Error('The "component" property of a returned HookData object must either contain the component class or a LazyLoadComponentConfig');
304+
throw Error('The "component" property of a returned HookData object must either contain the component class, a function that returns a promise with the component class or an explicit LazyLoadComponentConfig');
299305
}
300306

301307
return componentClassLoaded;

projects/ngx-dynamic-hooks/src/tests/integration/componentLoading.spec.ts

Lines changed: 116 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ComponentFixtureAutoDetect, TestBed, fakeAsync, tick } from '@angular/c
33
import { first } from 'rxjs/operators';
44

55
// Testing api resources
6-
import { DynamicHooksComponent, LoadedComponent, anchorAttrHookId, anchorAttrParseToken, anchorElementTag, provideDynamicHooks } from '../testing-api';
6+
import { ComponentConfig, DynamicHooksComponent, LoadedComponent, anchorAttrHookId, anchorAttrParseToken, anchorElementTag, provideDynamicHooks } from '../testing-api';
77

88
// Custom testing resources
99
import { defaultBeforeEach, prepareTestingModule, testParsers } from './shared';
@@ -45,7 +45,7 @@ describe('Component loading', () => {
4545
it('#should ensure the passed componentConfig is correct', () => {
4646
// Load with nonsensical componentConfig
4747
expect(() => comp['dynamicHooksService']['componentCreator'].loadComponentClass(true as any))
48-
.toThrow(new Error('The "component" property of a returned HookData object must either contain the component class or a LazyLoadComponentConfig'));
48+
.toThrow(new Error('The "component" property of a returned HookData object must either contain the component class, a function that returns a promise with the component class or an explicit LazyLoadComponentConfig'));
4949
});
5050

5151
it('#should be able to load module components', () => {
@@ -531,122 +531,132 @@ describe('Component loading', () => {
531531
});
532532

533533
it('#should lazy-load components', fakeAsync(() => {
534-
const genericMultiTagParser = TestBed.inject(GenericMultiTagStringParser);
535-
genericMultiTagParser.onGetBindings = (hookId, hookValue, context) => {
536-
return {
537-
inputs: {
538-
numberProp: 4
534+
535+
const testLazyLoading = (lazyCompConfig: ComponentConfig) => {
536+
537+
const genericMultiTagParser = TestBed.inject(GenericMultiTagStringParser);
538+
genericMultiTagParser.onGetBindings = (hookId, hookValue, context) => {
539+
return {
540+
inputs: {
541+
numberProp: 4
542+
}
539543
}
540544
}
541-
}
542545

543-
// Whatever parsers lazy-loads a component for this test
544-
const genericWhateverParser = TestBed.inject(GenericWhateverStringParser);
545-
genericWhateverParser.component = {
546-
// Simulate that loading this component takes 100ms
547-
importPromise: () => new Promise(resolve => setTimeout(() => {
548-
resolve({LazyTestComponent: LazyTestComponent})
549-
}, 100)),
550-
importName: 'LazyTestComponent'
551-
};
552-
genericWhateverParser.onGetBindings = (hookId, hookValue, context) => {
553-
return {
554-
inputs: {
555-
name: 'sleepy'
546+
const genericSingleTagParser = TestBed.inject(GenericSingleTagStringParser);
547+
genericSingleTagParser.onGetBindings = (hookId, hookValue, context) => {
548+
return {
549+
inputs: {
550+
numberProp: 87
551+
}
556552
}
557553
}
558-
}
559554

560-
const genericSingleTagParser = TestBed.inject(GenericSingleTagStringParser);
561-
genericSingleTagParser.onGetBindings = (hookId, hookValue, context) => {
562-
return {
563-
inputs: {
564-
numberProp: 87
555+
// Whatever parsers lazy-loads a component for this test
556+
const genericWhateverParser = TestBed.inject(GenericWhateverStringParser);
557+
genericWhateverParser.component = lazyCompConfig;
558+
genericWhateverParser.onGetBindings = (hookId, hookValue, context) => {
559+
return {
560+
inputs: {
561+
name: 'sleepy'
562+
}
565563
}
566564
}
567-
}
568-
569-
const testText = `
570-
<p>
571-
A couple of components:
572-
[multitag-string]
573-
[whatever-string][/whatever-string]
574-
[/multitag-string]
575-
[singletag-string]
576-
</p>
577-
`;
578-
579-
comp.content = testText;
580-
comp.context = context;
581-
let loadedComponents: LoadedComponent[] = [];
582-
comp.componentsLoaded.pipe(first()).subscribe((lc: any) => loadedComponents = lc);
583-
comp.ngOnChanges({content: true, context: true} as any);
584565

585-
// Everything except the lazy-loaded component should be loaded
586-
expect(fixture.nativeElement.querySelector('.multitag-component')).not.toBe(null);
587-
expect(fixture.nativeElement.querySelector('.lazy-component')).toBe(null);
588-
expect(fixture.nativeElement.querySelector('.singletag-component')).not.toBe(null);
589-
590-
expect(Object.values(comp.hookIndex).length).toBe(3);
591-
expect(comp.hookIndex[1].componentRef!.instance.constructor.name).toBe('MultiTagTestComponent');
592-
expect(comp.hookIndex[2].componentRef).toBeNull();
593-
expect(comp.hookIndex[3].componentRef!.instance.constructor.name).toBe('SingleTagTestComponent');
594-
595-
// Make sure that onDynamicChanges has triggered on component init
596-
spyOn(comp.hookIndex[1].componentRef!.instance, 'onDynamicChanges').and.callThrough();
597-
expect(comp.hookIndex[1].componentRef!.instance.onDynamicChanges['calls'].count()).toBe(0);
598-
expect(comp.hookIndex[1].componentRef!.instance.changesContext).toEqual(context);
599-
expect(comp.hookIndex[1].componentRef!.instance.changesContentChildren).toBeUndefined();
600-
601-
// Make sure that onDynamicMount has not yet triggered
602-
spyOn(comp.hookIndex[1].componentRef!.instance, 'onDynamicMount').and.callThrough();
603-
expect(comp.hookIndex[1].componentRef!.instance.onDynamicMount['calls'].count()).toBe(0);
604-
expect(comp.hookIndex[1].componentRef!.instance.mountContext).toBeUndefined();
605-
expect(comp.hookIndex[1].componentRef!.instance.mountContentChildren).toBeUndefined();
606-
607-
// Also, componentsLoaded should not yet have triggered
608-
expect(loadedComponents).toEqual([]);
609-
610-
// Wait for imports via fakeAsync()'s tick() that synchronously advances time for testing
611-
// This didn't always work. Used to have to manually wait by using (done) => {} as the testing wrapper function isntead of faceAsync,
612-
// then wait via setTimeout() and call done() when testing is finished. This had the disadvantage of actually having to wait for the timeout
613-
tick(500);
614-
615-
// Lazy-loaded component should be loaded by now in anchor
616-
expect(fixture.nativeElement.querySelector('.lazy-component')).not.toBe(null);
617-
expect(fixture.nativeElement.querySelector('.lazy-component').parentElement.tagName).toBe(anchorElementTag.toUpperCase());
618-
expect(comp.hookIndex[2].componentRef!.instance.constructor.name).toBe('LazyTestComponent');
619-
expect(comp.hookIndex[2].componentRef!.instance.name).toBe('sleepy');
620-
621-
// Make sure that onDynamicChanges has triggered again (with contentChildren)
622-
expect(comp.hookIndex[1].componentRef!.instance.onDynamicChanges['calls'].count()).toBe(1);
623-
expect(comp.hookIndex[1].componentRef!.instance.changesContext).toEqual(context);
624-
expect(comp.hookIndex[1].componentRef!.instance.changesContentChildren.length).toBe(1);
625-
expect(comp.hookIndex[1].componentRef!.instance.changesContentChildren[0].componentRef.location.nativeElement.tagName).toBe(anchorElementTag.toUpperCase());
626-
627-
// Make sure that onDynamicMount has triggered
628-
expect(comp.hookIndex[1].componentRef!.instance.onDynamicMount['calls'].count()).toBe(1);
629-
expect(comp.hookIndex[1].componentRef!.instance.mountContext).toEqual(context);
630-
expect(comp.hookIndex[1].componentRef!.instance.mountContentChildren.length).toBe(1);
631-
expect(comp.hookIndex[1].componentRef!.instance.mountContentChildren[0].componentRef.location.nativeElement.tagName).toBe(anchorElementTag.toUpperCase());
632-
633-
// ComponentsLoaded should have emitted now and contain the lazy-loaded component
634-
expect(loadedComponents.length).toBe(3);
566+
const testText = `
567+
<p>
568+
A couple of components:
569+
[multitag-string]
570+
[whatever-string][/whatever-string]
571+
[/multitag-string]
572+
[singletag-string]
573+
</p>
574+
`;
575+
576+
comp.content = testText;
577+
comp.context = context;
578+
let loadedComponents: LoadedComponent[] = [];
579+
comp.componentsLoaded.pipe(first()).subscribe((lc: any) => loadedComponents = lc);
580+
comp.ngOnChanges({content: true, context: true} as any);
581+
582+
// Everything except the lazy-loaded component should be loaded
583+
expect(fixture.nativeElement.querySelector('.multitag-component')).not.toBe(null);
584+
expect(fixture.nativeElement.querySelector('.lazy-component')).toBe(null);
585+
expect(fixture.nativeElement.querySelector('.singletag-component')).not.toBe(null);
586+
587+
expect(Object.values(comp.hookIndex).length).toBe(3);
588+
expect(comp.hookIndex[1].componentRef!.instance.constructor.name).toBe('MultiTagTestComponent');
589+
expect(comp.hookIndex[2].componentRef).toBeNull();
590+
expect(comp.hookIndex[3].componentRef!.instance.constructor.name).toBe('SingleTagTestComponent');
591+
592+
// Make sure that onDynamicChanges has triggered on component init
593+
spyOn(comp.hookIndex[1].componentRef!.instance, 'onDynamicChanges').and.callThrough();
594+
expect(comp.hookIndex[1].componentRef!.instance.onDynamicChanges['calls'].count()).toBe(0);
595+
expect(comp.hookIndex[1].componentRef!.instance.changesContext).toEqual(context);
596+
expect(comp.hookIndex[1].componentRef!.instance.changesContentChildren).toBeUndefined();
597+
598+
// Make sure that onDynamicMount has not yet triggered
599+
spyOn(comp.hookIndex[1].componentRef!.instance, 'onDynamicMount').and.callThrough();
600+
expect(comp.hookIndex[1].componentRef!.instance.onDynamicMount['calls'].count()).toBe(0);
601+
expect(comp.hookIndex[1].componentRef!.instance.mountContext).toBeUndefined();
602+
expect(comp.hookIndex[1].componentRef!.instance.mountContentChildren).toBeUndefined();
603+
604+
// Also, componentsLoaded should not yet have triggered
605+
expect(loadedComponents).toEqual([]);
606+
607+
// Wait for imports via fakeAsync()'s tick() that synchronously advances time for testing
608+
// This didn't always work. Used to have to manually wait by using (done) => {} as the testing wrapper function isntead of faceAsync,
609+
// then wait via setTimeout() and call done() when testing is finished. This had the disadvantage of actually having to wait for the timeout
610+
tick(500);
611+
612+
// Lazy-loaded component should be loaded by now in anchor
613+
expect(fixture.nativeElement.querySelector('.lazy-component')).not.toBe(null);
614+
expect(fixture.nativeElement.querySelector('.lazy-component').parentElement.tagName).toBe(anchorElementTag.toUpperCase());
615+
expect(comp.hookIndex[2].componentRef!.instance.constructor.name).toBe('LazyTestComponent');
616+
expect(comp.hookIndex[2].componentRef!.instance.name).toBe('sleepy');
617+
618+
// Make sure that onDynamicChanges has triggered again (with contentChildren)
619+
expect(comp.hookIndex[1].componentRef!.instance.onDynamicChanges['calls'].count()).toBe(1);
620+
expect(comp.hookIndex[1].componentRef!.instance.changesContext).toEqual(context);
621+
expect(comp.hookIndex[1].componentRef!.instance.changesContentChildren.length).toBe(1);
622+
expect(comp.hookIndex[1].componentRef!.instance.changesContentChildren[0].componentRef.location.nativeElement.tagName).toBe(anchorElementTag.toUpperCase());
623+
624+
// Make sure that onDynamicMount has triggered
625+
expect(comp.hookIndex[1].componentRef!.instance.onDynamicMount['calls'].count()).toBe(1);
626+
expect(comp.hookIndex[1].componentRef!.instance.mountContext).toEqual(context);
627+
expect(comp.hookIndex[1].componentRef!.instance.mountContentChildren.length).toBe(1);
628+
expect(comp.hookIndex[1].componentRef!.instance.mountContentChildren[0].componentRef.location.nativeElement.tagName).toBe(anchorElementTag.toUpperCase());
629+
630+
// ComponentsLoaded should have emitted now and contain the lazy-loaded component
631+
expect(loadedComponents.length).toBe(3);
632+
633+
expect(loadedComponents[0].hookId).toBe(1);
634+
expect(loadedComponents[0].hookValue).toEqual({openingTag: `[multitag-string]`, closingTag: `[/multitag-string]`, element: null, elementSnapshot: null});
635+
expect(loadedComponents[0].hookParser).toBeDefined();
636+
expect(loadedComponents[0].componentRef.instance.numberProp).toBe(4);
637+
638+
expect(loadedComponents[1].hookId).toBe(2);
639+
expect(loadedComponents[1].hookValue).toEqual({openingTag: `[whatever-string]`, closingTag: `[/whatever-string]`, element: null, elementSnapshot: null});
640+
expect(loadedComponents[1].hookParser).toBeDefined();
641+
expect(loadedComponents[1].componentRef.instance.name).toBe('sleepy');
642+
643+
expect(loadedComponents[2].hookId).toBe(3);
644+
expect(loadedComponents[2].hookValue).toEqual({openingTag: `[singletag-string]`, closingTag: null, element: null, elementSnapshot: null});
645+
expect(loadedComponents[2].hookParser).toBeDefined();
646+
expect(loadedComponents[2].componentRef.instance.numberProp).toBe(87);
647+
}
635648

636-
expect(loadedComponents[0].hookId).toBe(1);
637-
expect(loadedComponents[0].hookValue).toEqual({openingTag: `[multitag-string]`, closingTag: `[/multitag-string]`, element: null, elementSnapshot: null});
638-
expect(loadedComponents[0].hookParser).toBeDefined();
639-
expect(loadedComponents[0].componentRef.instance.numberProp).toBe(4);
649+
// Test with function that returns promise with component class directly
650+
testLazyLoading(() =>
651+
new Promise(resolve => setTimeout(() => resolve({LazyTestComponent: LazyTestComponent}), 100))
652+
.then((m: any) => m.LazyTestComponent))
640653

641-
expect(loadedComponents[1].hookId).toBe(2);
642-
expect(loadedComponents[1].hookValue).toEqual({openingTag: `[whatever-string]`, closingTag: `[/whatever-string]`, element: null, elementSnapshot: null});
643-
expect(loadedComponents[1].hookParser).toBeDefined();
644-
expect(loadedComponents[1].componentRef.instance.name).toBe('sleepy');
654+
// Test with explicit LazyLoadComponentConfig
655+
testLazyLoading({
656+
importPromise: () => new Promise(resolve => setTimeout(() => resolve({LazyTestComponent: LazyTestComponent}), 100)),
657+
importName: 'LazyTestComponent'
658+
});
645659

646-
expect(loadedComponents[2].hookId).toBe(3);
647-
expect(loadedComponents[2].hookValue).toEqual({openingTag: `[singletag-string]`, closingTag: null, element: null, elementSnapshot: null});
648-
expect(loadedComponents[2].hookParser).toBeDefined();
649-
expect(loadedComponents[2].componentRef.instance.numberProp).toBe(87);
650660
}));
651661

652662
it('#should check that the "importPromise"-field of lazy-loaded parsers is not the promise itself', () => {

0 commit comments

Comments
 (0)