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;
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;
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.
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 clicks
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;
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++;
})
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.
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.
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>
);
}
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?
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