TypeError: document.getElementById(…) is null in aurelia, this only by first page load


#1

I use aurelia framework and Firefox browser. I became this error message when the page loads the first time: ‘TypeError: document.getElementById(…) is null’

Here the typescript code-snipped:

import { EventAggregator } from 'aurelia-event-aggregator';
import { HistoricalViewAll } from "./hist-view-all";
import { DeviceData } from "./model/device-data";
import { DeviceLocDataDTO } from "./dto/device-loc-data-dto";
import { activationStrategy } from 'aurelia-router';
import { autoinject } from 'aurelia-framework';
import { View } from "aurelia-framework";

@autoinject( EventAggregator )
@autoinject( HistoricalViewAll )
@autoinject( DeviceData )
@autoinject( DeviceLocDataDTO )
export class HistoricalViewSingle {

// any declarations...

constructor(private histDeviceDataDto: HistoricalViewAll, private deviceData: DeviceData, private deviceDataDTO: DeviceLocDataDTO, private ea: EventAggregator) {
    console.log( "HistoricalViewSingle constructor called..." );

    this.location = "Zurich";
    this.deviceSelection = "allDevices";
}

determineActivationStrategy() {
    console.log( "determineActivationStrategy() called..." );
    return activationStrategy.replace; 
}

activate( params, routeConfig, navigationInstruction ) {

    this.params = params;

    console.log( "params: " );
    console.log( params );

    this.routeConfig = routeConfig;
    console.log( "routeConfig in HistoricalViewSingle: " );
    console.log( this.routeConfig );
    this.routeConfig.navModel.setTitle = this.params.device;

    console.log( this.params.device.substr( 3, 2 ) );

    this.deviceInURL = params.device;
    console.log( this.deviceInURL );

    //necessary to get first the data before UI will be renderet... 
    return this.deviceDataDTO.postReqDeviceDataFromLocation( "devices-in-location", this.location, this.deviceSelection );
}

attached() {
    console.log( "HistoricalViewSingle is attached..." );
    this.deviceLocationList = this.deviceDataDTO.getDeviceLocationList();

    this.deviceLocationHint = "Übersicht Filiale " + this.locationName;

    if ( this.deviceLocationList.length != 0 ) {
        this.noDeviceHint = "devDataOk"
        this.setDeviceSelection( this.deviceInURL );
    } else {
        this.noDeviceHint = "Keine Sensordaten"
    }

    this.changeTimeWindow( 1 );
}

created(owningView, myView) {
    console.log("created() call in HistoricalViewSingle");
    console.log(myView);

    //this.changeTimeWindow( 1 );
}

changeTimeWindow( hours: number ) {
    var deviceId: string = this.selectedDevLocList[0].device_id;
    console.log("DeviceKey in changeTimeWindow(): " + deviceId);
    console.log(document.getElementById( 'chart-' + deviceId) + ' - chart-' + deviceId);
    const ctx1: CanvasRenderingContext2D = ( document.getElementById( 'chart-' + deviceId ) as any ).getContext( '2d' );
    this.histDeviceDataDto.changeTimeWindowForOneDevice( hours, deviceId, ctx1 );
}
}

The error will caused in this line:

const ctx1: CanvasRenderingContext2D = ( document.getElementById( 'chart-' + deviceId ) as any ).getContext( '2d' );

Here the html code:

<template> <require from="./hist-view-all"></require>
<div class="col-sm-12">
    <h1>
            <span id="filiale">${deviceLocationHint}</span>
    </h1>

    <div repeat.for="deviceData of deviceLocationList">
            <div class="pull-left">
                    <button class="btn btn-link btn-block" click.delegate="setDeviceSelection(deviceData.device_id)">${deviceData.device_id}</button>
            </div>
    </div>
    <h3>&nbsp</h3>

    <div class="margin">
            <div class="btn-group" role="group" aria-label="..." if.bind="noDeviceHint === 'devDataOk'">
                    <button name="defaultbutton" type="button" click.delegate="changeTimeWindow(1)" class="btn btn-default">1h</button>
                    <button type="button" click.delegate="changeTimeWindow(8)" class="btn btn-default">8h</button>
                    <button type="button" click.delegate="changeTimeWindow(24)" class="btn btn-default">1d</button>
                    <button type="button" click.delegate="changeTimeWindow(120)" class="btn btn-default">5d</button>
            </div>

            <div class="device-container">
                    <div repeat.for="deviceData of selectedDevLocList">
                            <div class="panel panel-default">
                                    <div class="panel-heading">
                                            <h2 class="panel-title">${deviceData.boardType}</h2>
                                            <small>${deviceData.locationName} ${deviceData.room}</small>
                                    </div>
                                    <div class="panel-body">
                                            <canvas class="chart" id="chart-${deviceData.device_id}"></canvas>
                                    </div>
                            </div>
                    </div>
            </div>
    </div>
</div>
</template>

after first page-load I click on button ‘8h’ as example, I don’t receive the error and the data in chartjs diagram will be rendered ok. The canvas id could be something like this ‘chart-am-ZH-Office’ and it would be already correct in the first page-load. I tried also to call method changeTimeWindow() in lifecycle method created(), but with the same error.

Any suggestion? Thanks


#2

Rather than using document.getElementById(), try using the attribute, ref, on the element and access the DOM element that way.

In your view…

<canvas class="chart" ref="theCanvas"></canvas>

In your viewmodel…

const ctx1: CanvasRenderingContext2D = this.theCanvas.getContext( '2d' );

See http://aurelia.io/docs/binding/basics#referencing-elements for more info on ref


#3

Thanks for fast answer, but that doesn’t work also. The “theCanvas” in viewmodel is not defined…
The aurelia-documentation explain about refs not so many…
Any other tips please?


#4

My apologies. I forgot the this. I’ve updated the original reply. I haven’t dwelt much with canvas so I’m not sure about the getContext() method. If that doesn’t work, make sure to debug the this.theCanvas and view that DOM object in your console debugger.


#5

The problem is that a ref it doesn’t work in a repeat.for loop. if I remove the repeat.for then it works fine, that is, it chart is rendered. I suspect that I have a bug with the life-cycle of the components. I first get data in the activated method and in the method attachment I call the method that gets the canvas reference to render the chart.
The error message is the following:
“TypeError: _this.theCanvas is undefined”

Here the view:

<template>
<div class="col-sm-12">
<h1>
				Historie <span id="filiale">Filialen an allen Standorte</span>
		</h1>

		<div class="margin">
				<div class="btn-group" role="group" aria-label="...">
						<button name="defaultbutton" type="button" click.delegate="changeTimeWindow(1)" class="btn btn-default">1h</button>
						<button type="button" click.delegate="changeTimeWindow(8)" class="btn btn-default">8h</button>
						<button type="button" click.delegate="changeTimeWindow(24)" class="btn btn-default">1d</button>
						<button type="button" click.delegate="changeTimeWindow(120)" class="btn btn-default">5d</button>
				</div>

				<div class="device-container">
						<div repeat.for="device of devices">
								<div class="panel panel-default">
										<div class="panel-heading">
												<h2 class="panel-title">${device.boardType}</h2>
												<small>${device.locationName} ${device.room}</small>
										</div>
										<div class="panel-body">
												<!-- <canvas class="chart" id="chart-${device.location_id}"></canvas> -->
												<canvas class="chart" ref=theCanvas></canvas>
										</div>
								</div>
						</div>
				</div>
		</div>
</template>

here the view-model:

import 'aws-sdk/dist/aws-sdk';
import * as _ from "lodash";
import * as Chart from "chart.js"; // TODO: use @types/chartjs (then the null check below is not needed anymore)
import * as Moment from "moment";
import { AmbientDevice } from "./model/ambient-device";
import { DeviceData } from "./model/device-data";
import { DeviceLocDataDTO } from "./dto/device-loc-data-dto";
import { autoinject } from 'aurelia-framework';
import { View } from "aurelia-framework";

@autoinject( DeviceData )
@autoinject( DeviceLocDataDTO )
export class HistoricalViewAll {

    private location: string;
    private deviceSelection: string;
    private devices: Array<DeviceData> = new Array<DeviceData>();
    private timewindowHours: number = 8;
    private findDevice: DeviceData = new DeviceData();
    private theCanvas;
    private ctx: CanvasRenderingContext2D;

    constructor( private deviceDataDTO: DeviceLocDataDTO, private deviceData: DeviceData ) {
        console.log( "HistViewAll constructor called..." );
        ..
        ..
        ..
        ..

        
        // init necessary to show correct boardType and deviceName in device-container
        this.devices.push(this.deviceData);
        //this.theCanvas = new HTMLCanvasElement(); 
        
        this.location = "Zurich";
        this.deviceSelection = "allDevices";
    }

    activate( params, routeConfig, navigationInstruction ) {
        //necessary to get first the data before UI will be renderet... 
        return this.deviceDataDTO.getReqAllDeviceDataFromAllLocation();
    }

    created( owningView: View, myView: View ) {
        console.log( "created() call in HistoricalViewAll" );
        console.log( owningView );
        console.log( myView );
    }
    
    bind(bindingContext: Object, overrideContext: Object) {
        console.log( "HistoricalViewAll is bindet..." );
        console.log(bindingContext);
        console.log(overrideContext);
        //this.devices = this.deviceDataDTO.getDeviceLocationList();
        console.log(this.devices);
        console.log(bindingContext);
    }

    attached() {
        console.log( "HistoricalViewAll is attached..." );
        this.devices = this.deviceDataDTO.getDeviceLocationList();
        this.changeTimeWindow( this.timewindowHours );
    }
    
    getDevices() {
        return this.devices;
    }

    changeTimeWindow( hours: number ) {
        this.timewindowHours = hours;

        this.devices.forEach(( device, i, arr ) => {
            //const ctx: CanvasRenderingContext2D = ( document.getElementById( 'chart-' + device.location_id ) as any ).getContext( '2d' );
            //console.log( this.ctx );
            //console.log( document.getElementById( 'chart-' + device.location_id ) );
            //if (typeof this.theCanvas !== null) {
                this.ctx = this.theCanvas.getContext( '2d' );
                this.drawDevice( this.ctx, device.locationName );
            //}
        } );
    }

    changeTimeWindowForOneDevice( hours: number, deviceKey: string, ctx: CanvasRenderingContext2D ) {

        this.timewindowHours = hours;

        Object.assign( this.findDevice, this.devices.find( device => device.device_id === deviceKey ) );
        this.drawDevice( ctx, deviceKey );
    }

    drawDevice( ctx: CanvasRenderingContext2D, devicename: string, correctionFactor: number = 1 ) {

    // the chart

    }
        
}

please any suggestion what I make wrong?


#6

ref would not work behind repeat.for or if.bind. You can refactor the stuff under repeat.for loop into a child component with its own js and html template. Then in the child component, you can get a valid ref.


#7

I have found the solution myself after 4 days. The solution was to move this.devices = this.deviceDataDTO.getDeviceLocationList(); in the lifecycle method created(), so when the attached method would be called the data in devices are ready. the ref works also in use with repeat.for…