Skip to content

Commit 001ce08

Browse files
committed
Feat: Added support for self-closing tags,
Feat: TextSelectorHookParser now uses selector as host element by default,
1 parent c2bf93f commit 001ce08

14 files changed

+219
-91
lines changed

CHANGELOG.md

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

55
## [Unreleased]
66

7+
## [3.1.0] - 2024-10-08
8+
### Added
9+
- Feat: Added support for self-closing tags in TextSelectorHookParser via new "allowSelfClosing" option in SelectorHookParserConfig
10+
- Feat: TextSelectorHookParser now uses selector as host element if no custom hostElementTag given
11+
12+
### Misc
13+
- Maintenance: Deprecated older "enclosing" option in SelectorHookParserConfig
14+
715
## [3.0.4] - 2024-09-26
816
### Maintenance
917
- Fix: Added support for new signal-based inputs
@@ -40,7 +48,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4048
### Misc
4149
- New website: https://angular-dynamic-hooks.com/
4250

43-
4451
## [2.1.2] - 2024-08-21
4552
### Added
4653
- Fix: Content slot elements should no longer appear as component children after rendering is done
@@ -142,7 +149,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
142149
### Added
143150
- This was the initial release, so everything was added here, really.
144151

145-
[Unreleased]: https://github.com/Angular-Dynamic-Hooks/ngx-dynamic-hooks/compare/v3.0.4...HEAD
152+
[Unreleased]: https://github.com/Angular-Dynamic-Hooks/ngx-dynamic-hooks/compare/v3.1.0...HEAD
153+
[3.1.0]: https://github.com/Angular-Dynamic-Hooks/ngx-dynamic-hooks/compare/v3.0.4...v3.1.0
146154
[3.0.4]: https://github.com/Angular-Dynamic-Hooks/ngx-dynamic-hooks/compare/v3.0.3...v3.0.4
147155
[3.0.3]: https://github.com/Angular-Dynamic-Hooks/ngx-dynamic-hooks/compare/v3.0.2...v3.0.3
148156
[3.0.2]: https://github.com/Angular-Dynamic-Hooks/ngx-dynamic-hooks/compare/v3.0.1...v3.0.2

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.4",
3+
"version": "3.1.0",
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/constants/regexes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const regexes: any = {};
22

33
// General
44
const variableName = '[a-zA-Z_$]+[a-zA-Z0-9_$]*';
5-
const attributeName = '[a-zA-Z_$\\-]+[a-zA-Z0-9_$]*';
5+
const attributeName = '[a-zA-Z$\\-_:][a-zA-Z$\\-_:0-9\\.]*';
66

77
// Attribute regex
88
regexes.attributeNameNoBracketsRegex = '(' + attributeName + ')';

projects/ngx-dynamic-hooks/src/lib/parsers/selector/selectorHookParserConfig.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,17 @@ export interface SelectorHookParserConfig {
2626
hostElementTag?: string;
2727

2828
/**
29-
* The Injector to create the component with.
30-
*/
31-
injector?: Injector;
32-
33-
/**
34-
* The EnvironmentInjector to create the component with.
29+
* Whether to use regular expressions rather than HTML/DOM-based methods to find the hook elements
3530
*/
36-
environmentInjector?: EnvironmentInjector;
31+
parseWithRegex?: boolean;
3732

3833
/**
39-
* Whether to use regular expressions rather than HTML/DOM-based methods to find the hook elements
34+
* Whether to allow using self-closing selector tags (<hook/>) in addition to enclosing tags (`<hook>...</hook>`)
4035
*/
41-
parseWithRegex?: boolean;
36+
allowSelfClosing?: boolean;
4237

4338
/**
44-
* Whether the selector is enclosing (`<hook>...</hook>`) or not (`<hook>`).
39+
* @deprecated Whether the selector is enclosing (`<hook>...</hook>`) or not (`<hook>`). Use the "allowSelfClosing" option for a more modern approach.
4540
*/
4641
enclosing?: boolean;
4742

@@ -60,6 +55,16 @@ export interface SelectorHookParserConfig {
6055
*/
6156
unescapeStrings?: boolean;
6257

58+
/**
59+
* The Injector to create the component with.
60+
*/
61+
injector?: Injector;
62+
63+
/**
64+
* The EnvironmentInjector to create the component with.
65+
*/
66+
environmentInjector?: EnvironmentInjector;
67+
6368
/**
6469
* A list of inputs to ignore.
6570
*/
@@ -104,6 +109,7 @@ export const selectorHookParserConfigDefaults: SelectorHookParserConfigDefaults
104109
selector: undefined,
105110
hostElementTag: undefined,
106111
injector: undefined,
112+
allowSelfClosing: true,
107113
enclosing: true,
108114
bracketStyle: {opening: '<', closing: '>'},
109115
parseInputs: true,

projects/ngx-dynamic-hooks/src/lib/parsers/selector/selectorHookParserConfigResolver.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,24 @@ export class SelectorHookParserConfigResolver {
5353
parserConfig.selector = userParserConfig.selector;
5454
}
5555

56+
// hostElementTag
5657
if (userParserConfig.hasOwnProperty('hostElementTag')) {
5758
if (typeof userParserConfig.hostElementTag !== 'string') { throw Error('The submitted "hostElementTag" property in the SelectorHookParserConfig must be of type string, was ' + typeof userParserConfig.hostElementTag); }
5859
parserConfig.hostElementTag = userParserConfig.hostElementTag;
5960
}
6061

61-
// injector (defaults to undefined)
62-
if (userParserConfig.hasOwnProperty('injector')) {
63-
parserConfig.injector = userParserConfig.injector;
64-
}
65-
66-
// environmentInjector (defaults to undefined)
67-
if (userParserConfig.hasOwnProperty('environmentInjector')) {
68-
parserConfig.environmentInjector = userParserConfig.environmentInjector;
69-
}
70-
7162
// parseWithRegex
7263
if (userParserConfig.hasOwnProperty('parseWithRegex')) {
7364
if (typeof userParserConfig.parseWithRegex !== 'boolean') { throw Error('The submitted "parseWithRegex" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.parseWithRegex); }
7465
parserConfig.parseWithRegex = userParserConfig.parseWithRegex;
7566
}
7667

68+
// allowSelfClosing
69+
if (userParserConfig.hasOwnProperty('allowSelfClosing')) {
70+
if (typeof userParserConfig.allowSelfClosing !== 'boolean') { throw Error('The submitted "allowSelfClosing" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.allowSelfClosing); }
71+
parserConfig.allowSelfClosing = userParserConfig.allowSelfClosing;
72+
}
73+
7774
// enclosing
7875
if (userParserConfig.hasOwnProperty('enclosing')) {
7976
if (typeof userParserConfig.enclosing !== 'boolean') { throw Error('The submitted "enclosing" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.enclosing); }
@@ -88,6 +85,16 @@ export class SelectorHookParserConfigResolver {
8885
parserConfig.bracketStyle = userParserConfig.bracketStyle;
8986
}
9087

88+
// injector (defaults to undefined)
89+
if (userParserConfig.hasOwnProperty('injector')) {
90+
parserConfig.injector = userParserConfig.injector;
91+
}
92+
93+
// environmentInjector (defaults to undefined)
94+
if (userParserConfig.hasOwnProperty('environmentInjector')) {
95+
parserConfig.environmentInjector = userParserConfig.environmentInjector;
96+
}
97+
9198
// unescapeStrings
9299
if (userParserConfig.hasOwnProperty('unescapeStrings')) {
93100
if (typeof userParserConfig.unescapeStrings !== 'boolean') { throw Error('The submitted "unescapeStrings" property in the SelectorHookParserConfig must be of type boolean, was ' + typeof userParserConfig.unescapeStrings); }

projects/ngx-dynamic-hooks/src/lib/parsers/selector/text/tagHookFinder.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ export class TagHookFinder {
4848
return this.hookFinder.find(content, openingTagRegex, closingTagRegex, true, options);
4949
}
5050

51+
/**
52+
* Finds self-closing Angular component selectors
53+
*
54+
* @param content - The content to parse
55+
* @param selector - The Angular selector to find
56+
* @param bracketStyle - What bracket style to use
57+
* @param options - The current ParseOptions
58+
*/
59+
findSelfClosingTags(content: string, selector: string, bracketStyle: {opening: string, closing: string} = {opening: '<', closing: '>'}, options: ParseOptions): HookPosition[] {
60+
const selfClosingTagRegex = this.generateOpeningTagRegex(selector, bracketStyle, true);
61+
62+
return this.hookFinder.find(content, selfClosingTagRegex, undefined, undefined, options);
63+
}
64+
5165
// Hook regex helper
5266
// ----------------------------------------------------------------------------------------------------------------------------------------
5367

@@ -57,13 +71,13 @@ export class TagHookFinder {
5771
* @param selector - The selector name
5872
* @param bracketStyle - What bracket style to use
5973
*/
60-
private generateOpeningTagRegex(selector: string, bracketStyle: {opening: string, closing: string} = {opening: '<', closing: '>'}): RegExp {
74+
private generateOpeningTagRegex(selector: string, bracketStyle: {opening: string, closing: string} = {opening: '<', closing: '>'}, selfClosing: boolean = false): RegExp {
6175
// Find opening tag of hook lazily
62-
// Examples for this regex: https://regex101.com/r/WjTsmA/1
76+
// Examples for this regex: https://regex101.com/r/Glyt2Z/1
6377
// Features: Ignores redundant whitespace & line-breaks, supports n attributes, both normal and []-attribute-name-syntax, both ' and " as attribute-value delimiters
6478
const openingArrow = this.escapeRegex(bracketStyle.opening);
6579
const selectorName = this.escapeRegex(selector);
66-
const closingArrow = this.escapeRegex(bracketStyle.closing);
80+
const closingArrow = (selfClosing ? '\\/' : '') + this.escapeRegex(bracketStyle.closing);
6781
const space = '\\s';
6882

6983
const attributeValuesOR = '(?:' + regexes.attributeValueDoubleQuotesRegex + '|' + regexes.attributeValueSingleQuotesRegex + ')';

projects/ngx-dynamic-hooks/src/lib/parsers/selector/text/textSelectorHookParser.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,25 @@ export class TextSelectorHookParser implements HookParser {
2222
}
2323

2424
public findHooks(content: string, context: any, options: ParseOptions): HookPosition[] {
25-
return this.config.enclosing ?
25+
let hookPositions = this.config.enclosing ?
2626
this.tagHookFinder.findEnclosingTags(content, this.config.selector!, this.config.bracketStyle, options) :
2727
this.tagHookFinder.findSingleTags(content, this.config.selector!, this.config.bracketStyle, options);
28+
29+
if (this.config.allowSelfClosing) {
30+
hookPositions = [
31+
...hookPositions,
32+
...this.tagHookFinder.findSelfClosingTags(content, this.config.selector!, this.config.bracketStyle, options)
33+
];
34+
hookPositions.sort((a, b) => a.openingTagStartIndex - b.openingTagStartIndex);
35+
}
36+
37+
return hookPositions;
2838
}
2939

3040
public loadComponent(hookId: number, hookValue: HookValue, context: any, childNodes: any[], options: ParseOptions): HookComponentData {
3141
return {
3242
component: this.config.component,
33-
hostElementTag: this.config.hostElementTag,
43+
hostElementTag: this.config.hostElementTag || this.config.selector, // If no hostElementTag specified, use selector (which in the case of TextSelectorHookParser is only allowed to be tag name)
3444
injector: this.config.injector,
3545
environmentInjector: this.config.environmentInjector
3646
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ describe('DynamicHooksComponent', () => {
8181
const firstP = section.children[0];
8282
expect(firstP.childNodes[0].textContent).toBe('Here is a singletag component: ');
8383
const singletagStringComp = firstP.children[0];
84-
expect(singletagStringComp.tagName).toBe(anchorElementTag.toUpperCase());
84+
expect(singletagStringComp.tagName).toBe('SINGLETAG-STRING-SELECTOR');
8585
expect(singletagStringComp.children[0].classList.contains('singletag-component')).toBe(true);
8686
const secondP = section.children[1];
8787
expect(secondP.childNodes[0].textContent).toBe('And here is a multitag component');
8888
const multitagStringComp = section.children[2];
89-
expect(multitagStringComp.tagName).toBe(anchorElementTag.toUpperCase());
89+
expect(multitagStringComp.tagName).toBe('MULTITAG-STRING-SELECTOR');
9090
expect(multitagStringComp.children[0].classList.contains('multitag-component')).toBe(true);
9191
const span = multitagStringComp.children[0].children[0];
9292
expect(span.textContent).toBe('The first inner content');

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ describe('DynamicHooksService', () => {
8686
const firstP = section.children[0];
8787
expect(firstP.childNodes[0].textContent).toBe('Here is a singletag component: ');
8888
const singletagStringComp = firstP.children[0];
89-
expect(singletagStringComp.tagName).toBe(anchorElementTag.toUpperCase());
89+
expect(singletagStringComp.tagName).toBe('SINGLETAG-STRING-SELECTOR');
9090
expect(singletagStringComp.children[0].classList.contains('singletag-component')).toBe(true);
9191
const secondP = section.children[1];
9292
expect(secondP.childNodes[0].textContent).toBe('And here is a multitag component');
9393
const multitagStringComp = section.children[2];
94-
expect(multitagStringComp.tagName).toBe(anchorElementTag.toUpperCase());
94+
expect(multitagStringComp.tagName).toBe('MULTITAG-STRING-SELECTOR');
9595
expect(multitagStringComp.children[0].classList.contains('multitag-component')).toBe(true);
9696
const span = multitagStringComp.children[0].children[0];
9797
expect(span.textContent).toBe('The first inner content');

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ describe('Parser configuration', () => {
8383
expect(comp.activeParsers[0].constructor.name).toBe('TextSelectorHookParser');
8484
expect(Object.keys(comp.hookIndex).length).toBe(1);
8585
expect(comp.hookIndex[1].componentRef!.instance.constructor.name).toBe('SingleTagTestComponent');
86-
expect(fixture.nativeElement.innerHTML).toContain('This is a sentence with a <' + anchorElementTag);
87-
expect(fixture.nativeElement.children[0].tagName).toBe(anchorElementTag.toUpperCase());
86+
expect(fixture.nativeElement.innerHTML).toContain('This is a sentence with a <singletagtest');
87+
expect(fixture.nativeElement.children[0].tagName).toBe('SINGLETAGTEST');
8888
expect(fixture.nativeElement.querySelector('.singletag-component')).not.toBeNull();
8989

9090
// Should be able to load parsers that are services

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

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ describe('SelectorHookParserConfig', () => {
7474
expect(() => configResolver.processConfig(config as any))
7575
.toThrow(new Error('The submitted "parseWithRegex" property in the SelectorHookParserConfig must be of type boolean, was string'));
7676

77+
// Wrong allowSelfClosing type
78+
config = { component: SingleTagTestComponent, allowSelfClosing: 'true' };
79+
expect(() => configResolver.processConfig(config as any))
80+
.toThrow(new Error('The submitted "allowSelfClosing" property in the SelectorHookParserConfig must be of type boolean, was string'));
81+
7782
// Wrong enclosing type
7883
config = { component: SingleTagTestComponent, enclosing: 'true' };
7984
expect(() => configResolver.processConfig(config as any))
@@ -329,7 +334,7 @@ describe('SelectorHookParserConfig', () => {
329334
expect((<any>console.error)['calls'].count()).toBe(1);
330335
}));
331336

332-
it('#should recognize singletag hooks', () => {
337+
it('#should recognize non-enclosing hooks', () => {
333338
({fixture, comp} = prepareTestingModule(() => [
334339
provideDynamicHooks({
335340
parsers: [{
@@ -339,20 +344,64 @@ describe('SelectorHookParserConfig', () => {
339344
})
340345
]));
341346

342-
const testText = `<p>Here the multitag hook is set to be single tag instead: <multitagtest [simpleArray]="['arial', 'calibri']">text within hook</multitagtest></p>`;
347+
const testText = `<p>Here the multitag hook is set to be single tag instead: <multitagtest [simpleArray]="['arial', 'calibri']">. Some text after hook.</p>`;
343348
comp.content = testText;
344349
comp.ngOnChanges({content: true} as any);
345350

346-
expect(fixture.nativeElement.querySelector('.multitag-component')).not.toBe(null);
347-
expect(fixture.nativeElement.querySelector('.multitag-component').innerHTML.trim()).toBe('');
348351
expect(fixture.nativeElement.children[0].childNodes[0].textContent).toContain('Here the multitag hook is set to be single tag instead:');
349-
expect(fixture.nativeElement.children[0].childNodes[1].textContent).not.toContain('text within hook');
350-
expect(fixture.nativeElement.children[0].childNodes[2].textContent).toContain('text within hook');
352+
expect(fixture.nativeElement.children[0].childNodes[1].tagName).toBe('MULTITAGTEST');
353+
expect(fixture.nativeElement.children[0].childNodes[1].querySelector('.multitag-component')).not.toBe(null);
354+
expect(fixture.nativeElement.children[0].childNodes[1].querySelector('.multitag-component').innerHTML.trim()).toBe('');
355+
expect(fixture.nativeElement.children[0].childNodes[2].textContent).toContain('. Some text after hook.');
356+
expect(Object.keys(comp.hookIndex).length).toBe(1);
357+
expect(comp.hookIndex[1].componentRef!.instance.constructor.name).toBe('MultiTagTestComponent');
358+
expect(comp.hookIndex[1].componentRef!.instance.simpleArray).toEqual(['arial', 'calibri']);
359+
});
360+
361+
it('#should allow self-closing hooks', () => {
362+
({fixture, comp} = prepareTestingModule(() => [
363+
provideDynamicHooks({
364+
parsers: [{
365+
component: MultiTagTestComponent,
366+
allowSelfClosing: true,
367+
parseWithRegex: true
368+
}]
369+
})
370+
]));
371+
372+
const testText = `<p>Here the multitag hook is set to be self-closing instead: <multitagtest [simpleArray]="['arial', 'calibri']"/>. Some text after hook.</p>`;
373+
comp.content = testText;
374+
comp.ngOnChanges({content: true} as any);
375+
376+
expect(fixture.nativeElement.children[0].childNodes[0].textContent).toContain('Here the multitag hook is set to be self-closing instead:');
377+
expect(fixture.nativeElement.children[0].childNodes[1].tagName).toBe('MULTITAGTEST');
378+
expect(fixture.nativeElement.children[0].childNodes[1].querySelector('.multitag-component')).not.toBe(null);
379+
expect(fixture.nativeElement.children[0].childNodes[1].querySelector('.multitag-component').innerHTML.trim()).toBe('');
380+
expect(fixture.nativeElement.children[0].childNodes[2].textContent).toContain('. Some text after hook.');
351381
expect(Object.keys(comp.hookIndex).length).toBe(1);
352382
expect(comp.hookIndex[1].componentRef!.instance.constructor.name).toBe('MultiTagTestComponent');
353383
expect(comp.hookIndex[1].componentRef!.instance.simpleArray).toEqual(['arial', 'calibri']);
354384
});
355385

386+
it('#should disallow self-closing hooks, if requested', () => {
387+
({fixture, comp} = prepareTestingModule(() => [
388+
provideDynamicHooks({
389+
parsers: [{
390+
component: MultiTagTestComponent,
391+
allowSelfClosing: false,
392+
parseWithRegex: true
393+
}]
394+
})
395+
]));
396+
397+
const testText = `<p>Here the multitag hook is set to be self-closing instead: <multitagtest [simpleArray]="['arial', 'calibri']"/>. Some text after hook.</p>`;
398+
comp.content = testText;
399+
comp.ngOnChanges({content: true} as any);
400+
401+
expect(fixture.nativeElement.querySelector('.multitag-component')).toBe(null);
402+
expect(Object.keys(comp.hookIndex).length).toBe(0);
403+
});
404+
356405
it('#should recognize unique bracket styles', () => {
357406
({fixture, comp} = prepareTestingModule(() => [
358407
provideDynamicHooks({

0 commit comments

Comments
 (0)