Search code examples
webpackcdnblazorarcgis-js-apistimulusjs

How to include ArcGIS Javascript API via CDN with Webpack to work with Stimulus?


In the quick start guide for the ArcGIS javascript api, it has the following sample code:

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>ArcGIS API for JavaScript Hello World App</title>
  <style>
    html, body, #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
  </style>

  <link rel="stylesheet" href="https://js.arcgis.com/4.12/esri/css/main.css">
  <script src="https://js.arcgis.com/4.12/"></script>

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

      var map = new Map({
        basemap: "topo-vector"
      });

      var view = new MapView({
        container: "viewDiv",
        map: map,
        center: [-118.71511,34.09042],
        zoom: 11
      });

    });
  </script>
</head>
<body>
  <div id="viewDiv"></div>
</body>
</html>

Which works great for a simple web page. However, I'm using Blazor (server-side) and I would like to encapsulate the (above) code into a Blazor component. So I hit my first stumbling block - I'm not allowed to add a <script> tag inside a Blazor component. That's because the control could be created dynamically at any time. So I thought I'd address the issue with Stimulus instead.

Here's my Blazor component (so far). I have a file called Map.razor:

<div data-controller="map"></div>

@code {
    protected override bool ShouldRender()
    {
        var allowRefresh = false;

        return allowRefresh;
    }
}

I added the ShouldRender method so that the component will be rendered only once (when it's added). And here's what I'm trying to achieve in my Stimulus controller map-controller.js:

import { Controller } from "stimulus";

import EsriMap from "esri/Map";
import MapView from "esri/views/MapView";

export default class extends Controller {
    connect() {
        var map = new EsriMap({
            basemap: "topo-vector"
        });

        var view = new MapView({
            container: this.element,
            map: map,
            center: [-118.80500, 34.02700], // longitude, latitude
            zoom: 13
        });
    }
}

Originally, I tried adding ArcGIS so that it would be built using Webpack (so that the above code would work). However, I came across a compatibility issue between the ArcGIS javascript api and tailwindcss. ArcGIS would not compile because there was an issue with a call to require('fs').

Rather than workaround the require('fs') issue (which is outside my existing experience), I opted to pull in the ArcGIS js via the CDN. So I tried to setup ArcGIS using the external config feature in Webpack. Here's my webpack.js.config file:

const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const bundleFileName = 'holly';
const dirName = 'Holly/wwwroot/dist';

module.exports = (env, argv) => {
    return {
        mode: argv.mode === "production" ? "production" : "development",
        externals: {
            'esrimap': 'esri/Map',
            'mapview': 'esri/views/MapView'
        },
        entry: ['./Holly/wwwroot/js/app.js', './Holly/wwwroot/css/styles.css'],
        output: {
            filename: bundleFileName + '.js',
            path: path.resolve(__dirname, dirName),
            libraryTarget: "umd"
        },
        module: {
            rules: [{
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader'
                ]
            }]
        },
        plugins: [
            new MiniCssExtractPlugin({
                filename: bundleFileName + '.css'
            })
        ]
    };
};

And this is what I did in my stimulus controller:

import { Controller } from "stimulus";

import EsriMap from "esrimap";
import MapView from "mapview";

export default class extends Controller {
    connect() {
        var map = new EsriMap({
            basemap: "topo-vector"
        });

        var view = new MapView({
            container: this.element,
            map: map,
            center: [-118.80500, 34.02700], // longitude, latitude
            zoom: 13
        });
    }
}

However, I see the following exception in the browser:

TypeError: esrimap__WEBPACK_IMPORTED_MODULE_1___default.a is not a constructor
    at Controller.connect (map_controller.js:14)
    at Context.connect (context.ts:35)
    at Module.connectContextForScope (module.ts:40)
    at eval (router.ts:109)
    at Array.forEach (<anonymous>)
    at Router.connectModule (router.ts:109)
    at Router.loadDefinition (router.ts:60)
    at eval (application.ts:51)
    at Array.forEach (<anonymous>)
    at Application.load (application.ts:51)

As you might have guessed, I'm hitting the limit of my javascript/webpack knowledge. I did do a little research on the ArcGIS javascript API and whether it supports commonjs. Apparently it uses Dojo, which supports AMD. So I tried the following config instead:

        externals: [{
            'esrimap': {
                commonjs: 'esri/Map',
                commonjs2: 'esri/Map',
                amd: 'esri/Map'
            },
            'mapview': {
                commonjs: 'esri/views/MapView',
                commonjs2: 'esri/views/MapView',
                amd: 'esri/views/MapView'
            }
        }],

But I get the same error. I've read through the webpack documentation - it's not clear to me how I should configure this. Am I doing something fundamentally wrong?


Solution

  • So I thought I'd address the issue with Stimulus instead.

    The following is an answer to the question preceding the phrase above, ignoring everything that follows. An alternative solution that should work even though it's not an answer to your complete question.

    Create a script dedicated to your blazor component, and render it or reference it in your Pages/_host.cshtml or wwwroot/index.html:

    <script>
        window.myComponent = { 
          init: function(options) { 
            require([
              "esri/Map",
              "esri/views/MapView"
            ], function(Map, MapView) {
    
              var map = new Map({
                basemap: "topo-vector"
              });
    
              var view = new MapView({
                container: options.containerId,
                map: map,
                center: [-118.71511,34.09042],
                zoom: 11
              });
    
            }); // end require
    
           } // end init
    
         }; // end myComponent
      </script>
    

    And call this script in you component by overriding async Task OnAfterRenderAsync(bool isFirstRender). Only call it if isFirstRender is set to true.

    You can call the script by injecting IJSRuntime and calling await InvokeAsync("myComponent.init", "container-id")

    Something like this (untested)

    @inject IJSRuntime JSRuntime
    <div id="@containerId" data-controller="map"></div>
    
    @code {
        private string containerId = Guid.CreateGuid().ToString("n");
        protected override bool ShouldRender()
        {
            var allowRefresh = false;
    
            return allowRefresh;
        }
    
        protected override async Task OnAfterRenderAsync(bool isFirstRender) {
          if (isFirstRender){ 
             await JSRuntime.InvokeAsync("myComponent.init", new { containerId: containerId }) 
          }
        }
    }