Skip to content

Commit

Permalink
feat(attributes): Add dynamic attributes directive
Browse files Browse the repository at this point in the history
That is working with ngComponentOutlet directive for right now

related #120
  • Loading branch information
gund committed Apr 23, 2018
1 parent c3e45dc commit 71f10ad
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 0 deletions.
210 changes: 210 additions & 0 deletions src/dynamic/dynamic-attributes.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import {
InjectedComponent,
TestComponent as TestComponentBase,
TestModule,
} from '../test/test.component';
import { getByPredicate } from '../test/util';
import { COMPONENT_INJECTOR } from './component-injector';
import { ComponentOutletInjectorDirective } from './component-outlet-injector.directive';
import { DynamicAttributesDirective } from './dynamic-attributes.directive';
import { DynamicComponent } from './dynamic.component';

const getInjectedComponentFrom = getByPredicate<InjectedComponent>(
By.directive(InjectedComponent),
);

@Component({})
class TestComponent extends TestComponentBase {
comp = InjectedComponent;
attrs: { [k: string]: string };
}

describe('DynamicAttributesDirective', () => {
let hostTemplate = '';
let fixture: ComponentFixture<TestComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CommonModule, TestModule],
declarations: [
DynamicAttributesDirective,
TestComponent,
ComponentOutletInjectorDirective,
DynamicComponent,
],
providers: [{ provide: COMPONENT_INJECTOR, useValue: DynamicComponent }],
})
.overrideTemplate(TestComponent, hostTemplate)
.compileComponents();

fixture = TestBed.createComponent(TestComponent);
}));

describe('with `ngComponentOutlet`', () => {
beforeAll(() =>
(hostTemplate = `<ng-container [ngComponentOutlet]="comp" [ndcDynamicAttributes]="attrs"></ng-container>`));

it('should set attrs on injected component', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;

fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toMatchObject(attrs);
});

it('should not do anything if attrs are not defined', () => {
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual({});
});

it('should set attrs if they were not set initially', () => {
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual({});

const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;

fixture.detectChanges();

expect(injectedElem.attributes).toMatchObject(attrs);
});

it('should replace attrs if new object set', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual(attrs);

const attrs2 = {
val3: 'new',
};
fixture.componentInstance.attrs = attrs2;

fixture.detectChanges();

expect(injectedElem.attributes).toMatchObject(attrs2);
});

it('should unset attrs if set to null/undefined', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual(attrs);

fixture.componentInstance.attrs = null;

fixture.detectChanges();

// Angular renderer sets removed attrs to null
Object.keys(attrs).forEach(k => (attrs[k] = null));
expect(injectedElem.attributes).toEqual(attrs);
});

it('should add new attr if added to object', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
} as any;
fixture.componentInstance.attrs = attrs;
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual(attrs);

attrs.val3 = 'new';
fixture.detectChanges();

expect(injectedElem.attributes).toEqual(attrs);
});

it('should remove attr if removed from object', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual(attrs);

delete attrs.attrTwo;
fixture.detectChanges();

// Angular renderer sets removed attrs to null
attrs.attrTwo = null;
expect(injectedElem.attributes).toEqual(attrs);
});

it('should update attr if updated in object', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;
fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toEqual(attrs);

attrs.attrTwo = 'new';
fixture.detectChanges();

expect(injectedElem.attributes).toEqual(attrs);
});
});

describe('with `ndc-dynamic`', () => {
beforeAll(() =>
(hostTemplate = `<ndc-dynamic [ndcDynamicComponent]="comp" [ndcDynamicAttributes]="attrs"></ndc-dynamic>`));

it('should set attributes on injected component', () => {
const attrs = {
'attr-one': 'val-1',
attrTwo: 'val-two',
};
fixture.componentInstance.attrs = attrs;

fixture.detectChanges();

const injectedElem = getInjectedComponentFrom(fixture).componentElem;

expect(injectedElem.attributes).toMatchObject(attrs);
});
});
});
71 changes: 71 additions & 0 deletions src/dynamic/dynamic-attributes.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
Directive,
DoCheck,
Host,
Inject,
Injector,
Input,
KeyValueChanges,
KeyValueDiffers,
Optional,
Renderer2,
} from '@angular/core';

import { COMPONENT_INJECTOR, ComponentInjector } from './component-injector';
import { ComponentOutletInjectorDirective } from './component-outlet-injector.directive';

export interface AttributesMap {
[key: string]: string;
}

@Directive({
selector: '[ndcDynamicAttributes]',
exportAs: 'ndcDynamicAttributes',
})
export class DynamicAttributesDirective implements DoCheck {
@Input() ndcDynamicAttributes: AttributesMap;

private _attrsDiffer = this.differs.find({}).create<string, string>();
private _componentInjector: ComponentInjector = this.injector.get(this.componentInjectorType, {});

private get _compInjector() {
return this.componentOutletInjector || this._componentInjector;
}

private get _nativeElement() {
return this._compInjector.componentRef.location.nativeElement;
}

constructor(
private renderer: Renderer2,
private differs: KeyValueDiffers,
private injector: Injector,
@Inject(COMPONENT_INJECTOR)
private componentInjectorType: ComponentInjector,
@Optional()
@Host()
private componentOutletInjector: ComponentOutletInjectorDirective,
) {}

ngDoCheck(): void {
const changes = this._attrsDiffer.diff(this.ndcDynamicAttributes);

if (changes) {
this._updateAttributes(changes);
}
}

setAttribute(name: string, value: string, namespace?: string) {
this.renderer.setAttribute(this._nativeElement, name, value, namespace);
}

removeAttribute(name: string, namespace?: string) {
this.renderer.removeAttribute(this._nativeElement, name, namespace);
}

private _updateAttributes(changes: KeyValueChanges<string, string>) {
changes.forEachAddedItem(r => this.setAttribute(r.key, r.currentValue));
changes.forEachChangedItem(r => this.setAttribute(r.key, r.currentValue));
changes.forEachRemovedItem(r => this.removeAttribute(r.key));
}
}
3 changes: 3 additions & 0 deletions src/dynamic/dynamic.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {

import { COMPONENT_INJECTOR, ComponentInjector } from './component-injector';
import { ComponentOutletInjectorDirective } from './component-outlet-injector.directive';
import { DynamicAttributesDirective } from './dynamic-attributes.directive';
import { DynamicComponent } from './dynamic.component';
import { DynamicDirective } from './dynamic.directive';

Expand All @@ -17,11 +18,13 @@ import { DynamicDirective } from './dynamic.directive';
DynamicComponent,
DynamicDirective,
ComponentOutletInjectorDirective,
DynamicAttributesDirective,
],
exports: [
DynamicComponent,
DynamicDirective,
ComponentOutletInjectorDirective,
DynamicAttributesDirective,
],
})
export class DynamicModule {
Expand Down
1 change: 1 addition & 0 deletions src/dynamic/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './dynamic.module';
export * from './dynamic.directive';
export * from './dynamic.component';
export * from './dynamic-attributes.directive';
export { ComponentInjector } from './component-injector';

0 comments on commit 71f10ad

Please sign in to comment.