Skip to content

Commit

Permalink
feat(ng-core): add `WrappedFormControlSuperclass.setUpInnerToOuter$()…
Browse files Browse the repository at this point in the history
…` and `.setUpOuterToInner$`

Closes #37
  • Loading branch information
ersimont committed Apr 21, 2021
1 parent ad1a474 commit 6c90588
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 16 deletions.
71 changes: 70 additions & 1 deletion projects/ng-core/src/lib/wrapped-form-control-superclass.spec.ts
@@ -1,11 +1,18 @@
import { ChangeDetectionStrategy, Component, Injector } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
Injector,
Input,
} from '@angular/core';
import {
ComponentFixtureAutoDetect,
flushMicrotasks,
} from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { ComponentContext, ComponentContextNext } from '@s-libs/ng-dev';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { click, find, findButton, setValue } from '../test-helpers';
import { DirectiveSuperclass } from './directive-superclass';
import {
Expand Down Expand Up @@ -127,6 +134,68 @@ describe('WrappedFormControlSuperclass', () => {
});
});

it('allows setting up an observable to translate between inner and outer values', () => {
@Component({
selector: 's-observable-translation',
template: `<input [formControl]="formControl" />`,
providers: [provideValueAccessor(ObservableTranslationComponent)],
})
class ObservableTranslationComponent extends WrappedFormControlSuperclass<
number,
string
> {
constructor(injector: Injector) {
super(injector);
}

protected setUpOuterToInner$(
value$: Observable<number>,
): Observable<string> {
return value$.pipe(map((outer) => String(outer / 2)));
}

protected setUpInnerToOuter$(
value$: Observable<string>,
): Observable<number> {
return value$.pipe(
map((inner) => +inner * 2),
filter((val) => !isNaN(val)),
);
}
}

@Component({
template: `
<s-observable-translation
[(ngModel)]="outerValue"
></s-observable-translation>
`,
})
class WrapperComponent {
@Input() outerValue!: number;
}

const ctx = new ComponentContextNext(WrapperComponent, {
imports: [FormsModule, ReactiveFormsModule],
declarations: [ObservableTranslationComponent],
});
ctx.assignInputs({ outerValue: 38 });
ctx.run(() => {
const input: HTMLInputElement = ctx.fixture.debugElement.query(
By.css('input'),
).nativeElement;
expect(input.value).toBe('19');

setValue(input, '6');
ctx.tick();
expect(ctx.getComponentInstance().outerValue).toBe(12);

setValue(input, "you can't double me");
ctx.tick();
expect(ctx.getComponentInstance().outerValue).toBe(12);
});
});

it('provides help for `onTouched`', () => {
masterCtx.run(() => {
expect(masterCtx.fixture.nativeElement.innerText).not.toContain(
Expand Down
90 changes: 75 additions & 15 deletions projects/ng-core/src/lib/wrapped-form-control-superclass.ts
@@ -1,6 +1,8 @@
import { Injector } from '@angular/core';
import { FormControl } from '@angular/forms';
import { wrapMethod } from '@s-libs/js-core';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { FormControlSuperclass } from './form-control-superclass';

/**
Expand Down Expand Up @@ -32,15 +34,15 @@ import { FormControlSuperclass } from './form-control-superclass';
* super(injector);
* }
*
* protected innerToOuter(value: string): Date {
* return new Date(value + "Z");
* protected innerToOuter(inner: string): Date {
* return new Date(inner + "Z");
* }
*
* protected outerToInner(value: Date): string {
* if (value === null) {
* protected outerToInner(outer: Date): string {
* if (outer === null) {
* return ""; // happens during initialization
* }
* return value.toISOString().substr(0, 16);
* return outer.toISOString().substr(0, 16);
* }
* }
* ```
Expand All @@ -52,11 +54,19 @@ export abstract class WrappedFormControlSuperclass<
/** Bind this to your inner form control to make all the magic happen. */
formControl = new FormControl();

private incomingValues$ = new Subject<OuterType>();

constructor(injector: Injector) {
super(injector);
this.subscribeTo(this.formControl.valueChanges, (value) => {
this.emitOutgoingValue(this.innerToOuter(value));
this.subscribeTo(this.setUpOuterToInner$(this.incomingValues$), (inner) => {
this.formControl.setValue(inner, { emitEvent: false });
});
this.subscribeTo(
this.setUpInnerToOuter$(this.formControl.valueChanges),
(outer) => {
this.emitOutgoingValue(outer);
},
);
wrapMethod(this.formControl, 'markAsTouched', {
after: () => {
this.onTouched();
Expand All @@ -65,8 +75,8 @@ export abstract class WrappedFormControlSuperclass<
}

/** Called as angular propagates values changes to this `ControlValueAccessor`. You normally do not need to use it. */
handleIncomingValue(value: OuterType): void {
this.formControl.setValue(this.outerToInner(value), { emitEvent: false });
handleIncomingValue(outer: OuterType): void {
this.incomingValues$.next(outer);
}

/** Called as angular propagates disabled changes to this `ControlValueAccessor`. You normally do not need to use it. */
Expand All @@ -79,13 +89,63 @@ export abstract class WrappedFormControlSuperclass<
super.setDisabledState(this.isDisabled);
}

/** Override this to modify a value coming from the outside to the format needed within this component. */
protected outerToInner(value: OuterType): InnerType {
return (value as any) as InnerType;
/**
* Override this to modify a value coming from the outside to the format needed within this component.
*
* For more complex needs, see {@link #setUpOuterToInner$} instead.
*/
protected outerToInner(outer: OuterType): InnerType {
return (outer as unknown) as InnerType;
}

/**
* Override this to for full control over the stream of values passed in to your subclass.
*
* In this example, incoming values are debounced before being passed through to the inner form control
* ```ts
* setUpOuterToInner$(outer$: Observable<OuterType>): Observable<InnerType> {
* return values$.pipe(
* debounce(300),
* map((outer) => doExpensiveTransformToInnerValue(outer)),
* );
* }
* ```
*
* For a simple transformation, see {@link #outerToInner} instead.
*/
protected setUpOuterToInner$(
outer$: Observable<OuterType>,
): Observable<InnerType> {
return outer$.pipe(map((outer) => this.outerToInner(outer)));
}

/**
* Override this to modify a value coming from within this component to the format expected on the outside.
*
* For more complex needs, see {@link #setUpInnerToOuter$} instead.
*/
protected innerToOuter(inner: InnerType): OuterType {
return (inner as unknown) as OuterType;
}

/** Override this to modify a value coming from within this component to the format expected on the outside. */
protected innerToOuter(value: InnerType): OuterType {
return (value as any) as OuterType;
/**
* Override this to for full control over the stream of values emitted from your subclass.
*
* In this example, illegal values are not emitted
* ```ts
* setUpInnerToOuter$(inner$: Observable<InnerType>): Observable<OuterType> {
* return values$.pipe(
* debounce(300),
* filter((inner) => isLegalValue(outer)),
* );
* }
* ```
*
* For a simple transformation, see {@link #innerToOuter} instead.
*/
protected setUpInnerToOuter$(
inner$: Observable<InnerType>,
): Observable<OuterType> {
return inner$.pipe(map((inner) => this.innerToOuter(inner)));
}
}

0 comments on commit 6c90588

Please sign in to comment.