-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(common): introduce ng-dynamic directive
- Loading branch information
Showing
19 changed files
with
340 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# NgDynamic | ||
|
||
Helper directive to render dynamic tag. | ||
|
||
## Type | ||
|
||
**Directive** | ||
|
||
## Provenance | ||
|
||
None. | ||
|
||
## NgModule | ||
|
||
`@angular-contrib/core#ContribNgDynamicModule` | ||
|
||
## Usage | ||
|
||
```typescript | ||
@Component({ | ||
template: ` | ||
<ng-dynamic [tag]="tag" [props]="props" [attrs]="attrs" [styles]="styles"></ng-dynamic> | ||
` | ||
}) | ||
class MyComponent {} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './ng-dynamic'; | ||
export * from './ng-dynamic.module'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { NgDynamic } from './ng-dynamic'; | ||
|
||
@NgModule({ | ||
declarations: [NgDynamic], | ||
exports: [NgDynamic], | ||
}) | ||
export class ContribNgDynamicModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TestComponent>; | ||
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(`<section>Content</section>`); | ||
expect(elements[1].nativeElement.outerHTML).toBe(`<section id="test-id">Content</section>`); | ||
expect(elements[2].nativeElement.outerHTML).toBe(`<section title="test-title">Content</section>`); | ||
expect(elements[3].nativeElement.outerHTML).toBe(`<section style="height: 50px;">Content</section>`); | ||
}); | ||
|
||
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(`<article>Content</article>`); | ||
expect(elements[1].nativeElement.outerHTML).toBe(`<article id="test-id2">Content</article>`); | ||
expect(elements[2].nativeElement.outerHTML).toBe(`<article title="test-title2">Content</article>`); | ||
expect(elements[3].nativeElement.outerHTML).toBe(`<article style="height: 60px;">Content</article>`); | ||
}); | ||
}); | ||
|
||
@Component({ | ||
template: ` | ||
<ng-dynamic [tag]="tag">Content</ng-dynamic> | ||
<ng-dynamic [tag]="tag" [props]="props">Content</ng-dynamic> | ||
<ng-dynamic [tag]="tag" [attrs]="attrs">Content</ng-dynamic> | ||
<ng-dynamic [tag]="tag" [styles]="styles">Content</ng-dynamic> | ||
`, | ||
}) | ||
class TestComponent { | ||
tag = 'section'; | ||
props = { id: 'test-id' }; | ||
attrs = { title: 'test-title' }; | ||
styles = { height: '50px' }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, unknown> = {}; | ||
@Input() attrs: Record<string, string | null> = {}; | ||
@Input() styles: Record<string, string | number | null> = {}; | ||
|
||
@HostBinding('style.display') display = 'none'; | ||
|
||
private hostParent!: Element; | ||
private el: Element | null = null; | ||
private propsDiffer: KeyValueDiffer<string, unknown> | null = null; | ||
private attrsDiffer: KeyValueDiffer<string, string | null> | null = null; | ||
private stylesDiffer: KeyValueDiffer<string, string | number | null> | null = null; | ||
|
||
constructor( | ||
private host: ElementRef<HTMLElement>, | ||
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: `<ng-content></ng-content>`, | ||
template: ``, | ||
}) | ||
export class NgLet<T = unknown> { | ||
export class NgLet<T = unknown> implements OnInit { | ||
@Input() data!: T; | ||
|
||
constructor( | ||
private renderer: Renderer2, | ||
private host: ElementRef<HTMLElement>, | ||
) {} | ||
|
||
ngOnInit(): void { | ||
this.renderer.removeChild(this.renderer.parentNode(this.host.nativeElement), this.host.nativeElement); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`); | ||
} | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './renderer-extension'; | ||
export * from './renderer-extension.module'; |
7 changes: 7 additions & 0 deletions
7
projects/core/src/renderer-extension/renderer-extension.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { RendererExtension } from './renderer-extension'; | ||
|
||
@NgModule({ | ||
providers: [ RendererExtension ], | ||
}) | ||
export class ContribRendererExtensionModule {} |
Empty file.
10 changes: 10 additions & 0 deletions
10
projects/core/src/renderer-extension/renderer-extension.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Injectable } from '@angular/core'; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class RendererExtension { | ||
getChildNodes(node: Node): NodeList { | ||
return node.childNodes; | ||
} | ||
} |
Oops, something went wrong.