Skip to content

Commit

Permalink
feat(html-tag): add properties to html-tag configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
gund committed Mar 12, 2022
1 parent 8eb225c commit f259524
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 44 deletions.
32 changes: 28 additions & 4 deletions libs/html-tag/src/lib/html-tag/html-tag-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('HtmlTagConfig', () => {
});

describe('tag prop', () => {
const { testValid, testInvalid } = testValidProp('tag');
const { testValid, testInvalid } = testProp('tag');

testValid(undefined);
testValid(null);
Expand All @@ -34,7 +34,7 @@ describe('HtmlTagConfig', () => {
});

describe('namespace prop', () => {
const { testValid, testInvalid } = testValidProp('namespace');
const { testValid, testInvalid } = testProp('namespace');

testValid(undefined);
testValid(null);
Expand All @@ -46,7 +46,7 @@ describe('HtmlTagConfig', () => {
});

describe('attributes prop', () => {
const { testValid, testInvalid } = testValidProp('attributes');
const { testValid, testInvalid } = testProp('attributes');

testValid(undefined);
testValid(null);
Expand All @@ -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(() =>
Expand Down
7 changes: 5 additions & 2 deletions libs/html-tag/src/lib/html-tag/html-tag-config.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -11,7 +11,10 @@ export class HtmlTagConfig {
namespace?: string;

@OptionTypeFactory(() => record(string, string))
attributes?: { [attr: string]: string };
attributes?: Record<string, string>;

@OptionTypeFactory(() => record(string, unknown))
properties?: Record<string, unknown>;

@Option()
text?: string;
Expand Down
161 changes: 134 additions & 27 deletions libs/html-tag/src/lib/html-tag/html-tag.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };

Expand All @@ -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', () => {
Expand Down
61 changes: 50 additions & 11 deletions libs/html-tag/src/lib/html-tag/html-tag.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
Input,
KeyValueDiffer,
KeyValueDiffers,
OnChanges,
OnDestroy,
Expand All @@ -16,8 +17,16 @@ import {
OrchestratorConfigItem,
OrchestratorDynamicComponent,
} from '@orchestrator/core';
import { string } from 'fp-ts';
import { HtmlTagConfig } from './html-tag-config';

interface TrackRecord<T> {
differ: KeyValueDiffer<string, T>;
getRecord(): Record<string, T> | undefined;
set(name: string, value: T): void;
remove(name: string): void;
}

@Component({
selector: 'orc-html-tag',
templateUrl: './html-tag.component.html',
Expand All @@ -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<void>;
readonly _contentTpl?: TemplateRef<void>;

private readonly objectDiffer = this.keyValDiffers.find({});

private readonly hostElement: unknown = this.vcr.element.nativeElement;

private readonly trackRecordsMap = {
attributes: {
differ: this.objectDiffer.create<string, string>(),
getRecord: () => this.config?.attributes,
set: (name, value) => this.setAttr(name, value),
remove: (name) => this.removeAttr(name),
} as TrackRecord<string>,
properties: {
differ: this.objectDiffer.create<string, unknown>(),
getRecord: () => this.config?.properties,
set: (name, value) => this.setProp(name, value),
remove: (name) => this.setProp(name, undefined),
} as TrackRecord<unknown>,
};

private attrsDiffer = this.keyValDiffers.find({}).create<string, string>();
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 {
Expand Down Expand Up @@ -84,6 +110,7 @@ export class HtmlTagComponent
}

this.updateAttrs();
this.updateProps();
this.updateContent();
}

Expand Down Expand Up @@ -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<T>(trackMap: TrackRecord<T>) {
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) {
Expand All @@ -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(
Expand Down

0 comments on commit f259524

Please sign in to comment.