handling and rendering in a widget

692
9
06-26-2021 08:46 AM
litch
by
New Contributor III

I have a simple logic working that I want to make a widget out of.

i have a problem implementing it inside the render() function.

the current state is i created the widget, and the individual functions work but once inside the are called dozens of times.

what is the correct way to call them from the render? 

widget params:

---featurelayer - from drop down list chosen by the user. - the list created on widget start and updates when map changes(layers get added or removed).

---range -number - for querying the layer - from user

---enabled - boolean - is querying active? - changes on click 

all this logic already works a quick overview of the four functions and watch:

---populateLayerList - creates feature layer List select

---onMapChange() function - calls populate function

watch for webmap changes and recreate dropdown list

---toggleQueryButton -  true or false activates the view watch for user click on map and query the layer.

---executeQuery() - gets the params and adds graphic layer to the map

 

 

import Widget from "@arcgis/core/widgets/Widget";
import MapView from "@arcgis/core/views/MapView";
import WebMap from "@arcgis/core/WebMap";
import GraphicsLayer from "@arcgis/core/layers/GraphicsLayer";
import Graphic from "@arcgis/core/Graphic";
import { subclass, property } from "@arcgis/core/core/accessorSupport/decorators";
import { tsx } from "@arcgis/core/widgets/support/widget";

import FeatureLayer from "@arcgis/core/layers/FeatureLayer";

@subclass("esri.widgets.SimpleWidget")
class SimpleWidget extends Widget {

    constructor(map: WebMap, mapView: MapView) {
        super();
        this.map = map;
        this.view = mapView;

    }

    @property()
    enabled: boolean = false

    @property()
    map: WebMap  //| null = null

    @property()
    view: MapView //| null = null


    render() {
        let dropdown = this.populateLayerList()
        this.onMapChange()
        // widget container
        // layer list to get all layers of the view - updates when change happens
        // meter range for query
        // execute button toggles on/of view.on click event that creates the query 
        //
        return <div class="groundZeroWidget">
            {dropdown}
            <input id="queryRange" type="number" />
            <button onclick={this.toggleQueryButton()}>execute</button>
        </div>
    }

    //creates the list of feature layers on open and on change
    private populateLayerList() {

        console.log("populateLayerList");

        let layerList: FeatureLayer[] = [] //list of all feature layers

        this.map.allLayers.forEach((x: FeatureLayer) => {
            if (x.type == "feature") {
                console.log(x.title);
                layerList.push(x) //list of layer titles
            }
        });

        function MakeItem(x: FeatureLayer) {
            return <option key={x}>{x.title}</option>;
        };


        return <select id="layerTitleList" >{layerList.map(MakeItem)}</select>;


    }

    private onMapChange() {

        this.map.allLayers.on("change", function (event) {
            console.log("onMapChange");
            
            // change event fires after an item has been added, moved or removed from the collection.
            // event.moved - an array of moved layers
            // event.removed - an array of removed layers
            // event.added returns an array of added layers

            // this.populateLayerList() ----------not stable yet
        })

    }

    // toggles widget active, changes button color 
    private toggleQueryButton() {
        
        console.log('toggleQueryButton');

        let layerList: __esri.Collection<FeatureLayer> | FeatureLayer[] = [] //list of all feature layers
        this.map.allLayers.forEach((x: FeatureLayer) => {
            if (x.type == "feature") {
                layerList.push(x)
            }
        });

        (this.enabled) ? (this.enabled = false) : (this.enabled = true) //toggles widget active
        console.log("enabled?:" + this.enabled);
        // change button color


        this.view.on("click", (event) => {
            console.log("click event: ", event.mapPoint);
            let groundZeroXY = event.mapPoint;
            if (this.enabled) {
                this.executeQuery(groundZeroXY, layerList);
            }
        });
    }
    //when active creates layer from specifications and adds it to the map 
    private executeQuery(screenPoint: __esri.MapViewScreenPoint, layerList: any[] | __esri.Collection<FeatureLayer>) {

        console.log("executeQuery");
        
        //get selected layer
        let layerTitle = (document.getElementById('layerTitleList') as HTMLSelectElement).value
        console.log(layerTitle);

        let queryLayer: FeatureLayer = layerList.find(x => x.title == layerTitle);

        //get selected range
        let range = (document.getElementById('queryRange') as HTMLInputElement).value;

        console.log("range: " + range);

        let Glayer = new GraphicsLayer();

        let map = this.map

        const point = this.view.toMap(screenPoint);

        // Query the for the items within x-KM from where the user clicked for layer x
        var query = queryLayer.createQuery();
        query.geometry = point;
        query.spatialRelationship = "intersects";
        query.distance = parseInt(range);
        query.units = "meters";
        query.returnGeometry = true;
        query.outFields = ["*"];
        queryLayer.queryFeatures(query).then(function (res: __esri.FeatureSet) {
            if (res.features.length === 0) {
                console.log("no items 1st layer");
            }

            console.log(res.features);
            console.log(res.features[0]);

            let graphic: Graphic[] = res.features;


            Glayer.addMany(graphic);
            map.add(Glayer);

            if (Glayer.graphics.length === 0) {
                console.log("no featuers");
            }
        });
    }

}
export = SimpleWidget;

 

 

 

 

 

0 Kudos
9 Replies
litch
by
New Contributor III

I have a simple logic working that I want to make a widget out of.

i have a problem implementing it inside the render() function.

the current state is i created the widget, and the individual functions work but once inside the are called dozens of times.

what is the correct way to call them from the render? 

widget params:

featurelayer - from drop down list chosen by the user. - the list created on widget start and updates when map changes(layers get added or removed).

range -number - for querying the layer - from user

enabled - boolean - is querying active? - changes on click 

all this logic already works a quick overview of the four functions and watch:

populateLayerList - creates feature layer List select

onMapChange() function - calls populate function

watch for webmap changes and recreate dropdown list

toggleQueryButton -  true or false activates the view watch for user click on map and query the layer.

executeQuery() - gets the params and adds graphic layer to the map

 

 

import Widget from "@arcgis/core/widgets/Widget";
import MapView from "@arcgis/core/views/MapView";
import WebMap from "@arcgis/core/WebMap";
import GraphicsLayer from "@arcgis/core/layers/GraphicsLayer";
import Graphic from "@arcgis/core/Graphic";
import { subclass, property } from "@arcgis/core/core/accessorSupport/decorators";
import { tsx } from "@arcgis/core/widgets/support/widget";

import FeatureLayer from "@arcgis/core/layers/FeatureLayer";

@subclass("esri.widgets.SimpleWidget")
class SimpleWidget extends Widget {

    constructor(map: WebMap, mapView: MapView) {
        super();
        this.map = map;
        this.view = mapView;

    }

    @property()
    enabled: boolean = false

    @property()
    map: WebMap  //| null = null

    @property()
    view: MapView //| null = null


    render() {
        let dropdown = this.populateLayerList()
        this.onMapChange()
        // widget container
        // layer list to get all layers of the view - updates when change happens
        // meter range for query
        // execute button toggles on/of view.on click event that creates the query 
        //
        return <div class="groundZeroWidget">
            {dropdown}
            <input id="queryRange" type="number" />
            <button onclick={this.toggleQueryButton()}>execute</button>
        </div>
    }

    //creates the list of feature layers on open and on change
    private populateLayerList() {

        console.log("populateLayerList");

        let layerList: FeatureLayer[] = [] //list of all feature layers

        this.map.allLayers.forEach((x: FeatureLayer) => {
            if (x.type == "feature") {
                console.log(x.title);
                layerList.push(x) //list of layer titles
            }
        });

        function MakeItem(x: FeatureLayer) {
            return <option key={x}>{x.title}</option>;
        };


        return <select id="layerTitleList" >{layerList.map(MakeItem)}</select>;


    }

    private onMapChange() {

        this.map.allLayers.on("change", function (event) {
            console.log("onMapChange");
            
            // change event fires after an item has been added, moved or removed from the collection.
            // event.moved - an array of moved layers
            // event.removed - an array of removed layers
            // event.added returns an array of added layers

            // this.populateLayerList() ----------not stable yet
        })

    }

    // toggles widget active, changes button color 
    private toggleQueryButton() {
        
        console.log('toggleQueryButton');

        let layerList: __esri.Collection<FeatureLayer> | FeatureLayer[] = [] //list of all feature layers
        this.map.allLayers.forEach((x: FeatureLayer) => {
            if (x.type == "feature") {
                layerList.push(x)
            }
        });

        (this.enabled) ? (this.enabled = false) : (this.enabled = true) //toggles widget active
        console.log("enabled?:" + this.enabled);
        // change button color


        this.view.on("click", (event) => {
            console.log("click event: ", event.mapPoint);
            let groundZeroXY = event.mapPoint;
            if (this.enabled) {
                this.executeQuery(groundZeroXY, layerList);
            }
        });
    }
    //when active creates layer from specifications and adds it to the map 
    private executeQuery(screenPoint: __esri.MapViewScreenPoint, layerList: any[] | __esri.Collection<FeatureLayer>) {

        console.log("executeQuery");
        
        //get selected layer
        let layerTitle = (document.getElementById('layerTitleList') as HTMLSelectElement).value
        console.log(layerTitle);

        let queryLayer: FeatureLayer = layerList.find(x => x.title == layerTitle);

        //get selected range
        let range = (document.getElementById('queryRange') as HTMLInputElement).value;

        console.log("range: " + range);

        let Glayer = new GraphicsLayer();

        let map = this.map

        const point = this.view.toMap(screenPoint);

        // Query the for the items within x-KM from where the user clicked for layer x
        var query = queryLayer.createQuery();
        query.geometry = point;
        query.spatialRelationship = "intersects";
        query.distance = parseInt(range);
        query.units = "meters";
        query.returnGeometry = true;
        query.outFields = ["*"];
        queryLayer.queryFeatures(query).then(function (res: __esri.FeatureSet) {
            if (res.features.length === 0) {
                console.log("no items 1st layer");
            }

            console.log(res.features);
            console.log(res.features[0]);

            let graphic: Graphic[] = res.features;


            Glayer.addMany(graphic);
            map.add(Glayer);

            if (Glayer.graphics.length === 0) {
                console.log("no featuers");
            }
        });
    }

}
export = SimpleWidget;

 

 

 

0 Kudos
AndyGup
Esri Regular Contributor

Hi @litch it's best to avoid setting up any state management in the render() method. One recommendation is to move any state management code (e.g. onMapChange) into the postInitialize() stage of the widget's life cycle.  You can implement postInitialize() just like you can with render(). Here's some additional Widget-related documentation you can read through, if you haven't done so already: https://developers.arcgis.com/javascript/latest/custom-widget/#implement-properties-and-methods

0 Kudos
litch
by
New Contributor III

Hey Andy, thank you for the answer.

To better understand creating widgets and what went wrong, I made a skeleton widget, a toggling button.

I still experience hectic behavior, while not clicking once it's still flickering "on and off".

how do I correctly use the toggle function?

import Widget from "@arcgis/core/widgets/Widget";
import { subclass, property } from "@arcgis/core/core/accessorSupport/decorators";
import { tsx } from "@arcgis/core/widgets/support/widget";


@subclass("esri.widgets.SimpleWidget2")
class SimpleWidget2 extends Widget {
    
    @property()
    bool =  false


    toggle(){
        this.bool = !this.bool
    }

    render() {
        const {bool} = this

        return (
            <div >
                <button onclick={this.toggle()}>{bool ? "Stop" : "Start"}</button>
            </div>
        );    }

}

export = SimpleWidget2;

 changing with no clickschanging with no clicks

0 Kudos
AndyGup
Esri Regular Contributor

Yep, getting closer. There is still state management in the render() method, you'll need to move the button click event listener code to the component.  Some other suggestions, note this is psuedo-code because I haven't tested it:

 

 

 

import Widget from "@arcgis/core/widgets/Widget";
import { subclass, property } from "@arcgis/core/core/accessorSupport/decorators";
import { tsx } from "@arcgis/core/widgets/support/widget";

@subclass("esri.widgets.SimpleWidget2")
class SimpleWidget2 extends Widget {
    
    @property()
    msg =  "test"; //to test simply assign this property a different string.

    private _toggle(){
        return this.msg;
    }

    render() {
        const testMessage = this._toggle();
        return (
            <div>
                {testMessage}
            </div>
        );    
    }
}

export default SimpleWidget2;

 

 

 

 

AndyGup
Esri Regular Contributor

Just a quick follow-up, I tested the above code and it worked. Here's some more psuedo-code:

 

 

    //Add widget to component
    this.simpleWidget = new SimpleWidget2({
       container: document.createElement("div")
    });
    // Be sure to add widget to the view
    this.view.ui.add(this.simpleWidget, "bottom-right");      

    // After map initialized and widget has been added to the component
    let count = 0;
    const c = this.simpleWidget.container.addEventListener("click", () => {
        this.simpleWidget.msg = "test" + this.count++;
    })

 

 

 

0 Kudos
litch
by
New Contributor III

so I tested the code by myself and the string did change.

but the main problem I had was behavior.

adding a log to your code revealed that it was still calling the function multiple times without clicking.

6d2c551e-ca2b-49b5-b29d-a266a2fbe43d.jpg

 

private _toggle(){
        console.log("test");
        return this.msg;
    }

 

^only change I made.

 

I also created a cleaner version of my skeleton widget:

 

import Widget from "@arcgis/core/widgets/Widget";
import { subclass, property } from "@arcgis/core/core/accessorSupport/decorators";
import { tsx } from "@arcgis/core/widgets/support/widget";


@subclass("esri.widgets.SimpleWidget2")
class SimpleWidget2 extends Widget {
    
    @property()
    bool =  false


    private toggle(): void {
        this.bool = !this.bool;        
      }

    render() {
        const bool = this.bool
        const text = bool ? "Stop" : "Start"
        const toggle =this.toggle

        return (
            <div >
                <button onclick={toggle}>{text}</button>
            </div>
        );    }

}

export = SimpleWidget2;

 

I tried a version with:

@property()
text = this.bool ? "Stop" : "Start"

and in the renderer:

const text = this.text  

both versions didn't work.

adding "bind={this} " which I found in Esri repos worked!

and this is a working sample:

 

import Widget from "@arcgis/core/widgets/Widget";
import { subclass, property } from "@arcgis/core/core/accessorSupport/decorators";
import { tsx } from "@arcgis/core/widgets/support/widget";


@subclass("esri.widgets.SimpleWidget2")
class SimpleWidget2 extends Widget {
    
    @property()
    bool =  false


    private toggle(): void {
        this.bool = !this.bool;
        console.log(this.bool);
      }

    render() {
        const bool = this.bool
        const text = bool ? "Stop" : "Start"
        const toggle =this.toggle

        return (
            <div >
                <button bind={this} onclick={toggle}>{text}</button>
            </div>
        );    }

}

export = SimpleWidget2;

 

this is how I add my widgets if it matters:

 

view.ui.add(new SimpleWidget2(),"top-right")

 

id like to better understand why your code calls the function multiple times and how can I control it,

in my original widget functions are supposed to create an HTML element once, toggle, query, and listen.

the problem is the multiple and unintentional function calling.

P.S I added a postInitialize() as you suggested and the listener seems to work.

0 Kudos
AndyGup
Esri Regular Contributor

Hmmm, the multiple calls are most likely related to the toggle function, it shouldn't really be there. A better approach would be to remove toggleI() and any other state management/business logic from the widget and only use plain old data binding. That way the View is only used as a View. The component should handle all state management and business logic.

So in the widget you'd have:

@property()
message = "Start"; //default value


render() {
   return (
     <div>{this.message}</div>
   );
}

 

 

0 Kudos
litch
by
New Contributor III

This is the heart of my question.

My widget uses the maps feature layers(dynamic dropdown), this doesn't only change every app, but in some apps changes while using.

The data isn't static, I have a function to create it. How do I link it correctly?

My widget can have an on/off state while open(not destroyed just off). How can I change it if every function calls itself without control?

0 Kudos
AndyGup
Esri Regular Contributor

You'll need to use Angular data binding to pass data between the component class and the widget class, here's some additional info: https://angular.io/guide/two-way-binding and https://angular.io/guide/binding-syntax 

0 Kudos