Search code examples
htmlangulartypescriptelementref

Angular cannot loop a list of ElementRef


I have a list of ElementRef for all my inputs but when I try to add listeners to them it makes like textInputs is empty but it's not.


@ViewChildren('text_input') textInputs!: QueryList<ElementRef>;

ngAfterViewInit(): void {
    this.initTextInputsListeners();
}

private initTextInputsListeners() {
    this.textInputs.forEach(input => {
      const inputElement = input.nativeElement;
      const parentNode = inputElement.parentNode;
      inputElement.addEventListener('focus', () => {
        parentNode.classList.add('user-input-active')
      });
      inputElement.addEventListener('focusout', () => {
        if (inputElement.value === '') parentNode.classList.remove('user-input-active');
      });
    });
  }
<div class="user-input send-message" [formGroup]="userInputForm" (ngSubmit)="onSendMessage()">
      <div class="user-input-box">
        <label for="message">Send message</label>
        <input id="message" class="form-control" type="text" name="message" #text_input formControlName="message" (keyup.enter)="onSendMessage()">
        <button class="submit-btn" type="submit" value="" (click)="onSendMessage()">
          <svg width="32" height="32" viewBox="0 0 24 24">
            <path
              d="m3.4 20.4l17.45-7.48a1 1 0 0 0 0-1.84L3.4 3.6a.993.993 0 0 0-1.39.91L2 9.12c0 .5.37.93.87.99L17 12L2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z"/>
          </svg>
        </button>
      </div>
    </div>

Solution

  • your code looks like work. So the problem is that when you execute the initTextInputsListeners textInputs have not all the inputs

    So To be sure, you can subscribe to this.textInputs.changes

      ngAfterViewInit(): void {
        this.textInputs.changes.pipe(startWith(null)).subscribe((_) => {
          this.initTextInputsListeners();
        });
      }
    

    There're another approach to achieve you want that it's use directives

    You can use a directive applied to your inputs like

    @Directive({
      selector: '[specialfocus]'
    })
    export class AddClassParentDirective {
      @Input('specialfocus') class="user-input-active"
      @HostListener('focus') addClass(){
        this.el.nativeElement.parentNode.classList.add(this.class)
      }
      @HostListener('blur') removeClass(){
        this.el.nativeElement.parentNode.classList.remove(this.class)
      }
      constructor(private el:ElementRef) { }
    }
    

    And use as

    <div class="user-input-box">
      <label for="message">Send message</label>
      <input [specialfocus] class="form-control" type="text" />
    </div>
    

    Or use a directive applied to the div (see that in this case I use as selector '.user-input-box2', so each div that has a class "user-input-box2" is really a UserInputBoxDirective

    @Directive({
      selector: '.user-input-box2'
    })
    export class UserInputBoxDirective implements AfterViewInit,OnDestroy {
      focus:boolean=false;
      subscription:any=null
      @HostBinding('class.user-input-active') get _(){
        return this.focus?true:null
      }
      @ContentChild(HTMLInputElement) input:HTMLInputElement
      constructor(private el:ElementRef) { }
      ngAfterViewInit()
      {
        const inputs=this.el.nativeElement.getElementsByTagName('input')
        if (inputs && inputs.length)
        this.subscription=merge(fromEvent(inputs[0],'focus').pipe(map(_=>true)),
                                fromEvent(inputs[0],'blur').pipe(map(_=>false)))
                                .subscribe(res=>{
                                  this.focus=res
                                })
      }
      ngOnDestroy()
      {
        this.subscription && this.subscription.unsubscribe
      }
    }
    

    You use like

    <div class="user-input-box2">
      <label for="message">Send message</label>
      <input class="form-control" type="text" />
    </div>
    

    You has the three approach (your's and this about directives) in this stackblitz