11

In my Angular 2 application, I want to have a list of inputs. Pressing Enter inside one of them would add a new input and immediately focus on it. This is a question that has already been asked on this site and Eric Martinez provided a neat answer to it that accomplishes that with a custom directive.

His solution is based on a dummy list of integers. I am having difficulties trying to adapt it to a more realistic scenario. I have forked Eric's plunk, so you can run the code here, but the most important file is this one:

//our root app component
import {Component, Directive, Renderer, ElementRef} from 'angular2/core'

class Person { name: string }

@Directive({
  selector : 'input'
})
class MyInput {
  constructor(public renderer: Renderer, public elementRef: ElementRef) {}

  // It won't work at construction time
  ngOnInit() {
    this.renderer.invokeElementMethod(
      this.elementRef.nativeElement, 'focus', []);
  }
}

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div *ngFor="#input of inputs">
      <input
        (keydown.enter)="add()" 
        [(ngModel)]="input.name"
        type="text"/>
    </div>
  `,
  directives: [MyInput]
})
export class App {
  inputs: Person[] = [{name: 'Alice'}];

  add() {
    var newPerson = new Person();
    newPerson.name = 'Bob';

    this.inputs.push(newPerson);
  }
}

My array of inputs is now a list of Person objects. The inputs are bound bidirectionally to the name property of a Person. The <input> is now wrapped inside a <div>, as I expect that later I will write more markup to display each Person.

After making these changes, the example works only at the first attempt of pressing Enter - a new input with text Bob appears as expected. But when I then try to press Enter for the second time, I get an error:

angular2.dev.js:23730 Error: Expression 'ngClassUntouched in App@2:6' has changed after it was checked. Previous value: 'true'. Current value: 'false'
    at ExpressionChangedAfterItHasBeenCheckedException.BaseException [as constructor] (angular2.dev.js:7587)
    at new ExpressionChangedAfterItHasBeenCheckedException (angular2.dev.js:4992)
    at ChangeDetector_App_1.AbstractChangeDetector.throwOnChangeError (angular2.dev.js:9989)
    at ChangeDetector_App_1.detectChangesInRecordsInternal (viewFactory_App:143)
    at ChangeDetector_App_1.AbstractChangeDetector.detectChangesInRecords (angular2.dev.js:9874)
    at ChangeDetector_App_1.AbstractChangeDetector.runDetectChanges (angular2.dev.js:9857)
    at ChangeDetector_App_0.AbstractChangeDetector._detectChangesContentChildren (angular2.dev.js:9930)
    at ChangeDetector_App_0.AbstractChangeDetector.runDetectChanges (angular2.dev.js:9858)
    at ChangeDetector_HostApp_0.AbstractChangeDetector._detectChangesInViewChildren (angular2.dev.js:9936)
    at ChangeDetector_HostApp_0.AbstractChangeDetector.runDetectChanges (angular2.dev.js:9861)

How can I fix that?

I am running the example in Chrome. I found it easiest to demostrate the problem using Eric Martinez's plunks that are based on beta 12 version of Angular2 but my real world application where I get the same error is currently using Angular 2.0.0.

Community
  • 1
  • 1
kamilk
  • 3,829
  • 1
  • 27
  • 40

1 Answers1

6

Angular2 doesn't like when the model is changed during a change detection callback (like ngOnInit() is). Calling ChangeDetectorRef.detectChanges() should fix it:

class MyInput {
  constructor(public renderer: Renderer, public elementRef: ElementRef
      ,private cdRef:ChangeDetectorRef) {}

  // It won't work at construction time
  ngOnInit() {
    this.renderer.invokeElementMethod(
      this.elementRef.nativeElement, 'focus', []);
    this.cdRef.detectChanges();
  }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Doing this in Plunker causes chrome to crash with an out of memory error, because of some reason. In my real application, this prevents any new `` from appearing, even though the underlying array is still appended to. I also don't really understand in what way I'm changing the model. – kamilk Nov 12 '16 at 13:39
  • 1
    You shouldn't try to focus in `ngOnInit()` anyway because here the element doesn't yet exist. Use `ngAfterViewInit()` or `ngAfterViewChecked()`. – Günter Zöchbauer Nov 12 '16 at 13:42
  • 1
    I tried both events, I tried combining them with `.detectChanges()`, still the same issue... https://plnkr.co/edit/Hh3H36fceFSm0Q20PVV3?p=preview – kamilk Nov 12 '16 at 13:54
  • What are you trying to accomplish anyway? I can't make sense of calling `focus()` on elements created with `*ngFor` when they are created. – Günter Zöchbauer Nov 12 '16 at 13:57
  • 4
    I got it. It makes sense if only one element after the after is created. I wouldn't consider it a too good solution when the array already contains a list of values initially. I got it working with `setTimeout()`. Not sure why it doesn't work with `detectChanges()` but I guess it is because the error is not from `MyInput` but from `NgModel` which is not affected by `detectChanges()`. Another approach that might work if you don't like `setTimeout()` is to inject `ApplicationRef` (instead of `ChangeDetectorRef`) and call its `.tick()` (instead of `detectChanges()`) – Günter Zöchbauer Nov 12 '16 at 14:02
  • 1
    Thank you, I'll go with the `setTimeout()` solution :) What would we do without the good old `setTimeout`. – kamilk Nov 12 '16 at 14:10