Why template local variables are not usable in templates when using *ngIf?

前端 未结 1 1903
轻奢々
轻奢々 2020-12-04 10:42

Part 1: \"#test\" is undefined when using *ngIf

When referencing an input that can be hidden/\"destroyed\" (because the *ngIf is used and some of the

相关标签:
1条回答
  • 2020-12-04 11:15

    As for a solution to the focus problem, you could create an attribute directive, focusMe:

    import {Component, Directive, ElementRef} from 'angular2/core';
    @Directive({
      selector: '[focusMe]'
    })
    export class FocusDirective {
      constructor(private el: ElementRef) {}
      ngAfterViewInit() {
        this.el.nativeElement.focus();
      }
    }
    @Component({
        selector: 'my-app',
        directives: [FocusDirective],
        template: `<h1>My First Angular 2 App</h1>
          <button (click)="toggle()">toggle</button>
          <input focusMe *ngIf="isVisible">
        `
    })
    export class AppComponent { 
      constructor() { console.clear(); }
      private isVisible = false;
      toggle() {
        this.isVisible = !this.isVisible;
      }
    }
    

    Plunker

    Update 1: Adding the solution for the re-focus feature:

    import {Component, Directive, ElementRef, Input} from 'angular2/core';
    
    @Directive({
      selector: '[focusMe]'
    })
    export class FocusMe {
        @Input('focusMe') hasFocus: boolean;
        constructor(private elementRef: ElementRef) {}
        ngAfterViewInit() {
          this.elementRef.nativeElement.focus();
        }
        ngOnChanges(changes) {
          //console.log(changes);
          if(changes.hasFocus && changes.hasFocus.currentValue === true) {
            this.elementRef.nativeElement.focus();
          }
        }
    }
    @Component({
        selector: 'my-app',
        template: `<h1>My First Angular 2 App</h1>
        <button (click)="showInput()">Make it visible</button>
        <input *ngIf="inputIsVisible" [focusMe]="inputHasFocus">
        <button (click)="focusInput()" *ngIf="inputIsVisible">Focus it</button>
        `,
        directives:[FocusMe]
    })
    export class AppComponent {
      private inputIsVisible = false;
      private inputHasFocus = false;
      constructor() { console.clear(); }
      showInput() {
        this.inputIsVisible = true;
      }
      focusInput() {
        this.inputHasFocus = true;
        setTimeout(() => this.inputHasFocus = false, 50);
      }
    }
    

    Plunker

    An alternative to using setTimeout() to reset the focus property to false would be to create an event/output property on the FocusDirective, and emit() an event when focus() is called. The AppComponent would then listen for that event and reset the focus property.

    Update 2: Here's an alternative/better way to add the re-focus feature, using ViewChild. We don't need to track the focus state this way, nor do we need an input property on the FocusMe directive.

    import {Component, Directive, ElementRef, Input, ViewChild} from 'angular2/core';
    
    @Directive({
      selector: '[focusMe]'
    })
    export class FocusMe {
        constructor(private elementRef: ElementRef) {}
        ngAfterViewInit() {
          // set focus when element first appears
          this.setFocus();
        }
        setFocus() {
          this.elementRef.nativeElement.focus();
        }
    }
    @Component({
        selector: 'my-app',
        template: `<h1>My First Angular 2 App</h1>
        <button (click)="showInput()">Make it visible</button>
        <input *ngIf="inputIsVisible" focusMe>
        <button (click)="focusInput()" *ngIf="inputIsVisible">Focus it</button>
        `,
        directives:[FocusMe]
    })
    export class AppComponent {
      @ViewChild(FocusMe) child;
      private inputIsVisible = false;
      constructor() { console.clear(); }
      showInput() {
        this.inputIsVisible = true;
      }
      focusInput() {
        this.child.setFocus();
      }
    }
    

    Plunker

    Update 3: Here's yet another alternative that does not require a directive, which still uses ViewChild, but we access the child via a local template variable rather than an attribute directive (thanks @alexpods for the tip):

    import {Component, ViewChild, NgZone} from 'angular2/core';
    
    @Component({
        selector: 'my-app',
        template: `<h1>Focus test</h1>
        <button (click)="showInput()">Make it visible</button>
        <input #input1 *ngIf="input1IsVisible">
        <button (click)="focusInput1()" *ngIf="input1IsVisible">Focus it</button>
        `,
    })
    export class AppComponent {
      @ViewChild('input1') input1ElementRef;
      private input1IsVisible = false;
      constructor(private _ngZone: NgZone) { console.clear(); }
      showInput() {
        this.input1IsVisible = true;
        // Give ngIf a chance to render the <input>.
        // Then set the focus, but do this outside the Angualar zone to be efficient.
        // There is no need to run change detection after setTimeout() runs,
        // since we're only focusing an element.
        this._ngZone.runOutsideAngular(() => { 
          setTimeout(() => this.focusInput1(), 0);
       });
      }
      setFocus(elementRef) {
        elementRef.nativeElement.focus();
      }
      ngDoCheck() {
        // if you remove the ngZone stuff above, you'll see
        // this log 3 times instead of 1 when you click the
        // "Make it visible" button.
        console.log('doCheck');
      }
      focusInput1() {
        this.setFocus(this.input1ElementRef);
      }
    }
    

    Plunker

    Update 4: I updated the code in Update 3 to use NgZone so that we don't cause Angular's change detection algorithm to run after the setTimeout() finishes. (For more on change detection, see this answer).

    Update 5: I updated the code in the above plunker to use Renderer to make it web worker safe. Accessing focus() directly on nativeElement is discouraged.

    focusInput1() {
      this._renderer.invokeElementMethod(
        this.input1ElementRef.nativeElement, 'focus', []);
    }
    

    I learned a lot from this question.

    0 讨论(0)
提交回复
热议问题