-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
304 additions
and
13 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
203 changes: 203 additions & 0 deletions
203
projects/s-ng-utils/src/lib/directive-superclass.spec.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,203 @@ | ||
import { | ||
ChangeDetectionStrategy, | ||
Component, | ||
Inject, | ||
Injector, | ||
Input, | ||
} from "@angular/core"; | ||
import { | ||
ComponentFixture, | ||
ComponentFixtureAutoDetect, | ||
fakeAsync, | ||
flushMicrotasks, | ||
TestBed, | ||
} from "@angular/core/testing"; | ||
import { By } from "@angular/platform-browser"; | ||
import { BehaviorSubject, combineLatest, Observable } from "rxjs"; | ||
import { map } from "rxjs/operators"; | ||
import { click, find, findButton } from "../test-helpers"; | ||
import { DirectiveSuperclass } from "./directive-superclass"; | ||
|
||
describe("DirectiveSuperclass", () => { | ||
let fixture: ComponentFixture<TestComponent>; | ||
let el: HTMLElement; | ||
let color$: BehaviorSubject<string>; | ||
let colorTextComponent: ColorTextComponent; | ||
|
||
function init(initialAttrs?: Partial<TestComponent>) { | ||
color$ = new BehaviorSubject("Grey"); | ||
TestBed.configureTestingModule({ | ||
declarations: [ColorTextComponent, TestComponent], | ||
providers: [ | ||
{ provide: "color$", useValue: color$ }, | ||
{ provide: ComponentFixtureAutoDetect, useValue: true }, | ||
], | ||
}); | ||
fixture = TestBed.createComponent(TestComponent); | ||
Object.assign(fixture.componentInstance, initialAttrs); | ||
fixture.detectChanges(); | ||
flushMicrotasks(); | ||
el = fixture.nativeElement; | ||
colorTextComponent = fixture.debugElement.query( | ||
By.directive(ColorTextComponent), | ||
).componentInstance; | ||
} | ||
|
||
function darkButton() { | ||
return findButton(fixture, "Dark"); | ||
} | ||
|
||
function slateButton() { | ||
return findButton(fixture, "Slate"); | ||
} | ||
|
||
function bothButton() { | ||
return findButton(fixture, "Both"); | ||
} | ||
|
||
function hideButton() { | ||
return findButton(fixture, "Hide"); | ||
} | ||
|
||
function colorSpan() { | ||
return find<HTMLSpanElement>(fixture, "s-color-text span"); | ||
} | ||
|
||
///////// | ||
|
||
describe(".changedKeys$", () => { | ||
it("emits the keys that change", fakeAsync(() => { | ||
init(); | ||
const stub = jasmine.createSpy(); | ||
colorTextComponent.changedKeys$.subscribe(stub); | ||
|
||
click(darkButton()); | ||
expect(stub).toHaveBeenCalledTimes(1); | ||
expect(stub).toHaveBeenCalledWith(new Set(["prefix"])); | ||
|
||
click(slateButton()); | ||
expect(stub).toHaveBeenCalledTimes(2); | ||
expect(stub).toHaveBeenCalledWith(new Set(["prefix2"])); | ||
|
||
click(bothButton()); | ||
expect(stub).toHaveBeenCalledTimes(3); | ||
expect(stub).toHaveBeenCalledWith(new Set(["prefix", "prefix2"])); | ||
})); | ||
}); | ||
|
||
describe(".getInput$()", () => { | ||
it("emits the value of an input when it changes", fakeAsync(() => { | ||
init(); | ||
const stub = jasmine.createSpy(); | ||
colorTextComponent.getInput$("prefix2").subscribe(stub); | ||
|
||
click(darkButton()); | ||
expect(stub).not.toHaveBeenCalled(); | ||
|
||
click(slateButton()); | ||
expect(stub).toHaveBeenCalledTimes(1); | ||
expect(stub).toHaveBeenCalledWith("Slate"); | ||
|
||
click(bothButton()); | ||
expect(stub).toHaveBeenCalledTimes(2); | ||
expect(stub).toHaveBeenCalledWith(undefined); | ||
})); | ||
}); | ||
|
||
describe(".bindToInstance()", () => { | ||
it("sets the local value", fakeAsync(() => { | ||
init(); | ||
expect(colorSpan().innerText).toBe("Grey"); | ||
expect(colorSpan().style.backgroundColor).toBe("grey"); | ||
|
||
click(darkButton()); | ||
expect(colorSpan().innerText).toBe("DarkGrey"); | ||
expect(colorSpan().style.backgroundColor).toBe("darkgrey"); | ||
|
||
click(slateButton()); | ||
expect(colorSpan().innerText).toBe("DarkSlateGrey"); | ||
expect(colorSpan().style.backgroundColor).toBe("darkslategrey"); | ||
|
||
click(bothButton()); | ||
expect(colorSpan().innerText).toBe("Grey"); | ||
expect(colorSpan().style.backgroundColor).toBe("grey"); | ||
})); | ||
|
||
it("triggers change detection", fakeAsync(() => { | ||
init(); | ||
|
||
color$.next("Blue"); | ||
fixture.detectChanges(); | ||
expect(colorSpan().innerText).toBe("Blue"); | ||
expect(colorSpan().style.backgroundColor).toBe("blue"); | ||
|
||
click(bothButton()); | ||
expect(colorSpan().innerText).toBe("DarkSlateBlue"); | ||
expect(colorSpan().style.backgroundColor).toBe("darkslateblue"); | ||
})); | ||
}); | ||
|
||
describe(".subscribeTo()", () => { | ||
it("cleans up subscriptions", fakeAsync(() => { | ||
init(); | ||
expect(color$.observers.length).toBe(1); | ||
|
||
click(hideButton()); | ||
expect(color$.observers.length).toBe(0); | ||
})); | ||
}); | ||
}); | ||
|
||
@Component({ | ||
template: ` | ||
<button (click)="toggle('prefix', 'Dark')">Dark</button> | ||
<button (click)="toggle('prefix2', 'Slate')">Slate</button> | ||
<button (click)="toggle('prefix', 'Dark'); toggle('prefix2', 'Slate')"> | ||
Both | ||
</button> | ||
<button (click)="hide = !hide">Hide</button> | ||
<s-color-text | ||
*ngIf="!hide" | ||
[prefix]="prefix" | ||
[prefix2]="prefix2" | ||
></s-color-text> | ||
`, | ||
}) | ||
class TestComponent { | ||
color$ = new BehaviorSubject<string>("Green"); | ||
prefix?: string; | ||
prefix2?: string; | ||
hide = false; | ||
|
||
toggle(key: "prefix" | "prefix2", value: string) { | ||
this[key] = this[key] ? undefined : value; | ||
} | ||
} | ||
|
||
@Component({ | ||
selector: "s-color-text", | ||
template: ` | ||
<span [style.background]="color">{{ color }}</span> | ||
`, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
class ColorTextComponent extends DirectiveSuperclass { | ||
@Input() prefix?: string; | ||
@Input() prefix2?: string; | ||
color!: string; | ||
|
||
constructor( | ||
@Inject("color$") color$: Observable<string>, | ||
injector: Injector, | ||
) { | ||
super(injector); | ||
this.bindToInstance( | ||
"color", | ||
combineLatest( | ||
this.getInput$("prefix"), | ||
this.getInput$("prefix2"), | ||
color$, | ||
).pipe(map((parts) => parts.filter((p) => p).join(""))), | ||
); | ||
} | ||
} |
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,81 @@ | ||
import { | ||
OnChanges, | ||
SimpleChanges, | ||
Injector, | ||
ChangeDetectorRef, | ||
} from "@angular/core"; | ||
import { Observable, Subject } from "rxjs"; | ||
import { filter, map } from "rxjs/operators"; | ||
import { AutoDestroyable } from "./auto-destroyable"; | ||
|
||
/** | ||
* Extend this when creating a directive (including a component, which is a kind of directive) to gain access to the helpers demonstrated below. | ||
* | ||
* ```ts | ||
* @Component({ | ||
* selector: "s-color-text", | ||
* template: ` | ||
* <span [style.background]="color">{{ color }}</span> | ||
* `, | ||
* // note that `bindToInstance()` works even with OnPush change detection | ||
* changeDetection: ChangeDetectionStrategy.OnPush, | ||
* }) | ||
* class ColorTextComponent extends DirectiveSuperclass { | ||
* @Input() prefix?: string; | ||
* @Input() prefix2?: string; | ||
* color!: string; | ||
* | ||
* constructor( | ||
* @Inject("color$") color$: Observable<string>, | ||
* injector: Injector, | ||
* ) { | ||
* super(injector); | ||
* | ||
* // combine everything to calculate `color` and keep it up to date | ||
* this.bindToInstance( | ||
* "color", | ||
* combineLatest( | ||
* this.getInput$("prefix"), | ||
* this.getInput$("prefix2"), | ||
* color$, | ||
* ).pipe(map((parts) => parts.filter((p) => p).join(""))), | ||
* ); | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
export abstract class DirectiveSuperclass extends AutoDestroyable | ||
implements OnChanges { | ||
/** Emits the set of `@Input()` property names that change during each call to `ngOnChanges()`. */ | ||
changedKeys$ = new Subject<Set<keyof this>>(); | ||
|
||
protected changeDetectorRef: ChangeDetectorRef; | ||
|
||
constructor(injector: Injector) { | ||
super(); | ||
this.changeDetectorRef = injector.get(ChangeDetectorRef); | ||
} | ||
|
||
ngOnChanges(changes: SimpleChanges) { | ||
this.changedKeys$.next( | ||
new Set(Object.getOwnPropertyNames(changes) as Array<keyof this>), | ||
); | ||
} | ||
|
||
/** @return an observable of the values for one of this directive's `@Input()` properties */ | ||
getInput$<K extends keyof this>(key: K): Observable<this[K]> { | ||
return this.changedKeys$.pipe( | ||
filter((keys) => keys.has(key)), | ||
map(() => this[key]), | ||
); | ||
} | ||
|
||
/** | ||
* Binds an observable to one of this directive's instance variables. When the observable emits the instance variable will be updated, and change detection will be triggered to propagate any changes. Use this an an alternative to repeating `| async` multiple times in your template. */ | ||
bindToInstance<K extends keyof this>(key: K, value$: Observable<this[K]>) { | ||
this.subscribeTo(value$, (value) => { | ||
this[key] = value; | ||
this.changeDetectorRef.markForCheck(); | ||
}); | ||
} | ||
} |
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
Oops, something went wrong.