Skip to content

Commit b60fe40

Browse files
committed
fix: changed form control state directive to run its initialization code inside the ngAfterViewInit hook instead of ngOnInit to allow proper interaction with form elements that can have dynamically rendered children they depend on (e.g. dynamic options for selects)
1 parent d10c6dd commit b60fe40

File tree

4 files changed

+76
-5
lines changed

4 files changed

+76
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
#### Bugfixes
77

8-
* fix bug that caused control state value to not be properly set to form element if the ID of the control state changed but the state's value was the same as the last value the view reported for the previous state
8+
* fix issue that caused control state value to not be properly set to form element if the ID of the control state changed but the state's value was the same as the last value the view reported for the previous state
9+
* changed form control state directive to run its initialization code inside the `ngAfterViewInit` hook instead of `ngOnInit` to allow proper interaction with form elements that can have dynamically rendered children they depend on (e.g. dynamic `option`s for `select`s)
910

1011
<a name="1.0.0"></a>
1112
### 1.0.0

src/control/directive.comp.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { Component, Input } from '@angular/core';
3+
import { ActionsSubject, Action } from '@ngrx/store';
4+
import { Observable } from 'rxjs/Observable';
5+
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
6+
import 'rxjs/add/operator/first';
7+
import 'rxjs/add/operator/skip';
8+
9+
import { FormControlState, createFormControlState } from '../state';
10+
import { SetValueAction } from '../actions';
11+
import { NgrxFormsModule } from '../module';
12+
13+
const SELECT_OPTIONS = ['op1', 'op2'];
14+
15+
@Component({
16+
// tslint:disable-next-line:component-selector
17+
selector: 'select-test',
18+
template: '<select [ngrxFormControlState]="state"><option *ngFor="let o of options" [value]="o">{{o}}</option></select>',
19+
})
20+
export class SelectComponent {
21+
@Input() state: FormControlState<string>;
22+
options = SELECT_OPTIONS;
23+
}
24+
25+
describe(SelectComponent.name, () => {
26+
let component: SelectComponent;
27+
let fixture: ComponentFixture<SelectComponent>;
28+
let actionsSubject: ActionsSubject;
29+
let actions$: Observable<Action>;
30+
let element: HTMLSelectElement;
31+
const FORM_CONTROL_ID = 'test ID';
32+
const INITIAL_FORM_CONTROL_VALUE = SELECT_OPTIONS[1];
33+
const INITIAL_STATE = createFormControlState<string>(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE);
34+
35+
beforeEach(() => {
36+
actionsSubject = new BehaviorSubject<Action>({ type: '' }) as ActionsSubject;
37+
actions$ = actionsSubject as Observable<Action>; // cast required due to mismatch of lift() function signature
38+
});
39+
40+
beforeEach(async(() => {
41+
TestBed.configureTestingModule({
42+
imports: [NgrxFormsModule],
43+
declarations: [SelectComponent],
44+
providers: [{ provide: ActionsSubject, useValue: actionsSubject }],
45+
}).compileComponents();
46+
}));
47+
48+
beforeEach(() => {
49+
fixture = TestBed.createComponent(SelectComponent);
50+
component = fixture.componentInstance;
51+
component.state = INITIAL_STATE;
52+
element = (fixture.nativeElement as HTMLElement).querySelector('select') as HTMLSelectElement;
53+
fixture.detectChanges();
54+
});
55+
56+
it('should select the correct option initially', () => {
57+
expect(element.value).toBe(INITIAL_FORM_CONTROL_VALUE);
58+
});
59+
60+
it('should trigger a SetValueAction with the selected value when an option is selected', done => {
61+
const newValue = SELECT_OPTIONS[0];
62+
element.value = newValue;
63+
element.dispatchEvent(new Event('change'));
64+
actions$.first().subscribe(a => {
65+
expect(a.type).toBe(SetValueAction.TYPE);
66+
expect((a as SetValueAction<string>).payload.value).toBe(newValue);
67+
done();
68+
});
69+
}, 200);
70+
});

src/control/directive.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe(NgrxFormControlDirective.name, () => {
3636
};
3737
directive = new NgrxFormControlDirective<string>(elementRef, document, actionsSubject, [valueAccessor]);
3838
directive.ngrxFormControlState = INITIAL_STATE;
39-
directive.ngOnInit();
39+
directive.ngAfterViewInit();
4040
});
4141

4242
afterEach(() => {

src/control/directive.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Inject,
66
HostListener,
77
HostBinding,
8-
OnInit,
8+
AfterViewInit,
99
OnDestroy,
1010
Self,
1111
} from '@angular/core';
@@ -32,7 +32,7 @@ import { selectValueAccessor } from '../value-accessors';
3232
@Directive({
3333
selector: '[ngrxFormControlState]',
3434
})
35-
export class NgrxFormControlDirective<TValue extends FormControlValueTypes> implements OnInit, OnDestroy {
35+
export class NgrxFormControlDirective<TValue extends FormControlValueTypes> implements AfterViewInit, OnDestroy {
3636
@Input() set ngrxFormControlState(newState: FormControlState<TValue>) {
3737
if (!newState) {
3838
throw new Error('The control state must not be undefined!');
@@ -87,7 +87,7 @@ export class NgrxFormControlDirective<TValue extends FormControlValueTypes> impl
8787
@Input() convertViewValue: (value: any) => TValue = value => value;
8888
@Input() convertModelValue: (value: TValue) => any = value => value;
8989

90-
ngOnInit() {
90+
ngAfterViewInit() {
9191
if (!this.state) {
9292
throw new Error('The form state must not be undefined!');
9393
}

0 commit comments

Comments
 (0)