diff --git a/libs/html-tag/src/lib/html-tag/html-tag-config.spec.ts b/libs/html-tag/src/lib/html-tag/html-tag-config.spec.ts
index de7aee93..fe9c6a56 100644
--- a/libs/html-tag/src/lib/html-tag/html-tag-config.spec.ts
+++ b/libs/html-tag/src/lib/html-tag/html-tag-config.spec.ts
@@ -22,7 +22,7 @@ describe('HtmlTagConfig', () => {
});
describe('tag prop', () => {
- const { testValid, testInvalid } = testValidProp('tag');
+ const { testValid, testInvalid } = testProp('tag');
testValid(undefined);
testValid(null);
@@ -34,7 +34,7 @@ describe('HtmlTagConfig', () => {
});
describe('namespace prop', () => {
- const { testValid, testInvalid } = testValidProp('namespace');
+ const { testValid, testInvalid } = testProp('namespace');
testValid(undefined);
testValid(null);
@@ -46,7 +46,7 @@ describe('HtmlTagConfig', () => {
});
describe('attributes prop', () => {
- const { testValid, testInvalid } = testValidProp('attributes');
+ const { testValid, testInvalid } = testProp('attributes');
testValid(undefined);
testValid(null);
@@ -65,9 +65,33 @@ describe('HtmlTagConfig', () => {
testInvalid({ attr1: [] }, 'object records of arrays');
testInvalid({ attr1: {} }, 'object records of objects');
});
+
+ describe('properties prop', () => {
+ const { testValid, testInvalid } = testProp('properties');
+
+ testValid(undefined);
+ testValid(null);
+ testValid(
+ {
+ attr1: 'value1',
+ attr2: 2,
+ attr3: true,
+ attr4: [],
+ attr5: {},
+ attr6: () => {},
+ },
+ 'object records of unknown',
+ );
+ testValid({}, 'empty objects');
+
+ testInvalid('value', 'strings');
+ testInvalid(123, 'numbers');
+ testInvalid(true, 'booleans');
+ testInvalid([], 'arrays');
+ });
});
-function testValidProp(prop: string) {
+function testProp(prop: string) {
const testValid = (val: unknown, name?: string) =>
it(`should allow ${name || val}`, () => {
expect(() =>
diff --git a/libs/html-tag/src/lib/html-tag/html-tag-config.ts b/libs/html-tag/src/lib/html-tag/html-tag-config.ts
index 4c41f6e1..12b43390 100644
--- a/libs/html-tag/src/lib/html-tag/html-tag-config.ts
+++ b/libs/html-tag/src/lib/html-tag/html-tag-config.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Option, OptionTypeFactory } from '@orchestrator/core';
-import { record, string } from 'io-ts';
+import { record, string, unknown } from 'io-ts';
@Injectable({ providedIn: 'root' })
export class HtmlTagConfig {
@@ -11,7 +11,10 @@ export class HtmlTagConfig {
namespace?: string;
@OptionTypeFactory(() => record(string, string))
- attributes?: { [attr: string]: string };
+ attributes?: Record;
+
+ @OptionTypeFactory(() => record(string, unknown))
+ properties?: Record;
@Option()
text?: string;
diff --git a/libs/html-tag/src/lib/html-tag/html-tag.component.spec.ts b/libs/html-tag/src/lib/html-tag/html-tag.component.spec.ts
index 24a51dec..b507dd22 100644
--- a/libs/html-tag/src/lib/html-tag/html-tag.component.spec.ts
+++ b/libs/html-tag/src/lib/html-tag/html-tag.component.spec.ts
@@ -73,6 +73,24 @@ describe('HtmlTagComponent', () => {
expect(componentElem.children[0].name.toLowerCase()).toBe('div');
});
+ it('should not re-render when config.tag is same', () => {
+ component.config = { tag: 'div' };
+
+ fixture.detectChanges();
+
+ const componentElem1 = getComponentElem();
+ const divElem1 = componentElem1.children[0];
+
+ component.config = { tag: 'div' };
+
+ fixture.detectChanges();
+
+ const componentElem2 = getComponentElem();
+ const divElem2 = componentElem2.children[0];
+
+ expect(divElem1).toBe(divElem2);
+ });
+
it('should clear tag after config.tag is unset', () => {
component.config = { tag: 'div' };
@@ -99,43 +117,132 @@ describe('HtmlTagComponent', () => {
expect(divElem.nativeElement.namespaceURI).toBe('xxx');
});
- it('should render attributes from config.attributes', () => {
- component.config = {
- tag: 'div',
- attributes: { attr1: 'value1', attr2: 'value2' },
- };
+ describe('attributes', () => {
+ it('should render from config.attributes', () => {
+ component.config = {
+ tag: 'div',
+ attributes: { attr1: 'value1', attr2: 'value2' },
+ };
- fixture.detectChanges();
+ fixture.detectChanges();
- const componentElem = getComponentElem();
- const divElem = componentElem.children[0];
+ const componentElem = getComponentElem();
+ const divElem = componentElem.children[0];
+
+ expect(divElem.nativeElement.getAttribute('attr1')).toBe('value1');
+ expect(divElem.nativeElement.getAttribute('attr2')).toBe('value2');
+ });
+
+ it('should update when config.attributes changed', () => {
+ component.config = {
+ tag: 'div',
+ attributes: { attr1: 'value1', attr2: 'value2', attr3: 'value3' },
+ };
+
+ fixture.detectChanges();
+
+ component.config = {
+ tag: 'div',
+ attributes: { attr1: 'value1', attr2: 'value22', attr4: 'value4' },
+ };
+
+ fixture.detectChanges();
+
+ const componentElem = getComponentElem();
+ const divElem = componentElem.children[0];
+
+ expect(divElem.nativeElement.getAttribute('attr1')).toBe('value1');
+ expect(divElem.nativeElement.getAttribute('attr2')).toBe('value22');
+ expect(divElem.nativeElement.getAttribute('attr3')).toBe(null);
+ expect(divElem.nativeElement.getAttribute('attr4')).toBe('value4');
+ });
+
+ it('should clear when config.attributes is empty object', () => {
+ component.config = {
+ tag: 'div',
+ attributes: { attr1: 'value1', attr2: 'value2' },
+ };
+
+ fixture.detectChanges();
+
+ component.config = {
+ tag: 'div',
+ attributes: {},
+ };
- expect(divElem.nativeElement.getAttribute('attr1')).toBe('value1');
- expect(divElem.nativeElement.getAttribute('attr2')).toBe('value2');
+ fixture.detectChanges();
+
+ const componentElem = getComponentElem();
+ const divElem = componentElem.children[0];
+
+ expect(divElem.nativeElement.getAttribute('attr1')).toBe(null);
+ expect(divElem.nativeElement.getAttribute('attr2')).toBe(null);
+ });
});
- it('should update attributes when config.attributes changed', () => {
- component.config = {
- tag: 'div',
- attributes: { attr1: 'value1', attr2: 'value2', attr3: 'value3' },
- };
+ describe('properties', () => {
+ it('should render from config.properties', () => {
+ component.config = {
+ tag: 'div',
+ properties: { prop1: 'value1', prop2: true, prop3: 3 },
+ };
- fixture.detectChanges();
+ fixture.detectChanges();
- component.config = {
- tag: 'div',
- attributes: { attr1: 'value1', attr2: 'value22', attr4: 'value4' },
- };
+ const componentElem = getComponentElem();
+ const divElem = componentElem.children[0];
- fixture.detectChanges();
+ expect(divElem.nativeElement.prop1).toBe('value1');
+ expect(divElem.nativeElement.prop2).toBe(true);
+ expect(divElem.nativeElement.prop3).toBe(3);
+ });
- const componentElem = getComponentElem();
- const divElem = componentElem.children[0];
+ it('should update when config.properties changed', () => {
+ component.config = {
+ tag: 'div',
+ properties: { prop1: 'value1', prop2: true, prop3: 3 },
+ };
+
+ fixture.detectChanges();
+
+ component.config = {
+ tag: 'div',
+ properties: { prop1: 'value1', prop2: false, prop4: { prop4: true } },
+ };
- expect(divElem.nativeElement.getAttribute('attr1')).toBe('value1');
- expect(divElem.nativeElement.getAttribute('attr2')).toBe('value22');
- expect(divElem.nativeElement.getAttribute('attr3')).toBe(null);
- expect(divElem.nativeElement.getAttribute('attr4')).toBe('value4');
+ fixture.detectChanges();
+
+ const componentElem = getComponentElem();
+ const divElem = componentElem.children[0];
+
+ expect(divElem.nativeElement.prop1).toBe('value1');
+ expect(divElem.nativeElement.prop2).toBe(false);
+ expect(divElem.nativeElement.prop3).toBe(undefined);
+ expect(divElem.nativeElement.prop4).toEqual({ prop4: true });
+ });
+
+ it('should clear when config.properties is empty object', () => {
+ component.config = {
+ tag: 'div',
+ properties: { prop1: 'value1', prop2: true, prop3: 3 },
+ };
+
+ fixture.detectChanges();
+
+ component.config = {
+ tag: 'div',
+ properties: {},
+ };
+
+ fixture.detectChanges();
+
+ const componentElem = getComponentElem();
+ const divElem = componentElem.children[0];
+
+ expect(divElem.nativeElement.prop1).toBe(undefined);
+ expect(divElem.nativeElement.prop2).toBe(undefined);
+ expect(divElem.nativeElement.prop3).toBe(undefined);
+ });
});
it('should render items inside of the tag from items', () => {
diff --git a/libs/html-tag/src/lib/html-tag/html-tag.component.ts b/libs/html-tag/src/lib/html-tag/html-tag.component.ts
index 34b6e8c6..99000ca8 100644
--- a/libs/html-tag/src/lib/html-tag/html-tag.component.ts
+++ b/libs/html-tag/src/lib/html-tag/html-tag.component.ts
@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
Input,
+ KeyValueDiffer,
KeyValueDiffers,
OnChanges,
OnDestroy,
@@ -16,8 +17,16 @@ import {
OrchestratorConfigItem,
OrchestratorDynamicComponent,
} from '@orchestrator/core';
+import { string } from 'fp-ts';
import { HtmlTagConfig } from './html-tag-config';
+interface TrackRecord {
+ differ: KeyValueDiffer;
+ getRecord(): Record | undefined;
+ set(name: string, value: T): void;
+ remove(name: string): void;
+}
+
@Component({
selector: 'orc-html-tag',
templateUrl: './html-tag.component.html',
@@ -33,21 +42,38 @@ export class HtmlTagComponent
/** @internal */
@ViewChild('tagContentAnchor', { static: true, read: ViewContainerRef })
- _tagContentVcr?: ViewContainerRef;
+ readonly _tagContentVcr?: ViewContainerRef;
/** @internal */
@ViewChild('contentTpl', { static: true })
- _contentTpl?: TemplateRef;
+ readonly _contentTpl?: TemplateRef;
+
+ private readonly objectDiffer = this.keyValDiffers.find({});
+
+ private readonly hostElement: unknown = this.vcr.element.nativeElement;
+
+ private readonly trackRecordsMap = {
+ attributes: {
+ differ: this.objectDiffer.create(),
+ getRecord: () => this.config?.attributes,
+ set: (name, value) => this.setAttr(name, value),
+ remove: (name) => this.removeAttr(name),
+ } as TrackRecord,
+ properties: {
+ differ: this.objectDiffer.create(),
+ getRecord: () => this.config?.properties,
+ set: (name, value) => this.setProp(name, value),
+ remove: (name) => this.setProp(name, undefined),
+ } as TrackRecord,
+ };
- private attrsDiffer = this.keyValDiffers.find({}).create();
- private hostElement: unknown = this.vcr.element.nativeElement;
private tagName?: string;
private tagElement?: unknown;
constructor(
- private vcr: ViewContainerRef,
- private renderer: Renderer2,
- private keyValDiffers: KeyValueDiffers,
+ private readonly vcr: ViewContainerRef,
+ private readonly renderer: Renderer2,
+ private readonly keyValDiffers: KeyValueDiffers,
) {}
ngOnChanges(changes: SimpleChanges): void {
@@ -84,6 +110,7 @@ export class HtmlTagComponent
}
this.updateAttrs();
+ this.updateProps();
this.updateContent();
}
@@ -116,21 +143,29 @@ export class HtmlTagComponent
}
private updateAttrs() {
- const changes = this.attrsDiffer.diff(this.config?.attributes ?? {});
+ this.updateRecord(this.trackRecordsMap.attributes);
+ }
+
+ private updateProps() {
+ this.updateRecord(this.trackRecordsMap.properties);
+ }
+
+ private updateRecord(trackMap: TrackRecord) {
+ const changes = trackMap.differ.diff(trackMap.getRecord());
if (!changes || this.tagElement === undefined) {
return;
}
changes.forEachAddedItem((change) =>
- this.setAttr(change.key, change.currentValue),
+ trackMap.set(change.key, change.currentValue),
);
changes.forEachChangedItem((change) =>
- this.setAttr(change.key, change.currentValue),
+ trackMap.set(change.key, change.currentValue),
);
- changes.forEachRemovedItem((change) => this.removeAttr(change.key));
+ changes.forEachRemovedItem((change) => trackMap.remove(change.key));
}
private setAttr(key: string, value?: string | null) {
@@ -141,6 +176,10 @@ export class HtmlTagComponent
this.renderer.removeAttribute(this.tagElement, key);
}
+ private setProp(key: string, value: unknown) {
+ this.renderer.setProperty(this.tagElement, key, value);
+ }
+
private updateContent() {
if (this.config?.text) {
this.renderer.setProperty(