Skip to content

Commit

Permalink
feat(ng-core): DirectiveSuperclass.getInput$() wait until the input…
Browse files Browse the repository at this point in the history
… is set before emitting. Before, if called e.g. from the directive's constructor, it would emit `undefined` immediately, then emit again during `ngOnChanges`. **Caveat:** all emissions are now delayed on the microtask queue.

Closes #14.
  • Loading branch information
ersimont committed Dec 22, 2020
1 parent 818f1c7 commit 3b13611
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 6 deletions.
1 change: 1 addition & 0 deletions TODO.md
@@ -1,3 +1,4 @@
- add support/help for `--tag next` to `publish-libs.ts`
- landing page to link to all API docs
- coveralls
- help may be here, to combine multiple coverage runs into one report: https://github.com/angular/angular-cli/issues/11268
Expand Down
118 changes: 112 additions & 6 deletions projects/ng-core/src/lib/directive-superclass.spec.ts
@@ -1,11 +1,14 @@
import {
ChangeDetectionStrategy,
Component,
Directive,
Inject,
Injector,
Input,
OnChanges,
Pipe,
PipeTransform,
SimpleChanges,
} from '@angular/core';
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
Expand All @@ -15,7 +18,7 @@ import {
expectSingleCallAndReset,
} from '@s-libs/ng-dev';
import { BehaviorSubject, combineLatest, noop, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, take } from 'rxjs/operators';
import { click, find, findButton } from '../test-helpers';
import { DirectiveSuperclass } from './directive-superclass';

Expand Down Expand Up @@ -53,7 +56,6 @@ class TestComponent {
class ColorTextComponent extends DirectiveSuperclass {
@Input() prefix?: string;
@Input() prefix2?: string;
@Input() unspecified?: string;
color!: string;

constructor(
Expand Down Expand Up @@ -157,6 +159,7 @@ describe('DirectiveSuperclass', () => {
ctx.run(() => {
const stub = jasmine.createSpy();
colorTextComponent().getInput$('prefix2').subscribe(stub);
ctx.tick();
expect(stub).toHaveBeenCalledTimes(1);
expect(stub.calls.argsFor(0)).toEqual([undefined]);

Expand All @@ -175,10 +178,113 @@ describe('DirectiveSuperclass', () => {

// https://github.com/simontonsoftware/s-ng-utils/issues/10
it('emits `undefined` for unspecified inputs', () => {
ctx.run(() => {
const stub = jasmine.createSpy();
colorTextComponent().getInput$('unspecified').subscribe(stub);
expectSingleCallAndReset(stub, undefined);
@Directive({ selector: '[sTest]' })
class TestDirective extends DirectiveSuperclass {
@Input() myInput?: string;
emittedValue? = 'initial value';

constructor(injector: Injector) {
super(injector);
this.getInput$('myInput').subscribe((value) => {
this.emittedValue = value;
});
}
}

@Component({ template: '<div sTest></div>' })
class WrapperComponent {}

class TestContext2 extends ComponentContext<WrapperComponent> {
componentType = WrapperComponent;

constructor() {
super({ declarations: [TestDirective, WrapperComponent] });
}
}

const ctx2 = new TestContext2();
ctx2.run(() => {
const testDirective = ctx2.fixture.debugElement
.query(By.directive(TestDirective))
.injector.get(TestDirective);
expect(testDirective.emittedValue).toBe(undefined);
});
});

// https://github.com/simontonsoftware/s-libs/issues/14
it('emits immediately (only) if `ngOnChanges()` is called', () => {
@Directive({ selector: '[sTest]' })
class TestDirective extends DirectiveSuperclass implements OnChanges {
@Input() myInput?: string;
stage = 'before ngOnChanges';
emittedDuring?: string;

constructor(injector: Injector) {
super(injector);
this.getInput$('myInput')
.pipe(take(1))
.subscribe(() => {
this.emittedDuring = this.stage;
});
}

ngOnChanges(changes: SimpleChanges): void {
this.stage = 'after ngOnChanges';
super.ngOnChanges(changes);
}
}

@Component({ template: '<div sTest myInput="value"></div>' })
class WrapperComponent {}

class TestContext2 extends ComponentContext<WrapperComponent> {
componentType = WrapperComponent;

constructor() {
super({ declarations: [TestDirective, WrapperComponent] });
}
}

const ctx2 = new TestContext2();
ctx2.run(() => {
const testDirective = ctx2.fixture.debugElement
.query(By.directive(TestDirective))
.injector.get(TestDirective);
expect(testDirective.emittedDuring).toBe('after ngOnChanges');
});
});

it('emits even if no inputs are provided to the component', () => {
@Directive({ selector: '[sTest]' })
class TestDirective extends DirectiveSuperclass implements OnChanges {
@Input() myInput?: string;
emitted = false;

constructor(injector: Injector) {
super(injector);
this.getInput$('myInput').subscribe(() => {
this.emitted = true;
});
}
}

@Component({ template: '<div sTest></div>' })
class WrapperComponent {}

class TestContext2 extends ComponentContext<WrapperComponent> {
componentType = WrapperComponent;

constructor() {
super({ declarations: [TestDirective, WrapperComponent] });
}
}

const ctx2 = new TestContext2();
ctx2.run(() => {
const testDirective = ctx2.fixture.debugElement
.query(By.directive(TestDirective))
.injector.get(TestDirective);
expect(testDirective.emitted).toBe(true);
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions projects/ng-core/src/lib/directive-superclass.ts
Expand Up @@ -5,6 +5,7 @@ import {
OnChanges,
SimpleChanges,
} from '@angular/core';
import { delayOnMicrotaskQueue } from '@s-libs/rxjs-core';
import { Observable, Subject } from 'rxjs';
import { filter, map, startWith } from 'rxjs/operators';
import { InjectableSuperclass } from './injectable-superclass';
Expand Down Expand Up @@ -77,6 +78,7 @@ export abstract class DirectiveSuperclass
return this.inputChanges$.pipe(
filter((keys) => keys.has(key)),
startWith(undefined),
delayOnMicrotaskQueue(),
map(() => this[key]),
);
}
Expand Down

0 comments on commit 3b13611

Please sign in to comment.