diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6191fc3..b344928 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -51,6 +51,8 @@ jobs:
paths:
- node_modules
key: v1-dependencies-{{ checksum "yarn.lock" }}
+
+ - run: yarn build:all
- run: yarn test
diff --git a/projects/common/package.json b/projects/common/package.json
index 7c452fd..71629b8 100644
--- a/projects/common/package.json
+++ b/projects/common/package.json
@@ -3,6 +3,7 @@
"version": "{{ version }}",
"peerDependencies": {
"@angular/common": "^{{ angularCompatVersion }}",
- "@angular/core": "^{{ angularCompatVersion }}"
+ "@angular/core": "^{{ angularCompatVersion }}",
+ "@angular-contrib/core": "{{ version }}"
}
}
diff --git a/projects/common/src/ng-dynamic/README.md b/projects/common/src/ng-dynamic/README.md
new file mode 100644
index 0000000..540373f
--- /dev/null
+++ b/projects/common/src/ng-dynamic/README.md
@@ -0,0 +1,26 @@
+# NgDynamic
+
+Helper directive to render dynamic tag.
+
+## Type
+
+**Directive**
+
+## Provenance
+
+None.
+
+## NgModule
+
+`@angular-contrib/core#ContribNgDynamicModule`
+
+## Usage
+
+```typescript
+@Component({
+ template: `
+
+ `
+})
+class MyComponent {}
+```
diff --git a/projects/common/src/ng-dynamic/index.ts b/projects/common/src/ng-dynamic/index.ts
new file mode 100644
index 0000000..3168944
--- /dev/null
+++ b/projects/common/src/ng-dynamic/index.ts
@@ -0,0 +1,2 @@
+export * from './ng-dynamic';
+export * from './ng-dynamic.module';
diff --git a/projects/common/src/ng-dynamic/ng-dynamic.module.ts b/projects/common/src/ng-dynamic/ng-dynamic.module.ts
new file mode 100644
index 0000000..617e971
--- /dev/null
+++ b/projects/common/src/ng-dynamic/ng-dynamic.module.ts
@@ -0,0 +1,8 @@
+import { NgModule } from '@angular/core';
+import { NgDynamic } from './ng-dynamic';
+
+@NgModule({
+ declarations: [NgDynamic],
+ exports: [NgDynamic],
+})
+export class ContribNgDynamicModule {}
diff --git a/projects/common/src/ng-dynamic/ng-dynamic.spec.ts b/projects/common/src/ng-dynamic/ng-dynamic.spec.ts
new file mode 100644
index 0000000..83ca66b
--- /dev/null
+++ b/projects/common/src/ng-dynamic/ng-dynamic.spec.ts
@@ -0,0 +1,65 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ContribNgDynamicModule } from './ng-dynamic.module';
+import { By } from '@angular/platform-browser';
+
+describe('ng-dynamic', () => {
+ let fixture: ComponentFixture;
+ let component: TestComponent;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent],
+ imports: [CommonModule, ContribNgDynamicModule],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should render dynamic element', () => {
+ fixture.detectChanges();
+
+ const elements = fixture.debugElement.queryAll(By.css('section'));
+
+ expect(elements[0].nativeElement.outerHTML).toBe(``);
+ expect(elements[1].nativeElement.outerHTML).toBe(``);
+ expect(elements[2].nativeElement.outerHTML).toBe(``);
+ expect(elements[3].nativeElement.outerHTML).toBe(``);
+ });
+
+ it('should support changing tag name', () => {
+ fixture.detectChanges();
+
+ component.tag = 'article';
+ component.props = { id: 'test-id2' };
+ component.attrs = { title: 'test-title2' };
+ component.styles = { height: '60px' };
+ fixture.detectChanges();
+
+ const elements = fixture.debugElement.queryAll(By.css('article'));
+
+ expect(elements[0].nativeElement.outerHTML).toBe(`Content`);
+ expect(elements[1].nativeElement.outerHTML).toBe(`Content`);
+ expect(elements[2].nativeElement.outerHTML).toBe(`Content`);
+ expect(elements[3].nativeElement.outerHTML).toBe(`Content`);
+ });
+});
+
+@Component({
+ template: `
+ Content
+ Content
+ Content
+ Content
+ `,
+})
+class TestComponent {
+ tag = 'section';
+ props = { id: 'test-id' };
+ attrs = { title: 'test-title' };
+ styles = { height: '50px' };
+}
diff --git a/projects/common/src/ng-dynamic/ng-dynamic.ts b/projects/common/src/ng-dynamic/ng-dynamic.ts
new file mode 100644
index 0000000..2a83d8c
--- /dev/null
+++ b/projects/common/src/ng-dynamic/ng-dynamic.ts
@@ -0,0 +1,158 @@
+import { Input, KeyValueDiffer, KeyValueDiffers, DoCheck, SimpleChanges, OnChanges, OnInit, ElementRef, Renderer2, Component, ViewChild, AfterContentInit, HostBinding, Directive } from '@angular/core';
+import { RendererExtension } from '@angular-contrib/core';
+
+const EMPTY_OBJ = {};
+
+@Directive({
+ selector: 'ng-dynamic',
+})
+export class NgDynamic implements AfterContentInit, DoCheck, OnChanges, OnInit {
+ @Input() tag: string = 'ng-unknown';
+ @Input() props: Record = {};
+ @Input() attrs: Record = {};
+ @Input() styles: Record = {};
+
+ @HostBinding('style.display') display = 'none';
+
+ private hostParent!: Element;
+ private el: Element | null = null;
+ private propsDiffer: KeyValueDiffer | null = null;
+ private attrsDiffer: KeyValueDiffer | null = null;
+ private stylesDiffer: KeyValueDiffer | null = null;
+
+ constructor(
+ private host: ElementRef,
+ private renderer: Renderer2,
+ private kvDiffers: KeyValueDiffers,
+ private rendererEx: RendererExtension,
+ ) {}
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (this.hostParent == null) {
+ this.hostParent = this.renderer.parentNode(this.host.nativeElement);
+ }
+
+ if (changes['tag']) {
+ this.createNewElement();
+ }
+
+ if (this.propsDiffer == null && changes['props']) {
+ this.propsDiffer = this.kvDiffers.find(EMPTY_OBJ).create();
+ }
+
+ if (this.attrsDiffer == null && changes['attrs']) {
+ this.attrsDiffer = this.kvDiffers.find(EMPTY_OBJ).create();
+ }
+
+ if (this.stylesDiffer == null && changes['styles']) {
+ this.stylesDiffer = this.kvDiffers.find(EMPTY_OBJ).create();
+ }
+ }
+
+ ngOnInit(): void {
+ if (this.hostParent == null) {
+ this.hostParent = this.renderer.parentNode(this.host.nativeElement);
+ }
+
+ if (this.el == null) {
+ this.createNewElement();
+ }
+ }
+
+ ngDoCheck(): void {
+ if (this.propsDiffer != null) {
+ const changes = this.propsDiffer.diff(this.props);
+ if (changes != null) {
+ changes.forEachRemovedItem((record) => this.setProp(record.key, null));
+ changes.forEachAddedItem((record) => this.setProp(record.key, record.currentValue));
+ changes.forEachChangedItem((record) => this.setProp(record.key, record.currentValue));
+ }
+ }
+
+ if (this.attrsDiffer != null) {
+ const changes = this.attrsDiffer.diff(this.attrs);
+ if (changes != null) {
+ changes.forEachRemovedItem((record) => this.setAttr(record.key, null));
+ changes.forEachAddedItem((record) => this.setAttr(record.key, record.currentValue));
+ changes.forEachChangedItem((record) => this.setAttr(record.key, record.currentValue));
+ }
+ }
+
+ if (this.stylesDiffer != null) {
+ const changes = this.stylesDiffer.diff(this.styles);
+ if (changes != null) {
+ changes.forEachRemovedItem((record) => this.setStyle(record.key, null));
+ changes.forEachAddedItem((record) => this.setStyle(record.key, record.currentValue));
+ changes.forEachChangedItem((record) => this.setStyle(record.key, record.currentValue));
+ }
+ }
+ }
+
+ ngAfterContentInit(): void {
+ this.moveChildNodes(this.host.nativeElement, this.el!);
+ }
+
+ private createNewElement(): void {
+ const newElement = this.renderer.createElement(this.tag);
+
+ if (this.el != null) {
+ this.renderer.insertBefore(this.hostParent, this.host.nativeElement, this.el);
+ this.renderer.removeChild(this.hostParent, this.el);
+ this.moveChildNodes(this.el, newElement);
+ }
+
+ this.el = newElement;
+
+ const propKeys = Object.keys(this.props);
+ const attrKeys = Object.keys(this.attrs);
+ const styleKeys = Object.keys(this.styles);
+
+ for (let i = 0; i < propKeys.length; i++) {
+ const prop = propKeys[i];
+ this.setProp(prop, this.props[prop]);
+ }
+
+ for (let i = 0; i < attrKeys.length; i++) {
+ const attr = attrKeys[i];
+ this.setProp(attr, this.attrs[attr]);
+ }
+
+ for (let i = 0; i < styleKeys.length; i++) {
+ const style = styleKeys[i];
+ this.setStyle(style, this.styles[style]);
+ }
+
+ this.renderer.insertBefore(this.hostParent, this.el, this.host.nativeElement);
+ this.renderer.removeChild(this.hostParent, this.host.nativeElement);
+ }
+
+ private moveChildNodes(from: Node, to: Node): void {
+ const nodes = this.rendererEx.getChildNodes(from);
+ for (let i = 0; i < nodes.length; i++) {
+ this.renderer.appendChild(to, nodes[i]);
+ }
+ }
+
+ private setProp(name: string, value: unknown): void {
+ this.renderer.setProperty(this.el, name, value);
+ }
+
+ private setAttr(name: string, value: string | null): void {
+ if (value != null) {
+ this.renderer.setAttribute(this.el, name, value);
+ } else {
+ this.renderer.removeAttribute(this.el, name);
+ }
+ }
+
+ private setStyle(nameAndUnit: string, value: string | number | null): void {
+ const [name, unit] = nameAndUnit.split('.');
+ value = value != null && unit ? `${value}${unit}` : value;
+
+ if (value != null) {
+ this.renderer.setStyle(this.el, name, value as string);
+ } else {
+ this.renderer.removeStyle(this.el, name);
+ }
+ }
+}
diff --git a/projects/common/src/ng-let/ng-let.ts b/projects/common/src/ng-let/ng-let.ts
index ff7fd17..8b21a01 100644
--- a/projects/common/src/ng-let/ng-let.ts
+++ b/projects/common/src/ng-let/ng-let.ts
@@ -1,9 +1,18 @@
-import { Component, Input } from '@angular/core';
+import { Component, Input, ElementRef, Renderer2, OnInit } from '@angular/core';
@Component({
selector: 'ng-let[data]',
- template: ``,
+ template: ``,
})
-export class NgLet {
+export class NgLet implements OnInit {
@Input() data!: T;
+
+ constructor(
+ private renderer: Renderer2,
+ private host: ElementRef,
+ ) {}
+
+ ngOnInit(): void {
+ this.renderer.removeChild(this.renderer.parentNode(this.host.nativeElement), this.host.nativeElement);
+ }
}
diff --git a/projects/common/src/public-api.ts b/projects/common/src/public-api.ts
index 47bdb93..79b2a8f 100644
--- a/projects/common/src/public-api.ts
+++ b/projects/common/src/public-api.ts
@@ -1,3 +1,4 @@
+export * from './ng-dynamic/index';
export * from './ng-for-in/index';
export * from './ng-host/index';
export * from './ng-init/index';
diff --git a/projects/core/src/change-detection-scheduler/README.md b/projects/core/src/change-detection-scheduler/README.md
index 7e8c98b..22c36b0 100644
--- a/projects/core/src/change-detection-scheduler/README.md
+++ b/projects/core/src/change-detection-scheduler/README.md
@@ -4,7 +4,7 @@ Automatically trigger change detection without Zone.js.
## Type
-**Enhancement**
+**Patch**
## Provenance
diff --git a/projects/core/src/iterable-differs/README.md b/projects/core/src/iterable-differs/README.md
index 28b3d5c..f3afa44 100644
--- a/projects/core/src/iterable-differs/README.md
+++ b/projects/core/src/iterable-differs/README.md
@@ -16,7 +16,7 @@ Supports extending `IterableDiffers` with custom differ implementations.
## Usage
-Provding custom `IterableDifferFactory` (Non-exclusive):
+Providing custom `IterableDifferFactory` (Non-exclusive):
```typescript
import { ContribIterableDiffersModule, ITERABLE_DIFFER_FACTORIES } from '@angular-contrib/core';
diff --git a/projects/core/src/key-value-differs/README.md b/projects/core/src/key-value-differs/README.md
index 71a36ae..3ea859a 100644
--- a/projects/core/src/key-value-differs/README.md
+++ b/projects/core/src/key-value-differs/README.md
@@ -16,7 +16,7 @@ Supports extending `KeyValueDiffers` with custom differ implementations.
## Usage
-Provding custom `KeyValueDifferFactory` (Non-exclusive):
+Providing custom `KeyValueDifferFactory` (Non-exclusive):
```typescript
import { ContribKeyValueDiffersModule, KEY_VALUE_DIFFER_FACTORIES } from '@angular-contrib/core';
diff --git a/projects/core/src/public-api.ts b/projects/core/src/public-api.ts
index 51143be..b695622 100644
--- a/projects/core/src/public-api.ts
+++ b/projects/core/src/public-api.ts
@@ -2,3 +2,4 @@ export * from './change-detection-scheduler/index';
export * from './fast-iterable-differ/index';
export * from './iterable-differs/index';
export * from './key-value-differs/index';
+export * from './renderer-extension/index';
diff --git a/projects/core/src/renderer-extension/README.md b/projects/core/src/renderer-extension/README.md
new file mode 100644
index 0000000..50d6970
--- /dev/null
+++ b/projects/core/src/renderer-extension/README.md
@@ -0,0 +1,34 @@
+# Renderer Extension
+
+Addition renderer methods for low-level operations.
+
+## Type
+
+**Service**
+
+## Provenance
+
+None.
+
+## NgModule
+
+`@angular-contrib/core#ContribRendererExtensionModule`
+
+## Usage
+
+```typescript
+import { RendererExtension } from '@angular-contrib/core';
+
+@Component()
+class MyComponent {
+ constructor(private rendererEx: RendererExtension) {}
+
+ onAction(node: Node) {
+ const childLength = this.rendererEx.getChildNodes(node).length;
+
+ if (childLength === 0) {
+ throw new Error(`No children available`);
+ }
+ }
+}
+```
diff --git a/projects/core/src/renderer-extension/index.ts b/projects/core/src/renderer-extension/index.ts
new file mode 100644
index 0000000..63e0c12
--- /dev/null
+++ b/projects/core/src/renderer-extension/index.ts
@@ -0,0 +1,2 @@
+export * from './renderer-extension';
+export * from './renderer-extension.module';
diff --git a/projects/core/src/renderer-extension/renderer-extension.module.ts b/projects/core/src/renderer-extension/renderer-extension.module.ts
new file mode 100644
index 0000000..d4e624b
--- /dev/null
+++ b/projects/core/src/renderer-extension/renderer-extension.module.ts
@@ -0,0 +1,7 @@
+import { NgModule } from '@angular/core';
+import { RendererExtension } from './renderer-extension';
+
+@NgModule({
+ providers: [ RendererExtension ],
+})
+export class ContribRendererExtensionModule {}
diff --git a/projects/core/src/renderer-extension/renderer-extension.spec.ts b/projects/core/src/renderer-extension/renderer-extension.spec.ts
new file mode 100644
index 0000000..e69de29
diff --git a/projects/core/src/renderer-extension/renderer-extension.ts b/projects/core/src/renderer-extension/renderer-extension.ts
new file mode 100644
index 0000000..196aa68
--- /dev/null
+++ b/projects/core/src/renderer-extension/renderer-extension.ts
@@ -0,0 +1,10 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class RendererExtension {
+ getChildNodes(node: Node): NodeList {
+ return node.childNodes;
+ }
+}
diff --git a/tslint.json b/tslint.json
index 9987d7a..d7649d6 100644
--- a/tslint.json
+++ b/tslint.json
@@ -31,7 +31,12 @@
"max-classes-per-file": false,
"max-line-length": [
true,
- 140
+ {
+ "limit": 160,
+ "ignore-pattern": "^import |^export {(.*?)}",
+ "check-strings": true,
+ "check-regex": true
+ }
],
"member-access": false,
"member-ordering": [
@@ -62,7 +67,7 @@
"ignore-params",
"ignore-properties"
],
- "no-input-rename": true,
+ "no-input-rename": false,
"no-inputs-metadata-property": true,
"no-non-null-assertion": false,
"no-output-native": true,