Skip to content

Commit

Permalink
feat: introduce render-intercept
Browse files Browse the repository at this point in the history
  • Loading branch information
trotyl committed Sep 25, 2019
1 parent 7dfae7f commit 9f844bb
Show file tree
Hide file tree
Showing 17 changed files with 442 additions and 31 deletions.
12 changes: 10 additions & 2 deletions projects/common/src/ng-dynamic/ng-dynamic.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { NgModule } from '@angular/core';
import { ContribRenderExtensionModule } from '@angular-contrib/core';
import { NgDynamic } from './ng-dynamic';

@NgModule({
declarations: [NgDynamic],
exports: [NgDynamic],
imports: [
ContribRenderExtensionModule,
],
declarations: [
NgDynamic,
],
exports: [
NgDynamic,
],
})
export class ContribNgDynamicModule {}
4 changes: 1 addition & 3 deletions projects/common/src/ng-dynamic/ng-dynamic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
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 = {};

Expand All @@ -24,7 +23,6 @@ export class NgDynamic implements AfterContentInit, DoCheck, OnChanges, OnInit {
private host: ElementRef<HTMLElement>,
private renderer: Renderer2,
private kvDiffers: KeyValueDiffers,
private rendererEx: RendererExtension,
) {}

ngOnChanges(changes: SimpleChanges): void {
Expand Down Expand Up @@ -127,7 +125,7 @@ export class NgDynamic implements AfterContentInit, DoCheck, OnChanges, OnInit {
}

private moveChildNodes(from: Node, to: Node): void {
const nodes = this.rendererEx.getChildNodes(from);
const nodes = this.renderer.childNodes(from);
for (let i = 0; i < nodes.length; i++) {
this.renderer.appendChild(to, nodes[i]);
}
Expand Down
2 changes: 1 addition & 1 deletion projects/core/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ 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';
export * from './render-extension/index';
export * from './styling/index';
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@ Addition renderer methods for low-level operations.

## Type

**Service**
**Provider**

## Provenance

None.

## NgModule

`@angular-contrib/core#ContribRendererExtensionModule`
`@angular-contrib/core#ContribRenderExtensionModule`

## Usage

```typescript
import { RendererExtension } from '@angular-contrib/core';

@Component()
class MyComponent {
constructor(private rendererEx: RendererExtension) {}
constructor(private renderer: Renderer2) {}

onAction(node: Node) {
const childLength = this.rendererEx.getChildNodes(node).length;
const childLength = this.renderer.childNodes(node).length;

if (childLength === 0) {
throw new Error(`No children available`);
Expand Down
1 change: 1 addition & 0 deletions projects/core/src/render-extension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './render-extension.module';
20 changes: 20 additions & 0 deletions projects/core/src/render-extension/render-extension.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RENDER_INTERCEPTORS } from '../render-intercept/render-intercept';
import { ContribRenderInterceptModule } from '../render-intercept/render-intercept.module';
import { RendererExtensionInterceptor } from './render-extension';

declare module '@angular/core' {
interface Renderer2 {
childNodes(node: Node): NodeList;
}
}

@NgModule({
imports: [
ContribRenderInterceptModule,
],
providers: [
{ provide: RENDER_INTERCEPTORS, multi: true, useClass: RendererExtensionInterceptor },
]
})
export class ContribRenderExtensionModule {}
39 changes: 39 additions & 0 deletions projects/core/src/render-extension/render-extension.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Renderer2, Component, ElementRef } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ContribRenderExtensionModule } from './render-extension.module';

describe('Renderer extension', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContribRenderExtensionModule],
declarations: [TestComponent],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
});

it('should support childNodes', () => {
expect(component.childNodes.length).toBe(3);
});

});

@Component({
template: `<p><p><p>`,
})
class TestComponent {
constructor(
private host: ElementRef,
private renderer: Renderer2,
) {}

get childNodes() {
return this.renderer.childNodes(this.host.nativeElement);
}
}
7 changes: 7 additions & 0 deletions projects/core/src/render-extension/render-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RenderInterceptor } from '../render-intercept/render-intercept';

export class RendererExtensionInterceptor implements RenderInterceptor {
childNodes(node: Node): NodeList {
return node.childNodes;
}
}
42 changes: 42 additions & 0 deletions projects/core/src/render-intercept/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Renderer Interception

Intercepting the render process.

## Type

**Provider**

## Provenance

+ https://github.com/angular/angular/issues/3929

## NgModule

`@angular-contrib/core#ContribRenderInterceptModule`

## Usage

```typescript
import { ContribRenderInterceptModule, RenderInterceptor, RENDER_INTERCEPTORS } from '@angular-contrib/core';

class LinkSpyInterceptor implements RenderInterceptor {
setAttribute(el: Element, name: string, value: string, namespace: string | undefined, renderer: Renderer2) {
if (el.tagName === 'A' && name === 'href') {
console.log(`New link found: ${value}`);
}
return renderer.setAttribute(el, name, value, namespace);
}
}

@NgModule({
imports: [ ContribRenderInterceptModule ],
providers: [
{ provide: RENDER_INTERCEPTORS, multi: true, useClass: LinkSpyInterceptor },
]
})
class MyModule { }
```

## Requirements

+ ContribRenderInterceptModule
2 changes: 2 additions & 0 deletions projects/core/src/render-intercept/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './render-intercept';
export * from './render-intercept.module';
68 changes: 68 additions & 0 deletions projects/core/src/render-intercept/render-intercept.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NgModule, RendererFactory2, Renderer2, RendererType2, Inject, ComponentFactoryResolver, Injector } from '@angular/core';
import { RENDER_INTERCEPTORS, RenderInterceptor, createInterceptingRenderer, RendererRetriever } from './render-intercept';

declare module '@angular/core' {
interface Renderer2 {
isExtended?: boolean;
}
}

function throwMissingChildNodes(): never {
throw new Error(`Renderer2#childNodes not implemented, did you forget to import ContribRenderExtensionModule?`);
}

function patchRenderer(renderer: Renderer2): void {
if (renderer.isExtended == null) {
renderer.isExtended = true;
}

if (renderer.childNodes == null) {
renderer.childNodes = throwMissingChildNodes;
}
}

function patchExoticRenderer(renderer: Renderer2, delegate: Renderer2): void {
renderer.childNodes = (node: Node) => delegate.childNodes(node);
}

@NgModule({
declarations: [
RendererRetriever,
],
entryComponents: [
RendererRetriever,
],
})
export class ContribRenderInterceptModule {
private patchedFactories = new WeakSet<RendererFactory2>();

constructor(
injector: Injector,
cfr: ComponentFactoryResolver,
rendererFactory: RendererFactory2,
@Inject(RENDER_INTERCEPTORS) interceptors: RenderInterceptor[],
) {
if (!this.patchedFactories.has(rendererFactory)) {
const originalCreateRenderer = rendererFactory.createRenderer;
rendererFactory.createRenderer = function(hostElement: Node, type: RendererType2 | null): Renderer2 {
const originalRenderer = originalCreateRenderer.call(this, hostElement, type);
patchRenderer(originalRenderer);
return createInterceptingRenderer(originalRenderer, interceptors);
};
this.patchedFactories.add(rendererFactory);
}

const extendedRenderer = rendererFactory.createRenderer(null, null);
const host = extendedRenderer.createElement('div');
const componentFactory = cfr.resolveComponentFactory(RendererRetriever);
const { renderer } = componentFactory.create(injector, undefined, host).instance;

// DebugRenderer2 maybe in use, require manual patching
if (renderer.isExtended == null) {
const proto = Object.getPrototypeOf(renderer);
if (proto !== Object.prototype) {
patchExoticRenderer(proto, extendedRenderer);
}
}
}
}
52 changes: 52 additions & 0 deletions projects/core/src/render-intercept/render-intercept.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Renderer2, Component } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { RenderInterceptor, RENDER_INTERCEPTORS } from './render-intercept';
import { ContribRenderInterceptModule } from './render-intercept.module';

describe('Renderer interception', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
let interceptor: RenderInterceptor;

beforeEach(() => {
interceptor = {
setAttribute(el: Element, name: string, value: string, namespace: string | undefined, renderer: Renderer2) {
if (value === 'foo') {
value = 'bar';
}
return renderer.setAttribute(el, name, value, namespace);
},
createText(value: string, renderer: Renderer2) {
if (value === 'ABC') {
value = 'DEF';
}
return renderer.createText(value);
}
};
});

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContribRenderInterceptModule],
declarations: [TestComponent],
providers: [
{ provide: RENDER_INTERCEPTORS, multi: true, useValue: interceptor },
],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
});

it('should apply RenderInterceptor', () => {
expect(fixture.nativeElement.innerHTML).toBe(`<article id="bar">DEF</article>`);
});

});

@Component({
template: `<article id="foo">ABC</article>`,
})
class TestComponent {}
Loading

0 comments on commit 9f844bb

Please sign in to comment.