Experience Builder - Widget Not Adding Layers When Configured via JSON

311
6
Jump to solution
4 weeks ago
Labels (2)
Esprit3
New Contributor II

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:

Esprit3_0-1715627610779.png

 

 

0 Kudos
1 Solution

Accepted Solutions
Esprit3
New Contributor II

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!

View solution in original post

6 Replies
JeffreyThompson2
MVP Regular Contributor

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:

  1. Initial load - JimuMapView not ready. Nothing happens.
  2. 2nd render - JimuMapView ready. Layers is an empty array and the add layers function errors. The JSON is read and featureLayers created.
  3. 3rd render- The layers array is now populated, but the add layers function does not run.

Moving the add layers function within the same useEffect that reads the JSON should fix the issue.

GIS Developer
City of Arlington, Texas
Esprit3
New Contributor II

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;

 

 

 

0 Kudos
JeffreyThompson2
MVP Regular Contributor

Try changing your add layer line to this.

 //For adding the layers
      if (jimuMapView) {
        initialLayers.forEach(({ layer }) => jimuMapView.view.map.add(layer.layer));
      }
GIS Developer
City of Arlington, Texas
0 Kudos
Esprit3
New Contributor II

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:

Esprit3_1-1715627589702.png

 

 

0 Kudos
Esprit3
New Contributor II

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!

TimWestern
New Contributor III

 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).

0 Kudos