Using Angular 4 with TypeScript, RxJS and the Reactive Forms module.
Please consider the following code:
import {ChangeDetectionStrategy, Component, EventEmitter, Input, NgModule, Output, SimpleChanges, VERSION} from '@angular/core'
import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser'
import {BehaviorSubject,Subscription} from 'rxjs/Rx';
interface Question {
id: string;
label: string;
}
interface State {
question: Question;
}
@Component({
selector: 'my-app',
template: `
<div>
<h2>App</h2>
<edit-question [question]="question$ | async" (questionChange)="questionChanged($event)"></edit-question>
<edit-question [question]="question$ | async" (questionChange)="questionChanged($event)"></edit-question>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {
state$ = new BehaviorSubject<State>({
question: {id: 'q1', label: 'Question1'}
});
question$ = this.state$.map(s => s.question);
questionChanged(q: Question): void {
console.log('App.questionChanged', q);
this.state$.next({question: q});
}
}
@Component({
selector: 'edit-question',
template: `
<div [formGroup]="form">
<h3>Question</h3>
<div>ID: {{question.id}}</div>
<div>Label: <input type="text" formControlName="label"></div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EditQuestionComponent implements OnChanges, OnDestroy {
@Input() question: Question;
@Output() questionChange = new EventEmitter<Question>();
form: FormGroup;
formChangesSubscription: Subscription;
constructor(fb: FormBuilder) {
this.form = fb.group({id: '', label: ''});
this.formChangesSubscription = this.form.valueChanges.subscribe(value => {
console.log('EditQuestionComponent.valueChange', value);
let updated = {id: value.id, label: value.label};
console.log('EditQuestionComponent.questionChange.emit', updated);
this.questionChange.emit(updated);
});
}
ngOnChanges(changes: SimpleChanges): void {
console.log('EditQuestionComponent.ngOnChanges', changes);
this.form.reset({id: this.question.id, label: this.question.label}, {emitEvent: false});
}
ngOnDestroy(): void {
this.formChangesSubscription.unsubscribe();
}
}
@NgModule({
imports: [ BrowserModule, ReactiveFormsModule ],
declarations: [ App, EditQuestionComponent ],
bootstrap: [ App ]
})
export class AppModule {}
Also available as a Plunker here.
Here we have:
- A parent component and two instances of the same child component (on purpose)
- All components are using OnPush change detection strategy
- Parent maintains state using a BehaviorSubject
- Parent sends state down to child component using an async pipe
- Parent registers to state changes from child component and produces a new state based on given change
When running the Plunker, focusing at the begining of the 1st text input (before Question1) and typing a letter, the caret jumps to the end of the input.
From what I understand, this is to be expected:
- Form control value changes, so form value changes
- Form valueChanges subscription kicks in and emits a new question
- Parent event handler receives the new question and produces a new state
- New state value is produced, then mapped to a new question value, and new question value is pushed down to child component
- Child component detects new question value through ngOnChanges and patches form value, thereby pushing to the end of the input
So while this makes sense, I'm wondering how to avoid this. The child component could set a ignoreNextChange
flag before emitting the change and test + reset it in ngOnChanges
to make sure It ignore changes it itself initiated, but this feels like a kludge.
Is there a better way to do this?