TypeError: Cannot read properties of null (reading 'append'), angular 13 & @arcgis/core 4.22

17764
2
01-17-2022 07:01 AM
gryffs
by
New Contributor

Hello, I'm working with the ArcGIS API for Javascript, @arcgis/core ^4.22 inside an angular (13.1.1) application and encountering an error when the user navigates to the map component on second and subsequent loads. This error does not occur on the initial load of the map component, only if the user navigates to a different component and then returns back to the map component.

The map component contains two FeatureTables that will display (one at a time) based on the user selecting the layer list they desire and toggling the ability to show the FeatureTables. This error occurs when the second FeatureTable is set to be created within the code, invoked on line 86, referencing lines 383-399 of the component.

This component is quite busy with the map, layers, and table setup. It also uses a splitter on the display to make it so that the tables can be hidden or displayed and even resized on the fly. By default, only the map is displayed, with the tables hidden.

Has anyone else encountered this error? I have been unable to catch this error (with multiple try/catchs) in the component itself and have run out of places to search. Any help, guidance, insight would be greatly appreciated. Thank you!

Here is the error that is thrown:

core.mjs:6469 ERROR Error: Uncaught (in promise): TypeError: Cannot read properties of null (reading 'append')
TypeError: Cannot read properties of null (reading 'append')
    at Popover.js:5:1313
    at ZoneDelegate.invoke (zone.js:372:1)
    at Object.onInvoke (core.mjs:25450:1)
    at ZoneDelegate.invoke (zone.js:371:1)
    at Zone.run (zone.js:134:1)
    at zone.js:1276:1
    at ZoneDelegate.invokeTask (zone.js:406:1)
    at Object.onInvokeTask (core.mjs:25437:1)
    at ZoneDelegate.invokeTask (zone.js:405:1)
    at Zone.runTask (zone.js:178:1)
    at resolvePromise (zone.js:1213:1)
    at zone.js:1283:1
    at ZoneDelegate.invokeTask (zone.js:406:1)
    at Object.onInvokeTask (core.mjs:25437:1)
    at ZoneDelegate.invokeTask (zone.js:405:1)
    at Zone.runTask (zone.js:178:1)
    at drainMicroTaskQueue (zone.js:582:1)

 

And the component:

import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { watch, whenFalse, whenTrue } from '@arcgis/core/core/watchUtils';
import Graphic from '@arcgis/core/Graphic';
import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import Map from '@arcgis/core/Map';
import PopupTemplate from '@arcgis/core/PopupTemplate';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol';
import MapView from '@arcgis/core/views/MapView';
import FeatureTable from '@arcgis/core/widgets/FeatureTable';
import LayerList from '@arcgis/core/widgets/LayerList';
import Locate from '@arcgis/core/widgets/Locate';
import Search from '@arcgis/core/widgets/Search';
import { TranslateService } from '@ngx-translate/core';
import { ARCGIS_CONFIG, ArcgisConfig } from '../arcgis';
import { DeviceDetectorService } from 'ngx-device-detector';

import HHelpers from '../helpers';

@Component({
    selector: 'hmap-container',
    templateUrl: '.hmap.component.html',
    styleUrls: ['.hmap.component.scss'],
    encapsulation: ViewEncapsulation.None
})
export class HContainerComponent implements OnInit, OnDestroy {

    public constructor(
        @Inject(ARCGIS_CONFIG) private config: ArcgisConfig,
        private deviceService: DeviceDetectorService,
        private translate: TranslateService,
    ) { this.helper = new HHelpers() }

    @ViewChild('mapViewNode', { static: true }) private mapViewEl!: ElementRef;
    @ViewChild('peopelTableDiv', { static: true }) private peopleTableDivEl!: ElementRef;
    @ViewChild('languageTableDiv', { static: true }) private languageTableDivEl!: ElementRef;
    @ViewChild('searchDiv', { static: true }) private searchDivEl!: ElementRef;
    private helper: any;
    public view!: MapView;
    public slideToggleScript: string = 'Show Feature Table';
    public isChecked: boolean = false;
    public isLoading = true;
    private isMobile: boolean = this.deviceService.isMobile();
    private map!: Map;
    private pointPeopleLayer!: FeatureLayer;
    private pointLanguageLayer!: FeatureLayer;
    private pollyPeopleLayer!: FeatureLayer;
    private pollyLanguageLayer!: FeatureLayer;
    private pntpplnCtryLayer!: FeatureLayer;
    private locateWidget!: Locate;
    private position: number[];
    private peopleTable!: FeatureTable;
    private languageTable!: FeatureTable;
    private layerList!: LayerList;
    private oldGraphics: any;
    private defaultZoom: number = 6;

    public ngOnInit(): void {
        this.positionAndMapLoad();

    };

    public ngOnDestroy(): void {
        // Since: ArcGIS API for JavaScript 4.17
        // Destroys the view, and any associated resources, including its map, popup, and UI elements.
        // this includes everything this map initializes, confirmed they are destroyed through logging.
        if (this.view) this.view.destroy();
        if (this.languageTable) this.languageTable.destroy();
        if (this.peopleTable) this.peopleTable.destroy();
    };

    public onSplitResize(): void {
        // below is an unfortunate hack to force the table to resize
        window.dispatchEvent(new Event('resize'));
    }

    private async positionAndMapLoad() {
        await this.setPosition();
        await this.initializeMap();
        await this.addLayers();
        console.log('map is initialized');
        this.layerList = new LayerList({
            view: this.view,
        })
        this.view.ui.add(this.layerList, 'bottom-left');
        this.initializePeopleTable();
        this.initializeLanguageTable();
        this.watchLayerViews();

        this.view.popup.watch('selectedFeature', async (feature) => {
            this.view.graphics.removeAll();
            const PEID = feature && feature.attributes.PEID;
            const ROL = feature && feature.attributes.ROL;
            if (this.view.popup.visible && ROL) {
                const query = this.pollyLanguageLayer.createQuery();
                query.where = `langCd = '${ROL}'`;
                query.returnGeometry = true;
                query.geometry = this.view.extent;
                const lanQueryResults = await this.pollyLanguageLayer.queryFeatures(query);
                const graphics = lanQueryResults.features.map((polyFeat) => {
                    return new Graphic({
                        geometry: polyFeat.geometry,
                        symbol: new SimpleFillSymbol({
                            color: [38, 0, 92, 0.4],
                            style: 'solid',
                            outline: {
                                color: [38, 0, 92, 0.4],
                                width: 0.5
                            }
                        })
                    })
                })
                this.view.graphics.addMany(graphics);
            }
            if (this.view.popup.visible && PEID) {
                const query = this.pollyPeopleLayer.createQuery();
                query.where = `PEID = ${PEID}`;
                query.returnGeometry = true;
                query.geometry = this.view.extent;
                const queryResults = await this.pollyPeopleLayer.queryFeatures(query);
                const graphics = queryResults.features.map((polyFeat) => {
                    const color = this.helper.setMapColorOpacity(feature.attributes.Special);
                    return new Graphic({
                        geometry: polyFeat.geometry,
                        symbol: new SimpleFillSymbol({
                            color,
                            style: 'solid',
                            outline: {
                                color,
                                width: 0.5
                            }
                        })
                    })
                })
                this.view.graphics.addMany(graphics);
            }
        })

        watch(this.view.popup.viewModel, 'active', () => {
            const feature = this.view.popup.selectedFeature;
            if (!this.view.popup.visible) {
                this.view.graphics.removeAll();
                return;
            }
            if (!feature) {
                this.view.graphics.removeAll();
                return;
            }
        });
    };

    private async updatePeople() {
        const newGraphics = await this.queryPeopleGraphics();
        const editResp = await this.pointPeopleLayer.applyEdits({
            addFeatures: newGraphics,
            deleteFeatures: this.oldGraphics
        });
        this.oldGraphics = editResp.addFeatureResults;
    };

    private async queryPeopleGraphics() {
        const query = this.pntpplnCtryLayer.createQuery();
        query.returnGeometry = true;
        query.geometry = this.view.extent;
        const queryResults = await this.pntpplnCtryLayer.queryFeatures(query);
        return queryResults.features.map((feat) => {
            const SpecialString = this.getStatus(feat.attributes.Special);
            return new Graphic({
                geometry: feat.geometry,
                attributes: {
                    SpecialString,
                    ...feat.attributes
                }
            })
        });
    };

    private async setPosition(): Promise<void> {
        try {
            const {
                coords
            } = await this.helper.getPosition();

            this.position = [coords.longitude, coords.latitude]
        } catch (error) {
            this.position = [0, 0];
            this.defaultZoom = 1;
        }
    };

    private initializeMap(): Promise<any> {
        const container = this.mapViewEl.nativeElement;

        const popupLang = new PopupTemplate({
            title: '{Language}',
            content: [{
                type: 'fields',
                fieldInfos: [{
                    fieldName: 'ROL',
                    label: 'ROL'
                }]
            }],
        });

        this.pntpplnCtryLayer = new FeatureLayer({
            url: this.config.pntpplnCtryFeatureServer,
            id: 'pntpplnCtryLayer',
            outFields: ['OBJECTID', ...this.helper.outfields['pntpplnCtryLayer']],
        });

        this.pointLanguageLayer = new FeatureLayer({
            url: this.config.pntlangFeatureServer,
            id: 'pntlangLayer',
            title: this.getTranslation('Common_Languages'),
            outFields: ['OBJECTID', ...this.helper.outfields['pointLanguageLayer']],
            opacity: 0.8,
            popupTemplate: popupLang,
            visible: false
        });

        this.pollyPeopleLayer = new FeatureLayer({
            url: this.config.peidFeatureServer,
            id: 'pollyPeopleLayer',
            title: 'People Geo',
            outFields: ['PEID'],
            opacity: 0.01,
            listMode: 'hide'
        });

        this.pollyLanguageLayer = new FeatureLayer({
            url: this.config.languageFeatureServer,
            id: 'pollyLanguageLayer',
            title: 'Language Geo',
            outFields: ['langCd'],
            listMode: 'hide',
            opacity: 0.01
        });

        const map = new Map({
            basemap: 'gray-vector',
        });

        const view = new MapView({
            container,
            map,
            zoom: this.defaultZoom,
            center: this.position
        });

        const searchWidget = new Search({
            view: view,
            popupEnabled: false,
            locationEnabled: false,
            resultGraphicEnabled: false,
            container: this.searchDivEl.nativeElement
        });
        searchWidget.on('select-result', () => {
            this.view.zoom = this.defaultZoom;
        });
        view.ui.add(searchWidget, {
            position: 'top-right',
            index: 1,
        });

        this.view = view;
        this.map = map;

        this.view.constraints = {
            rotationEnabled: false,
            minZoom: 10,
            maxZoom: 0
        };
        this.locateWidget = new Locate({
            view: view,
        });
        this.locateWidget.on('locate', () => {
            this.view.zoom = this.defaultZoom;
        });

        if (this.isMobile) {
            this.view.popup.collapseEnabled = false;
            this.view.ui.remove('zoom');
            this.view.ui.add(this.locateWidget, 'bottom-right');
        } else {
            this.view.popup.dockEnabled = true;
            this.view.popup.dockOptions = {
                position: 'bottom-right',
                buttonEnabled: false,
                breakpoint: false
            }
            this.view.ui.add(this.locateWidget, 'top-left');
        }
        return this.view.when();
    };

    private addLayers() {
        return Promise.all([
            this.initializePeopleLayer(),
            this.map.add(this.pointLanguageLayer),
        ]);
    };

    private async initializePeopleLayer() {

        const popupPeople = new PopupTemplate({
            title: '{Name}',
            overwriteActions: true,
            outFields: ['OBJECTID', ...this.helper.outfields['popupPeople']],
            content: (feature: __esri.Feature) => {
                const div = document.createElement('div');
                const {
                    PicURLS,
                    Lang,
                    Pop = 0,
                    Special,
                } = feature.graphic.attributes;

                div.innerHTML = `<div style="float: left"><img src=${PicURLS}></div>
                    <div style="float: left;padding-left: 10px;"><p><b>${this.getTranslation('Common_Population')}: </b>${Pop.toLocaleString()}</p>
                    <p><b>${this.getTranslation('Common_Language')}: </b>${Lang}</p><p>
                    <p><b>${this.getTranslation('Common_Status')}: </b>${this.getStatus(Special)}</p></div>`;

                return div;
            },
            fieldInfos: this.helper.popupPeopleFieldInfo(),
        });

        const graphics = await this.queryPeopleGraphics();
        this.oldGraphics = graphics;

        this.pointPeopleLayer = new FeatureLayer({
            id: 'pntPeopleLayer',
            title: this.getTranslation('Common_PeopleGroups'),
            fields: this.helper.peopleFields(),
            source: graphics,
            objectIdField: 'OBJECTID',
            geometryType: 'point',
            spatialReference: { wkid: 3857 },
            opacity: 0.8,
            popupTemplate: popupPeople,
            renderer: this.helper.peopleLayerRenderer(),
        });

        return this.map.add(this.pointPeopleLayer);
    };

    private getTranslation(key: string): string {
        return this.translate.instant(key);
    };

    private getStatus = (stat: number): string => {
        if (stat === 0) return `<span style="color: rgb(${this.helper.mapColors[0]});">${this.getTranslation('Common_Status1')}</span>`;
        if (stat === 1) return `<span style="color: rgb(${this.helper.mapColors[1]});">${this.getTranslation('Common_Status2')}</span>`;
        if (stat === 2) return `<span style="color: rgb(${this.helper.mapColors[2]});">${this.getTranslation('Common_Status3')}</span>`;
        return this.getTranslation('Common_Unknown');
    };

    private initializePeopleTable() {
        this.peopleTable = new FeatureTable({
            view: this.view,
            layer: this.pointPeopleLayer,
            filterGeometry: this.view.extent,
            fieldConfigs: [{
                name: 'Name',
                label: this.getTranslation('Common_PeopleName'),
                direction: 'asc',
            }, {
                name: 'Ctry',
                label: this.getTranslation('Common_Country'),
            }, {
                name: 'Pop',
                label: this.getTranslation('Common_Population'),
            }, {
                name: 'Lang',
                label: this.getTranslation('Common_Language'),
            }, {
                name: 'SpecialString',
                label: this.getTranslation('Common_Status'),
            }],
            container: this.peopleTableDivEl.nativeElement
        });
    };

    private initializeLanguageTable() {
        this.languageTable = new FeatureTable({
            view: this.view,
            layer: this.pointLanguageLayer,
            fieldConfigs: [{
                name: 'Language',
                label: this.getTranslation('Common_Language'),
                direction: 'asc'
            }, {
                name: 'ROL',
                label: 'ROL'
            }],
            container: this.languageTableDivEl.nativeElement
        });
        // this table is set invisible because its dom element is hidden on first load
        this.languageTable.visible = false;
    };

    private async watchLayerViews(): Promise<void> {
        const [
            peopleLayer,
            languageLayer
        ] = await this.whenLayerViews();

        whenTrue(this.view, 'stationary', async () => {
            if (this.view.extent && this.peopleTable) {
                await this.updatePeople(); // wait for update before applying filter
                this.peopleTable.filterGeometry = this.view.extent;
            }
        });

        whenFalse(this.view, 'updating', () => {
            if (this.view.extent && this.languageTable) {
                this.languageTable.filterGeometry = this.view.extent;
            }
            this.isLoading = false;
        });
        // when language layer is visible, get rid of the people layer
        watch(languageLayer, 'visible', (e) => {
            if (e) {
                this.pointPeopleLayer.visible = false;
                this.view.popup.close();
                this.view.graphics.removeAll();
                this.peopleTable.visible = false;
                this.languageTable.visible = true;
            }
        });
        // when the people layer is visible, get rid of the language layer
        watch(peopleLayer, 'visible', (e) => {
            if (e) {
                this.pointLanguageLayer.visible = false;
                this.view.popup.close();
                this.view.graphics.removeAll();
                this.languageTable.visible = false;
                this.peopleTable.visible = true;
            }
        });
    };

    private whenLayerViews() {
        return Promise.all([
            this.view.whenLayerView(this.pointPeopleLayer),
            this.view.whenLayerView(this.pointLanguageLayer)
        ]);
    };
}

 

Here is the html:

<div *ngIf="isLoading">
    <mat-spinner class="h-spinner"></mat-spinner>
</div>
<as-split direction="vertical"
          (dragEnd)="onSplitResize()">
    <as-split-area [size]="50">
        <div class="mapContainer">
            <div>
                <mat-slide-toggle [(ngModel)]="isChecked">
                    {{(isChecked ? 'Common_HideFeatureTable' : 'Common_ShowFeatureTable') | translate}}
                </mat-slide-toggle>
            </div>
            <div #searchDiv
                 class="hSearch"></div>
            <div #mapViewNode
                 class="mapViewNode"></div>
        </div>
    </as-split-area>
    <as-split-area [size]="50"
                   [visible]="isChecked">
        <div class="tableContainer">
            <div #peopelTableDiv></div>
            <div #languageTableDiv></div>
        </div>
    </as-split-area>
</as-split>

  

Tags (3)
0 Kudos
2 Replies
AndyGup
Esri Regular Contributor

Hi @gryffs this sounds like a component life-cycle issue and those can be a bit tricky. If you comment out line 86 does everything work correctly? I'm not sure what the dependencies are but that seemed like a good starting point.

0 Kudos
gryffs
by
New Contributor

Hi @AndyGup , yes, when I comment out line 86 I no longer receive the error. It is quite confusing because it works fine on initial load and only throws the error if the user navigates back to the component. I've moved the initializeTable function to multiple locations within the component in an attempt to find a place that does not cause the error, but this has been unsuccessful.  Also, even though the error is thrown, I do not notice any incorrect functionality, the map, layers, and tables seem to all work as intended.

0 Kudos