Using SDK classes in custom components

297
8
04-08-2024 06:29 AM
Rainald-Suchan
New Contributor II

Let's say I want to create a custom UI component / widget to add some functionality to a web app (build with the ArcGIS Maps SDK for JavaScript). And let's say I want to implement this as a standard Web Component (https://developer.mozilla.org/en-US/docs/Web/API/Web_components).
Then I create a JavaScript module and I write a class that extends HTMLElement. In that class I can build the UI of the component. That works without problems.
But in my custom component I also want to access the classes of the Maps SDK like the MapView and the Map. Maybe I want to create a GraphicsLayer and some Graphics in the component and add the layer to the map.
This leeds to some questions:
- What is the best way to import the classes of the Maps SDK in my module? Using the import statement or using the AMD style with require?
- What is the best way to get a reference to the mapView or map object defined in the main HTML / JavaScript file of the web app in my new JavaScript module? The new module is separate from the main JS file and so I don't have direct access to objects defined in this main JS file. Somehow I have to get the reference over to the module file. How can that be done?
Is there a sample showing how to do this?

0 Kudos
8 Replies
ReneRubalcava
Frequent Contributor

Here is a version of an app I have done before in various frameworks, but this one uses vanilla web components.

https://github.com/odoe/nearby-app-vanilla

  1. If you are using Vite, you can use arcgis/core to build your app and components.
  2. Not sure I understand reference to the mapView question, but you could expose a method to like `getMapView` or maybe even a property for when you use the component.

If you plan on building your components as your own package that you can use across various apps, do not build the JS SDK modules into your components. You want to treat them as external modules. You can do this by looking at the documentation for your build tools.

0 Kudos
RainaldSuchan5
New Contributor

Thanks for your example. It seems that it is a complete web app and the different components are not independent in this sample.
What I want to do is create a separate widget as a Web Component that is completely independent from the rest of the application. So someone else can just take this component and put it in his own web app.
In the easiest case you could just take one of the simple Esri examples from the Maps SDK documentation and put it in an HTML file. Then it should be possible to add the custom web component to this sample app just as you would add a legend widget. So the sample app has already a mapView object defined and it should be possible to access this mapView object insite of the new web component ( e. g. to call mapView.goTo(...)
If the web component is completely independant and is also used by others I think it is not a good idea to rely on special build system, Vite or something else. Some people don't use that but just write simple HTML and JavaScript files.

I'm not shure what you meen with the last part (treat the SDK modules as external modules).

0 Kudos
ReneRubalcava
Frequent Contributor

It would be the same thing minus the application part. You can expose the view or map on your components for users. You could even build a thin API of methods on your components to handle custom tasks, like zoom to graphics or turn layers on/off. You would probably want to use a build tool like Vite to bundle your components. With Vite you can set the maps sdk as an external dependency. I forget if it's directly in Vite config or as part of the rollup plugin part. But you don't want the maps sdk in your bundles, let the app do that.

There is also an option to use any of the web component compilers/frameworks out there like Stencil or Lit, even Svelte can compile to WC.

0 Kudos
Rainald-Suchan
New Contributor II

Ok, let's look at a simple example. I created a minimalistic app with a HTML file (index.html) and a JavaScript file (main.js).
And I created a simple web component that just adds a Graphic and a GraphicsLayer to the map and zooms to that location (MapComponent.js).

index.html

<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
    <title>Simple mapping app</title>
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>
    <link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css">
    <script type="module" src="./main.js"></script>
  </head>
  <body>
    <div id="viewDiv"></div>
  </body>
</html>

main.js

import MapView from 'https://js.arcgis.com/4.29/@arcgis/core/views/MapView.js';
import Map from 'https://js.arcgis.com/4.29/@arcgis/core/Map.js';
import MapComponent from "./MapComponent.js";

const map = new Map({
    basemap: "osm"
});

const view = new MapView({
    map: map,
    center: [-118.805, 34.027],
    zoom: 13,
    container: "viewDiv"
});

const mapComponent = document.createElement("map-component");
mapComponent.setView(view);
view.ui.add(mapComponent, "top-right");

MapComponent.js

import MapView from 'https://js.arcgis.com/4.29/@arcgis/core/views/MapView.js';
import Map from 'https://js.arcgis.com/4.29/@arcgis/core/Map.js';
import GraphicsLayer from 'https://js.arcgis.com/4.29/@arcgis/core/layers/GraphicsLayer.js';
import Graphic from 'https://js.arcgis.com/4.29/@arcgis/core/Graphic.js';

export default class MapComponent extends HTMLElement{

    constructor(){
        super();
        this.shadow = this.attachShadow({ mode: 'open' });
    }

    setView(view){
        this.view = view;
    }

    connectedCallback() {
        let goToButton = document.createElement("div");
        goToButton.setAttribute("style", "background-color:#d9dde0;padding:2px 6px;cursor:pointer");
        goToButton.textContent = "Add Graphic";
        this.shadow.appendChild(goToButton);
        goToButton.addEventListener("click", (function(){
            this.addGraphic();
        }).bind(this));
    }

    addGraphic(){
        let point = {
            type: "point",
            longitude: -71.2643,
            latitude: 42.0909
        };
        let markerSymbol = {
            type: "simple-marker",
            color: [226, 119, 40]
        };
        let graphic = new Graphic({
            geometry: point,
            symbol: markerSymbol
        });
        let graphicsLayer = new GraphicsLayer();
        graphicsLayer.add(graphic);
        this.view.map.add(graphicsLayer);
		this.view.goTo(graphic);
    }
}

customElements.define("map-component", MapComponent);

 

In the app (main.js) the mapView and map are created. The component is imported, added and the reference to the mapView of the application is passed to it with the setView method of the component. Is there perhaps a better way to pass the reference to the mapView to the web component? In the module of the component the classes of the maps SDK are imported from the CDN and not locally, because I don't know whether an existing web application that wants to use the component uses a local version of the maps SDK.
Is there anything fundamental that should be changed or improved in this example?

ReneRubalcava
Frequent Contributor

This is cool! I put it into a codepen, just to see how it felt going through the code. Nice work!

https://codepen.io/odoe/pen/bGJMJpV

This actually looks really good. If I were to do anything different, I might apply the css to your button via an embedded style tag in the shadow dom, but that is just a minor comment.

This is a useful pattern to write some web comps that you can still pass to the view ui.

If you want to keep this pattern, I would suggest using the AMD CDN, simply for perf purposes. You may also want to pass the class for GraphicsLayer to the component so that it's completely encapsulated, and the component doesn't reference any JS SDK modules directly, if that makes sense. This way it would work for quick apps that you test with the ESM CDN or when you use the AMD CDN.

Here is what I meant by encapsulating the JS SDK classes, this way the component is a little more portable.

https://codepen.io/odoe/pen/BaExgYb?editors=0010

0 Kudos
Rainald-Suchan
New Contributor II

Yes the style was just a quick solution for this mini example. In my real component I'm using a CSS file for the styles of the component.
One thing about the setView() method is that it works when you add the component in the JavaScript code like in my example. But if you add the component to your HTML with <map-component></map-component> the reference is not set. You would have to get the element in the JavaScript code and then call the setView() method. But I don't see a way that would work only in HTML.

I see the component as something like a widget. It is independent from the main application (which already has a mapView object). So the widget can be added to the app without knowing anything about the rest of the app. It's like the other widgets, e. g. the LayerList widget. You create an instance of the LayerList and pass a reference to the view object:

let layerList = new LayerList({
  view: view
});

I want to create a widget like this as a web component. So I keep it similar and have an encapsulated functionality that is only connected to the application by the mapView reference like the other widgets. In this way it can be used in any appication.

Is the AMD CDN the same as the ESM CDN? I thought it is different. How can I import the SDK classes with an ESM style import from the AMD CDN?
What would be for example the equivalent to

import Map from 'https://js.arcgis.com/4.29/@arcgis/core/Map.js';

This leeds to the next question. What if someone creates his web app with the AMD / require pattern as in the SDK examples?
And then he wants to use such a web component in his app (like the one of my example). I tested this but that wasn't working for me and I got several errors in the console of the dev tools of the browser. I think it is because of conflicts between the different ways of importing. The main application imports with require() from the AMD CDN and the component module imports with import from the ESM CDN. I'm not sure what the best way to avoid this is.

0 Kudos
ReneRubalcava
Frequent Contributor

The two CDNs are different. The AMD CDN can be used in production apps. We do recommend using ESM with arcgis/core npm package, but the AMD CDN is still viable for lots of folks. The ESM CDN is available for testing purposes, it's not optimized and will a lot of files.

There is an option for the AMD CDN to use a Promise based import, using $arcgis.import. There will be better doc on this with 4.30, but it's mentioned here.

https://developers.arcgis.com/javascript/latest/components-programming-patterns/#using-a-proxy

Ideally, if you want other users to use this component, that's why I suggested that the web component module not import any maps sdk modules on it's own. You can't mix AMD and ESM CDN.

Complex objects and classes can only be set via JS on web components, so you wouldn't be able to have an only HTML solution, you'd need some JS to set props. Attributes can only be strings/numbers/booleans.

0 Kudos
Rainald-Suchan
New Contributor II

Ok, the CDNs are different as I thougt and they can't be mixed.
So that means that you would need different versions of a web component (for AMD and ESM).
In fact you probably need 3 versions:
1) AMD (imports SDK classes from AMD CDN with require function)
2) ESM (imports SDK classes from local SDK with import @ArcGIS/core/...
3) ESM (imports SDK classes from ESM CDN with import https://js.arcgis.com/4.29/@arcgis/core/...
Then the user can take the appropriate version matching his web app.

I like the idea to set the imported SDK classes to the component class from the outsite. This way the component class is more independent.
I played around with the AMD version of my simple example and got it to work this way:

index.html

<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
    <title>Simple mapping app</title>
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>
    <link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css">
    <script src="https://js.arcgis.com/4.29/"></script>
	<script type="module" src="./main.js"></script>
  </head>
  <body>
    <div id="viewDiv"></div>
  </body>
</html>

main.js

import MapComponent from "./MapComponent.js";

require(["esri/Map", "esri/views/MapView"], function(Map, MapView) {

    const map = new Map({
        basemap: "osm"
    });

    const view = new MapView({
        map: map,
        center: [-118.805, 34.027],
        zoom: 13,
        container: "viewDiv"
    });

    const mapComponent = document.createElement("map-component");
    mapComponent.setView(view);
    view.ui.add(mapComponent, "top-right");
});

MapComponent.js

require(["esri/layers/GraphicsLayer", "esri/Graphic"], function(GraphicsLayer, Graphic) {
	MapComponent.GraphicsLayer = GraphicsLayer;
    MapComponent.Graphic = Graphic;
});

export default class MapComponent extends HTMLElement{
	static GraphicsLayer = null;
	static Graphic = null;

    constructor(){
        super();
        this.shadow = this.attachShadow({ mode: 'open' });
    }

    setView(view){
        this.view = view;
    }

    connectedCallback() {
        let goToButton = document.createElement("div");
        goToButton.setAttribute("style", "background-color:#d9dde0;padding:2px 6px;cursor:pointer");
        goToButton.textContent = "Add Graphic";
        this.shadow.appendChild(goToButton);
        goToButton.addEventListener("click", () => {
            this.addGraphic();
        });
    }

    addGraphic(){
        let point = {
            type: "point",
            longitude: -71.2643,
            latitude: 42.0909
        };
        let markerSymbol = {
            type: "simple-marker",
            color: [226, 119, 40]
        };
        if(this.view && MapComponent.Graphic && MapComponent.GraphicsLayer){
            let graphic = new MapComponent.Graphic({
                geometry: point,
                symbol: markerSymbol
            });
            let graphicsLayer = new MapComponent.GraphicsLayer();
            graphicsLayer.add(graphic);
            this.view.map.add(graphicsLayer);
            this.view.goTo(graphic);
        }
    }
}

customElements.define("map-component", MapComponent);

 

 

0 Kudos