Home > Blockchain >  Angular @viewChild element's "nativeElement: and "value" are undefined
Angular @viewChild element's "nativeElement: and "value" are undefined

Time:08-25

I am using an @ViewChild reference in my component:

@ViewChild('tableCellWidget', { static: false }) tableCellWidget: ElementRef<HTMLInputElement>;

And I am attempting to access the "value" of the element in which I have attached the tableCellWidget viewChild reference to in order to dynamically set the [innerHTML] of the trailing span element to the current value of the form element loaded via <select-widget-widget> template below (this is inside a loop of a dynamically generated mat-table:

   <td mat-cell *matCellDef="let data; let rowIndex = index"> 
    <ng-container *ngIf="column !== ACTIONS_COLUMN_NAME">
          <div *ngIf="layout[i] && layout[i].widget">
            <select-widget-widget
              #tableCellWidget
              [class]="dragDropEnhancedMode && childTemplateModel ? getActiveItemClass(childTemplateModel[i]) : ''"
              (change)="onCellChange($event, rowIndex, column)"
              [logic]="layout[i]?.options?.logic || {}"
              [rowIndex]="rowIndex   paginationRowOffset"
              [dataIndex]="[].concat(rowIndex   paginationRowOffset)"
              [layoutIndex]="(layoutIndex || []).concat(i)"
              [layoutNode]="layout[i]">
            </select-widget-widget>
            <span *ngIf="tableConfig?.columnTypes[column] === 'default'" [innerHTML]="getPlainText()"></span>
          </div>
    </ng-container>
  </td>

Here is the code for the innerHTML call:

getPlainText() {
  const myValue = this.tableCellWidget?.nativeElement?.value || 'no value';
  const myValue2 = this.tableCellWidget?.nativeElement;
  console.log('tableCellWidget value', myValue); // UNDEFINED
  console.log('tableCellWidget nativeElement', myValue2); // UNDEFINED
  console.log('this.getPlainTextHasValue', this.getPlainTextHasValue); // FALSE
  return myValue;
}

get getPlainTextHasValue(): boolean {
  // returns false;
  return hasValue(this.tableCellWidget?.nativeElement?.value);
}

The logs are all returning undefined and false as seen in the code. Here is an example of the actual DOM html I'm working with and the element I am trying to use to set the innerHTML on the span from:

 <div _ngcontent-sen-c24="" >
  <!--bindings={
      "ng-reflect-ng-if": "false"
      }-->
  <select-widget-widget _ngcontent-sen-c24="" ng-reflect-layout-node="[object Object]" ng-reflect-layout-index="0,1" ng-reflect-data-index="0" ng-reflect-row-index="0" ng-reflect-logic="[object Object]" >
      <!---->
      <hidden-widget _nghost-sen-c37="" >
        <input _ngcontent-sen-c37="" type="hidden" value="Rob Bischoff-4" ng-reflect-form="[object Object]" id="control1660760198229_1660760203705" name="accountName" ><!--bindings={
            "ng-reflect-ng-if": "false"
            }-->
      </hidden-widget>
  </select-widget-widget>
</div>

Any help much appreciated. There's obviously something missing from my approach.

Update 1: I have another viewChild element in my component. It appears that when I compare the console.logs for that element and my element, I can see that they are much different. It appears that my viewchild is referencing the angular template vs the underlying native element it represents? From the pic below you can see the log output for this.filterRef vs this.tableCellWidget. With this its obvious to me why its undefined but not obvious how I can obtain the reference to the underlying element instead.

enter image description here

Update2: Based on Chris answer below, here is the contents of select-widget-widget:

    import { ChangeDetectionStrategy } from '@angular/core';
import { Component } from '@angular/core';
import { ComponentFactoryResolver } from '@angular/core';
import { ComponentRef } from '@angular/core';
import { Input } from '@angular/core';
import { OnChanges } from '@angular/core';
import { OnInit } from '@angular/core';
import { ViewChild } from '@angular/core';
import { ViewContainerRef } from '@angular/core';

@Component({
  // tslint:disable-next-line:component-selector
  selector: 'select-widget-widget',
  template: `
    <ng-container #widgetContainer></ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectWidgetComponent implements OnChanges, OnInit {
  newComponent: ComponentRef<any> = null;
  @Input() layoutNode: any;
  @Input() layoutIndex: number[];
  @Input() dataIndex: number[];
  @Input() rowIndex: number;
  @Input() childTemplateModel: any;
  @Input() flexContainerClass: string;
  @ViewChild('widgetContainer', { static: true, read: ViewContainerRef }) widgetContainer: ViewContainerRef;

  constructor(private componentFactory: ComponentFactoryResolver) {}

  ngOnInit() {
    this.updateComponent();
  }

  ngOnChanges() {
    this.updateComponent();
  }

  updateComponent() {
    if (!this.newComponent && (this.layoutNode || {}).widget) {
      this.newComponent = this.widgetContainer.createComponent(this.componentFactory.resolveComponentFactory(this.layoutNode.widget));
    }
    if (this.newComponent) {
      for (const input of ['layoutNode', 'layoutIndex', 'dataIndex', 'rowIndex', 'childTemplateModel', 'flexContainerClass']) {
        this.newComponent.instance[input] = this[input];
      }
    }
  }
}

And here is the contents of hidden.component.ts (which is one of many components that can be dynamically pulled via <select-widget-widget> and is the component that creates the hidden input in my example code that I am trying to capture the value of for the row span text:

    import cloneDeep from 'lodash/cloneDeep';
import has from 'lodash/has';
import { AbstractControl, FormControl } from '@angular/forms';
import { ChangeDetectionStrategy } from '@angular/core';
import { Component } from '@angular/core';
import { Input } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { OnInit } from '@angular/core';
import { ViewEncapsulation } from '@angular/core';
import { JsonSchemaFormService, SyncComponents } from '../json-schema-form.service';
import { decodeHtmlValue, isFormControlParentInFormArray, retainUndefinedNullValue, safeUnsubscribe, setValueByType } from '../shared/utility.functions';
import { hasValue, isInterpolated } from '../shared/validator.functions';
import { Subscription } from 'rxjs';

@Component({
  // tslint:disable-next-line:component-selector
  selector: 'hidden-widget',
  template: `
    <input [formControl]="formControl" [id]="'control'   layoutNode?._id   '_'   componentId" [name]="controlName" type="hidden" />
    <!-- DnD - View for hidden element -->
    <div *ngIf="jsf?.formOptions?.dragDropEnhancedMode || jsf?.formOptions?.debug" >
      <strong>&nbsp;<mat-icon>visibility_off</mat-icon> {{ controlName ? controlName : 'hidden' }} </strong> {{ controlValueText }}
    </div>
  `,
  styles: [
    `
      .dnd-hidden-input {
        padding: 12px 0;
      }
      .dnd-hidden-input strong .mat-icon {
        position: relative;
        top: 7px;
      }
    `
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.Emulated // Emulated | Native | None | ShadowDom,
})
export class HiddenComponent implements OnInit, OnDestroy {
  componentId: string = JsonSchemaFormService.GetUniqueId();
  formControl: AbstractControl;
  controlName: string;
  controlValueInit: any;
  controlValue: any;
  controlValueText: any;
  options: any;
  syncComponentsSubscription: Subscription;
  @Input() layoutNode: any;
  @Input() layoutIndex: number[];
  @Input() dataIndex: number[];
  @Input() rowIndex: number;

  constructor(public jsf: JsonSchemaFormService) { }

  ngOnInit() {
    this.options = cloneDeep(this.layoutNode.options) || {};
    this.jsf.initializeControl(this);

    if (!hasValue(this.controlValue) && hasValue(this.options.defaultValue)) {
      this.controlValue = this.options.defaultValue;
      this.jsf.triggerSyncComponents();
    }

    this.controlValueInit = setValueByType(this.options.dataType, this.controlValue);
    if (this.controlValue) {
      this.controlValueText = `: ${this.controlValue}`;
    }
    /** TESTING - Tries to add backward compatibility for missing Admin value: `dynamicValue` */
    // if (isInterpolated(this.controlValue) && !this.isDynamicValue) {
    //   this.options.dynamicValue = this.controlValue;
    // }
    this.syncComponentsSubscription = this.jsf.syncComponents.subscribe((value: SyncComponents) => {
      if (!value.targets.length || value.targets.includes(this.controlName)) {
        if (has(value, `data.${this.controlName}`)) {
          this.controlValue = value.data[this.controlName];
        }
        this.syncChanges();
      }
    });

    this.jsf.registerComponentInit({ componentId: this.componentId, name: this.controlName });
  }

  ngOnDestroy() {

    safeUnsubscribe(this.syncComponentsSubscription);
  }

  updateValue(value: any) {
    const typedValue = retainUndefinedNullValue(setValueByType(this.options.dataType, value));
    this.jsf.updateValue(this, typedValue);
  }

  get isDynamicValue(): boolean {
    return hasValue(this.options.dynamicValue);
  }

  syncChanges() {
    let value: any;
    /**
     * NOTE - Try to maintain interpolated value. Old way relied on value from form.data, but that can be lost after changed.
     *        Interpolated values for Hidden inputs need to persist.
     */

    /** TESTING - Tries to add backward compatibility for missing Admin value: `dynamicValue` */
    // if (isInterpolated(this.controlValue) && !this.isDynamicValue) {
    //   this.controlValueInit = this.controlValue;
    //   this.options.dynamicValue = this.controlValue;
    // }

    if (this.isDynamicValue) {
      // NEW - Interpolated value set by Admin, should always be used to set latest value from.
      value = this.options.dynamicValue;
    } else if (isInterpolated(this.controlValueInit)) {
      // OLD - Uses `controlValueInit`, but init value can be lost when Hidden value has been changed and form is re-rendered.
      value = this.controlValueInit;
    } else {
      // Either way, use current value if not interpolated.
      value = this.controlValue;
    }
    const values = this.jsf.formGroup.value;

    /** Check for reference to FormControl data */
    if (this.jsf.hasFormControlDataVariables(value)) {
      let autocompleteData = {};
      let formControlInFormArray: FormControl;
      /** Check if this FormControl is part of a FormArray */
      if (isFormControlParentInFormArray(<FormControl>this.formControl)) {
        formControlInFormArray = <FormControl>this.formControl;
      }
      const result = this.jsf.getAutoCompleteFormControlData(value, formControlInFormArray);
      value = result.newValue;
      autocompleteData = result.autocompleteData;
      const keys = Object.keys(autocompleteData);
      for (let j = 0; j < keys.length; j  ) {
        values[keys[j]] = decodeHtmlValue(autocompleteData[keys[j]]);
      }
    }
    const parsedValue = this.jsf.parseVariables(value, values);
    const typedValue = retainUndefinedNullValue(setValueByType(this.options.dataType, parsedValue));
    this.controlValue = typedValue;
    if (this.controlValue) {
      this.controlValueText = `: ${this.controlValue}`;
    }
    this.updateValue(this.controlValue);
  }

}

CodePudding user response:

It's because you're referencing a component, not an HTML element. Angular is designed such that components are "black boxes" to each other. The Angular way is to have the child emit the value to the parent, not directly query the value through html.

I don't know the internal workings of select-widget-widget so I can't show you exactly what to do, but it should have an event emitter that emits that value whenever it changes. Then you just have a property in the parent you update on change. Perhaps (change) is already doing this, and you just need to update the property in onCellChange().

Example:

<select-widget-widget
  (valueChange)='widgetValue = $event'
  ...
></select-widget-widget>
<span [innerHTML]="widgetValue"></span>
// Component TS
widgetValue = ''

or if change is emitting the value already:

<select-widget-widget
  (change)="onCellChange($event, rowIndex, column)"
  ...
></select-widget-widget>
<span [innerHTML]="widgetValue"></span>
widgetValue = '';

onCellChange(ev, row, col) {
  this.widgetValue = ev;
  ...
}

But of course you don't have to do things the Angular way. You can always just use vanilla JS.

<select-widget-widget
   id='tableCellWidget'
   ...
></select-widget-widget>
<span [innerHTML]="tableCellWidget?.value"></span>
tableCellWidget?: HTMLInputElement;

ngAfterViewInit() {
  this.tableCellWidget = document.querySelector(
    '#tableCellWidget > hidden-widget > input'
  );
}

Warning: View encapsulation goes out the window with this method. If there are mutliple widgets, you need to ensure they all have a unique id or you're gonna have a bad time. You can just append something unique to the id, like the column / row numbers.

  • Related