I am using ArcGIS Experience Builder.
I'm working on a React component that toggles visibility of map layers in an Esri map. My widget works perfectly when I hardcode the layers configuration, but fails to add layers when I load the configuration from a config.json file.
Working Code Example:
import { React, AllWidgetProps } from 'jimu-core';
import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis';
import FeatureLayer from 'esri/layers/FeatureLayer';
import { IMConfig } from '../config';
const { useState, useEffect } = React;
const layersConfig = [
{
name: "Layer 1",
url: "https://example.com/arcgis/rest/services/Layer1/MapServer/0",
},
{
name: "Layer 2",
url: "https://example.com/arcgis/rest/services/Layer2/MapServer/0",
}
];
const Widget = (props: AllWidgetProps<IMConfig>) => {
const [jimuMapView, setJimuMapView] = useState<JimuMapView>(null);
const [layers, setLayers] = useState([]);
useEffect(() => {
if (props.config && props.config.layers) { //not necessary in this case
const initialLayers = layersConfig.map(layerConfig => ({
...layerConfig,
layer: new FeatureLayer({
url: layerConfig.url,
title: layerConfig.name,
visible: false
})
}));
console.log('Initial layers:', initialLayers);
setLayers([...initialLayers]);
}
}, [config.props]); //also not necessary in the working sample
const activeViewChangeHandler = (jmv: JimuMapView) => {
if(jmv) {
setJimuMapView(jmv);
layers.forEach(({ layer }) => jmv.view.map.add(layer));
}
};
const toggleLayerVisibility = (index) => {
const newLayers = layers.map((layer, idx) => {
if (idx === index) {
layer.layer.visible = !layer.layer.visible;
}
return layer;
});
setLayers(newLayers);
};
return (
<div>
{props.useMapWidgetIds && props.useMapWidgetIds.length === 1 && (
<JimuMapViewComponent props.useMapWidgetId={props.useMapWidgetIds[0]} onActiveViewChange={activeViewChangeHandler} />
)}
<div>
{layers.map((layer, index) => (
<label key={index}>
<input
type="checkbox"
checked={layer.layer.visible}
onChange={() => toggleLayerVisibility(index)}
/>
{layer.name}
</label>
))}
</div>
</div>
);
};
export default Widget;
Non-Working Code (using config.json):
// Similar to the above, but `layersConfig` is replaced with `props.config.layers`
useEffect(() => {
if (props.config && props.config.layers) {
const initialLayers = props.config.layers.map(layerConfig => ({ //<--- This changed!
...layerConfig,
layer: new FeatureLayer({
url: layerConfig.url,
title: layerConfig.name,
visible: false
})
}));
console.log('Initial layers:', initialLayers);
setLayers([...initialLayers]);
}
}, [props.config]);
config.json:
{
"layers": [
{
"name": "Layer 1",
"url": "hidden",
},
{
"name": "Layer 2",
"url": "hidden"
}
]
}
config.ts:
import { ImmutableObject } from 'seamless-immutable';
export interface LayerConfig {
name: string;
url: string;
}
export interface Config {
layers: LayerConfig[];
}
export type IMConfig = ImmutableObject<Config>;
Error Message:
[esri.WebMap] #add() The item being added is not a Layer or a Promise that resolves to a Layer.
This error occurs at the line where I try to add layers to the map:
layers.forEach(({ layer }) => jmv.view.map.add(layer));
Extra:
Here is initial layers for the working sample:
[initial layers for working sample](@https://i.sstatic.net/8ABzS7TK.png)
And for the non-working:
[initial layers for non working sample](@https://i.sstatic.net/XIKFGj3c.png)
1. Checked the URLs in the config file to ensure they are correct and accessible.
2. Added logs to various points in the component to ensure the data is being loaded and state changes are occurring as expected.
3. Ensured the component is correctly re-rendering on state changes.
Please help I've been trying to figure it out by different means for the past 10 hours...
Edit:
Config from both working and non-working samples:
Solved! Go to Solution.
SOLUTION:
useEffect(() => {
let layersConfig = [];
if (props.config && props.config.layers) {
// Parse the JSON string to JavaScript object
layersConfig = JSON.parse(JSON.stringify(props.config.layers));
const initialLayers = layersConfig.map(layerConfig => {
return {
...layerConfig,
layer: new FeatureLayer({
url: layerConfig.url,
title: layerConfig.name,
visible: false
})
};
});
setLayers([...initialLayers]);
}
}, [props.config]);
Since I typecasted it into ImmutableObject, it added a bunch of other properties that obscured it and I suppose it wasn't identified as a layer, so I parsed it again!
I think I have figured out what is going on. You have the add layer function within the activeViewChangeHandler() function, but that function only fires when jimuMapView changes, as in the web map is created, destroyed or replaced with an entirely new web map. This only happens once in your widget which is before the layer array is ready.
Here is how I think your widget is rendering:
Moving the add layers function within the same useEffect that reads the JSON should fix the issue.
I appreciate your response Jeffrey! Thank you for taking the time. Unfortunately, it still does not work. Here is my adapted code:
import { React, AllWidgetProps } from 'jimu-core';
import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis';
import FeatureLayer from 'esri/layers/FeatureLayer';
import { IMConfig } from '../config';
const { useState, useEffect } = React;
const Widget = (props: AllWidgetProps<IMConfig>) => {
const [jimuMapView, setJimuMapView] = useState<JimuMapView>(null);
const [layers, setLayers] = useState([]);
useEffect(() => {
//For reading the JSON
if (props.config && props.config.layers) {
const initialLayers = props.config.layers.map(layerConfig => ({
...layerConfig,
layer: new FeatureLayer({
url: layerConfig.url,
title: layerConfig.name,
visible: false
})
}));
console.log('Initial layers:', initialLayers);
setLayers([...initialLayers]);
//For adding the layers
if (jimuMapView) {
initialLayers.forEach(({ layer }) => jimuMapView.view.map.add(layer));
}
}
}, [props.config, jimuMapView]);
const activeViewChangeHandler = (jmv: JimuMapView) => {
if(jmv) {
setJimuMapView(jmv);
}
};
const toggleLayerVisibility = (index) => {
const newLayers = layers.map((layer, idx) => {
if (idx === index) {
layer.layer.visible = !layer.layer.visible;
}
return layer;
});
setLayers(newLayers);
};
return (
<div>
{props.useMapWidgetIds && props.useMapWidgetIds.length === 1 && (
<JimuMapViewComponent useMapWidgetId={props.useMapWidgetIds[0]} onActiveViewChange={activeViewChangeHandler} />
)}
<div>
{layers.map((layer, index) => (
<label key={index}>
<input
type="checkbox"
checked={layer.layer.visible}
onChange={() => toggleLayerVisibility(index)}
/>
{layer.name}
</label>
))}
</div>
</div>
);
};
export default Widget;
Try changing your add layer line to this.
//For adding the layers
if (jimuMapView) {
initialLayers.forEach(({ layer }) => jimuMapView.view.map.add(layer.layer));
}
Sample:
useEffect(() => {
//For readin JSON data
console.log('Config on effect:', props.config);
if (props.config && props.config.layers) {
const initialLayers = props.config.layers.map(layerConfig => {
console.log(`URL for layer: ${layerConfig.url}`);
return {
...layerConfig,
layer: new FeatureLayer({
url: layerConfig.url,
title: layerConfig.name,
visible: false
})
};
});
console.log('Initial layers:', initialLayers);
setLayers([...initialLayers]);
}
//For adding layers
if (jimuMapView) {
layers.forEach(({ layer }) => jimuMapView.view.map.add(layer.layer));
}
}, [props.config, jimuMapView]);
Does not work...
Edit:
I can only call initialLayers inside map function, but there is no layer property.
Extra:
SOLUTION:
useEffect(() => {
let layersConfig = [];
if (props.config && props.config.layers) {
// Parse the JSON string to JavaScript object
layersConfig = JSON.parse(JSON.stringify(props.config.layers));
const initialLayers = layersConfig.map(layerConfig => {
return {
...layerConfig,
layer: new FeatureLayer({
url: layerConfig.url,
title: layerConfig.name,
visible: false
})
};
});
setLayers([...initialLayers]);
}
}, [props.config]);
Since I typecasted it into ImmutableObject, it added a bunch of other properties that obscured it and I suppose it wasn't identified as a layer, so I parsed it again!
know you say you found a solution, but I'm curious how you were storing that JSON file? Was it in config? or another file?
I've found that sometimes when you set it up in a separate sampleData.json file and do an import, that the object comes in as plain JS, and sometimes not only do you need to parse it, but you may need to have it go through a factory type of method in order create the structure with the proper types that the rest of your script is expecting.
IE: Json on disc is generic and doesn't map to types directly. you may have to convert generic to an actual JS Object of the type you use to reference the data in it. (If its an object, an array, an array of objects, or an object, with an array of other objects for example).