5

I'm trying to draw two divs (each containing components) that are connected by a line. The line's length depends on the widths of the divs, which I'm accessing using the nativeElement property.

When I use the [style.width] property in the template, and call a function to calculate the width using nativeElements, I get this error:

ERROR TypeError: Cannot read property 'nativeElement' of undefined
    at BeanViewComponent.webpackJsonp.../../../../../src/app/pages/shared/stringbean/beans/beanView.component.ts.BeanViewComponent.getWidth (beanView.component.ts:81)
    at Object.eval [as updateRenderer] (BeanViewComponent.html:14)
    at Object.debugUpdateRenderer [as updateRenderer] (core.es5.js:13094)
    at checkAndUpdateView (core.es5.js:12241)
    at callViewAction (core.es5.js:12601)
    at execEmbeddedViewsAction (core.es5.js:12559)
    at checkAndUpdateView (core.es5.js:12237)
    at callViewAction (core.es5.js:12601)
    at execComponentViewsAction (core.es5.js:12533)
    at checkAndUpdateView (core.es5.js:12242)
    at callViewAction (core.es5.js:12601)
    at execEmbeddedViewsAction (core.es5.js:12559)
    at checkAndUpdateView (core.es5.js:12237)
    at callViewAction (core.es5.js:12601)
    at execEmbeddedViewsAction (core.es5.js:12559)

Here's the template:

<div class="bean-view" (click)="backgroundClick.emit()"> 
  <div class="bean-label-container" *ngIf="showLabels">
    <div class="bean-label" *ngFor="let label of bean.labels">{{label}}</div>
  </div>
  <div class="bean-icon-container" #beanIconContainerElement>
    <sb-bean-icon [type]="bean.type"></sb-bean-icon>
  </div>
  <div *ngIf="bean" class="bean" (click)="contentClick.emit()" #beanElement>
    <sb-bean-content [class.disable-click]="disableContentClick" [bean]="bean"></sb-bean-content>
  </div>
  <div class="bean-complete-marker completion-line" [style.width]="getWidth()"></div>
  <div class="bean-response-wrapper">
    <div class="bean-response" *ngFor="let response of bean.response" (click)="responseClick.emit()" #beanResponseElement>
      <sb-bean-content [bean]="response" [isResponse]="true"></sb-bean-content>
    </div>
  </div>
</div>

And here's the component code:

import {
  Component,
  OnInit,
  ChangeDetectionStrategy,
  Input,
  Output,
  EventEmitter,
  ViewChild,
  ElementRef
} from '@angular/core';
import { Bean } from '../../../../shared/model';

@Component({
  selector: 'bean-view',
  templateUrl: './beanView.html',
  styleUrls: ['./beanView.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BeanViewComponent implements OnInit {
  @Input() bean: Bean;
  @Input() disableContentClick: Bean;
  @Input() showLabels: boolean;

  @Output() contentClick = new EventEmitter();
  @Output() responseClick = new EventEmitter();
  @Output() backgroundClick = new EventEmitter();

  @ViewChild('beanIconContainerElement') beanIconContainerElement;
  @ViewChild('beanElement') beanElement;
  @ViewChild('beanResponseElement') beanResponseElement;

  constructor() {
  }
  ngOnInit() {
  }

  getWidth(): string {
    const bound: number = 200;
    const iconWidth: number = this.beanIconContainerElement.nativeElement.offsetWidth;
    const beanWidth: number = this.beanElement.nativeElement.offsetWidth;
    const width: number = bound - (iconWidth + beanWidth);
    return String(width) + 'px';
  }
}
plsplox
  • 179
  • 2
  • 11

3 Answers3

1

Try placing the code inside

ngAfterViewInit() { }

At this point the html should be completely rendered.

jeprubio
  • 17,312
  • 5
  • 45
  • 56
  • I tried using ngAfterViewInit(), and while the nativeElement access errors were not thrown, the lines were not drawn until I actually clicked on the elements. Any ideas why this could be? – plsplox Dec 20 '17 at 22:16
  • 1
    You can create a dynamicWidth var to undefined. Then, in ngAfterViewInit if dinamicWidth is undefined get the width with nativeElement and save it on that dinamicWidth var and reload the template. – jeprubio Dec 21 '17 at 10:36
  • Thanks for the lead! Could you point me in the right direction to for reloading the template? – plsplox Dec 21 '17 at 14:29
1

A possible solution:

public dynamicWidth: number = undefined;

ngAfterViewInit() {
   this.dynamicWidth = this.getWidth();
}

getWidth(): number {
  const bound: number = 200;
  const iconWidth: number = 
  this.beanIconContainerElement.nativeElement.offsetWidth;
  const beanWidth: number = this.beanElement.nativeElement.offsetWidth;
  const width: number = bound - (iconWidth + beanWidth);
  return width;
}

template:

<div *ngIf="dynamicWidth" [style.width.px]="dynamicWidth"></div>

Alternatively, if you're using flexbox, you could likely forego the entire width calculation and do something like

.bean-view {
    display: flex;
}
.completion-line {
    flex: auto;
}

There are likely many other ways to do this, but the above is the first thing that came to mind which is closest to what you're already doing.

Jaron Thatcher
  • 814
  • 2
  • 9
  • 24
  • I the first solution (load event), but unfortunately that didn't work :( the second solution threw the same nativeElement access error, but ngAfterViewInit() did not throw the same error. However, the ngAfterViewInit method only draws the lines with the correct widths when I click on the elements – plsplox Dec 20 '17 at 22:14
  • On the `div` with dynamic width, can you try putting `*ngIf="dynamicWidth"` – Jaron Thatcher Dec 20 '17 at 22:28
  • Unfortunately that didn't work. The line was not drawn at all – plsplox Dec 21 '17 at 18:34
  • That's actually particularly odd to me.. Going to see if I can replicate – Jaron Thatcher Dec 21 '17 at 18:38
  • Can you try the changes I made in the answer? It seems to work great for me using a number and `style.width.px`, when it didn't show up using the string with `'px'` appended – Jaron Thatcher Dec 21 '17 at 18:47
0

Ok so a trick that works: onInit do:

this.getWidth();

and getWidth should look like this:

getWidth(): string {
if (this.beanIconContainerElement.nativeElement.offsetWidth > 0 && this.beanElement.nativeElement.offsetWidth > 0) {
    const bound: number = 200;
    const iconWidth: number = this.beanIconContainerElement.nativeElement.offsetWidth;
    const beanWidth: number = this.beanElement.nativeElement.offsetWidth;
    const width: number = bound - (iconWidth + beanWidth);
    return String(width) + 'px';
  }
} else {
setTimeout(() => {
this.getWidth();}, 500);
DanielWaw
  • 669
  • 7
  • 22