5. ArcGIS JavaScript API 4.x Foundations & App Skeleton#
Global aims
Understand the 4.x mental model:
Map → View → Layers → Widgets → Popups.Stand up a clean 2D web map using AMD (
require()), then progressively add layers & UI.Know when to use FeatureLayer vs MapImageLayer vs GraphicsLayer.
Wire up popups (layer-based & graphic-based) and view constraints.
Leave with a working skeleton you can reuse for real apps.
5.1. 0) Project setup (AMD)#
What/Why
ArcGIS JS 4.x ships via CDN. The simplest start uses AMD
require()(no build tooling).For production you may move to ESM + bundlers, but AMD is perfect for learning.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ArcGIS JS 4.x Starter</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.23/esri/themes/light/main.css">
<style>
html, body, #viewDiv { height: 100%; margin: 0; padding: 0; }
</style>
<script src="https://js.arcgis.com/4.23/"></script>
</head>
<body>
<div id="viewDiv"></div>
<script>
require([
"esri/Map",
"esri/views/MapView"
], function (Map, MapView) {
const map = new Map({ basemap: "topo-vector" });
const view = new MapView({
container: "viewDiv",
map,
center: [-80.45, 38.75], // WV-ish
zoom: 7,
constraints: { minZoom: 6, maxZoom: 16 }
});
});
</script>
</body>
</html>
Hands-On
Switch basemap to
"hybrid"and"streets-navigation-vector".
map.basemap = "hybrid"; // or "streets-navigation-vector"
Change center/zoom to your city.
try
minZoom/maxZoomand observe behavior.
view.center = [-122.419, 37.774]; // San Francisco
view.zoom = 11;
view.constraints = { minZoom: 4, maxZoom: 19 };
5.2. 1) Core objects: Map & MapView + UI regions#
What/Why
Mapholds basemap + layer collection.MapViewrenders theMapin 2D and manages user interaction & the widget UI.The view has UI “corners” (
top-left,top-right, etc.) for tool placement.
Demo: add a containered UI panel
<div id="viewDiv"></div>
<div id="legendDiv" style="position:absolute; bottom:15px; right:15px; z-index: 5; background:#fff; padding:8px; border-radius:6px"></div>
You can later pass
container: "legendDiv"to widgets to render into this card.
5.2.1. Hands-On#
Create
#scaleDivand position itbottom-left.Add a custom “Help” button (
position: top-right) that justalert()s.
<!-- in body -->
<div id="scaleDiv" style="position:absolute;bottom:15px;left:15px;z-index:5;background:#fff;padding:6px;border-radius:6px"></div>
<button id="helpBtn" style="position:absolute;top:15px;right:15px;z-index:6">Help</button>
document.getElementById("helpBtn").addEventListener("click", () => {
alert("This is a demo help message.");
});
5.3. 2) Layer types & when to use them#
What/Why
FeatureLayer: vector features from a Feature Service. Client-side rendering, querying, popups, editing. Use for interactive, attribute-rich content.
MapImageLayer: server-drawn map images with sublayers (think ArcGIS Server map service). Great for big cartography/indices without client rendering. Sublayers can still have popups.
GraphicsLayer: ad-hoc client graphics you create at runtime (selection, sketches, transient overlays).
5.3.1. 2.1 FeatureLayer (client features, popups, queries)#
require([
"esri/Map", "esri/views/MapView",
"esri/layers/FeatureLayer", "esri/PopupTemplate"
], function(Map, MapView, FeatureLayer, PopupTemplate){
const map = new Map({ basemap: "topo-vector" });
const view = new MapView({ container: "viewDiv", map, center: [-80.45, 38.75], zoom: 7 });
const countyPopup = new PopupTemplate({
title: "{NAME} County",
content: `
<b>Population:</b> {POP}<br>
<b>Source:</b> {SOURCE}
`
});
const featureLayer = new FeatureLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
outFields: ["*"],
popupTemplate: countyPopup
});
map.add(featureLayer);
});
Hands-On
Add
definitionExpression(e.g.,POP > 50000) to filter server-side.Set
minScaleso counties only appear when zoomed out.
const counties = new FeatureLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
outFields: ["*"],
popupTemplate: { title: "{NAME} County", content: "POP: {POP2007}" },
definitionExpression: "POP2007 > 50000", // server filter
minScale: 0, // show any scale
maxScale: 4000000 // hide when zoomed in closer than ~1:4M
});
map.add(counties);
Tip: To change filter later:
counties.definitionExpression = "STATE_NAME = 'Texas'";
5.3.2. 2.2 MapImageLayer (server-drawn, fast, with sublayers)#
require(["esri/layers/MapImageLayer", "esri/PopupTemplate"], function(MapImageLayer, PopupTemplate){
const states = new MapImageLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer",
sublayers: [
{
id: 2, // States layer id
visible: true,
popupTemplate: new PopupTemplate({
title: "{STATE_NAME}",
content: "FIPS: {STATE_FIPS}"
})
}
],
opacity: 0.8
});
map.add(states);
});
Hands-On
Toggle a second sublayer at runtime; try
sublayer.visible = false/true.Change
opacityto blend with basemap.
const censusMIL = new MapImageLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer",
sublayers: [{ id: 2, visible: true }] // States
});
map.add(censusMIL);
// Toggle
const statesSL = censusMIL.sublayers.find(s => s.id === 2);
statesSL.visible = !statesSL.visible;
5.3.3. 2.3 GraphicsLayer (ad-hoc graphics like selection/highlight)#
require(["esri/layers/GraphicsLayer", "esri/Graphic"], function(GraphicsLayer, Graphic){
const selectionLayer = new GraphicsLayer({ id: "selection" });
map.add(selectionLayer);
// Add a temporary polygon graphic with a symbol
const poly = {
type: "polygon",
rings: [[-81,38],[-80,38],[-80,39],[-81,39],[-81,38]],
spatialReference: { wkid: 4326 }
};
const symbol = {
type: "simple-fill",
color: [255,255,0,0.3],
outline: { color: [255,0,0,1], width: 2 }
};
selectionLayer.add(new Graphic({ geometry: poly, symbol, attributes: { kind: "temp" } }));
});
Hands-On
Add a click handler to remove all graphics from
selectionLayer.Try changing the symbol on
mouseovervsmouseout.
// Clear on click (anywhere)
view.on("click", () => selectionLayer.removeAll());
5.4. 3) Widgets & UX (Home, ScaleBar, Legend, BasemapGallery)#
What/Why
Widgets give you polished UI quickly. Most take
viewand optional container.
Demo: add four common widgets
require([
"esri/widgets/Home",
"esri/widgets/ScaleBar",
"esri/widgets/Legend",
"esri/widgets/BasemapGallery"
], function(Home, ScaleBar, Legend, BasemapGallery){
const home = new Home({ view });
view.ui.add(home, "top-left");
const scaleBar = new ScaleBar({ view, unit: "dual" });
view.ui.add(scaleBar, { position: "bottom-left" });
const legend = new Legend({ view, container: "legendDiv" }); // render into our card
// view.ui.add(legend, "bottom-right"); // alternative if not using container
const basemapGallery = new BasemapGallery({ view });
view.ui.add(basemapGallery, "top-right");
});
Hands-On
Move
BasemapGalleryinto a custom floating card (likelegendDiv).
<div id="basemapCard" style="position:absolute;top:60px;right:15px;background:#fff;padding:8px;border-radius:6px;z-index:5"></div>
const basemapGallery = new BasemapGallery({ view, container: "basemapCard" });
Hide
Legendon small screens (view.widthBreakpoint) for responsiveness.
const legend = new Legend({ view });
view.ui.add(legend, "bottom-right");
view.watch("widthBreakpoint", bp => {
legend.visible = (bp !== "xsmall"); // hide when extra-small
});
5.5. 4) Popups 101 (FeatureLayer vs GraphicsLayer)#
What/Why
PopupTemplateformats content (HTMLallowed).FeatureLayer popups show when you click its features (if
popupEnabled !== false).GraphicsLayer needs graphics to carry
popupTemplate(or set at the layer in 4.30+); otherwise, nothing shows by default.
Demo: feature popup
const states = new FeatureLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/2",
outFields: ["*"],
popupTemplate: { title: "{STATE_NAME}", content: "FIPS: {STATE_FIPS}" }
});
map.add(states);
Demo: graphic popup
require(["esri/Graphic"], function(Graphic){
const g = new Graphic({
geometry: { type: "point", longitude: -80.5, latitude: 38.7 },
symbol: { type: "simple-marker", size: 8 },
attributes: { label: "Hello WV" },
popupTemplate: { title: "Marker", content: "{label}" }
});
selectionLayer.add(g);
});
Hands-On
Create a polygon graphic with attributes and a popup listing area/perimeter you compute on the fly (just store precomputed values in
attributesfor now).A) Geodesic (no projection step, great default)
require(["esri/geometry/geometryEngine", "esri/Graphic"], function(geometryEngine, Graphic){
const poly = {
type: "polygon",
rings: [[-80.6,38.6],[-80.2,38.6],[-80.2,38.9],[-80.6,38.9],[-80.6,38.6]],
spatialReference: { wkid: 4326 }
};
// Accurate on the ellipsoid
const area_km2 = geometryEngine.geodesicArea(poly, "square-kilometers");
const perimeter_km = geometryEngine.geodesicLength(geometryEngine.boundary(poly), "kilometers");
const attrs = {
name: "Demo AOI",
area_km2: Number(area_km2).toFixed(3),
perimeter_km: Number(perimeter_km).toFixed(3)
};
selectionGL.add(new Graphic({
geometry: poly,
symbol: { type: "simple-fill", color:[0,0,0,0.1], outline:{ color:[0,0,0,1], width:1.5 }},
attributes: attrs,
popupTemplate: {
title: "{name}",
content: "Area: {area_km2} km²<br>Perimeter: {perimeter_km} km"
}
}));
});
B) Projected (equal-area/UTM for planar measurements)
If you want planar area/length (e.g., in an equal-area projection):
require([
"esri/geometry/geometryEngine",
"esri/geometry/projection",
"esri/geometry/SpatialReference",
"esri/Graphic"
], function(geometryEngine, projection, SpatialReference, Graphic){
const poly = {
type: "polygon",
rings: [[-80.6,38.6],[-80.2,38.6],[-80.2,38.9],[-80.6,38.9],[-80.6,38.6]],
spatialReference: { wkid: 4326 }
};
// Choose an equal-area SR for the U.S. (NAD83 / Conus Albers)
const AEA = new SpatialReference({ wkid: 5070 }); // EPSG:5070
projection.load().then(() => {
const polyAEA = projection.project(poly, AEA);
const area_km2 = geometryEngine.planarArea(polyAEA, "square-kilometers");
const perim_km = geometryEngine.planarLength(geometryEngine.boundary(polyAEA), "kilometers");
const attrs = {
name: "Demo AOI (AEA)",
area_km2: Number(area_km2).toFixed(3),
perimeter_km: Number(perim_km).toFixed(3)
};
selectionGL.add(new Graphic({
geometry: poly, // keep original for display; metrics come from projected geometry
symbol: { type: "simple-fill", color:[0,0,0,0.1], outline:{ color:[0,0,0,1], width:1.5 }},
attributes: attrs,
popupTemplate: {
title: "{name}",
content: "Area: {area_km2} km²<br>Perimeter: {perimeter_km} km"
}
}));
});
});
Disable popups on a layer:
featureLayer.popupEnabled = false, confirm others still work.
counties.popupEnabled = false;
5.7. 6) Putting it together — a clean app skeleton#
What/Why
A tidy lifecycle helps scale:
init() → createMap() → createLayers() → createWidgets() → wireEvents().
Demo (single file skeleton)
<script>
require([
"esri/Map","esri/views/MapView",
"esri/layers/FeatureLayer","esri/layers/MapImageLayer","esri/layers/GraphicsLayer",
"esri/widgets/Home","esri/widgets/ScaleBar","esri/widgets/Legend","esri/widgets/BasemapGallery",
"esri/PopupTemplate"
], function(Map, MapView, FeatureLayer, MapImageLayer, GraphicsLayer,
Home, ScaleBar, Legend, BasemapGallery, PopupTemplate){
const App = {
map:null, view:null,
layers:{}, ui:{},
init(){
this.createMap();
this.createLayers();
this.createWidgets();
this.wireEvents();
},
createMap(){
this.map = new Map({ basemap: "topo-vector" });
this.view = new MapView({
container: "viewDiv",
map: this.map,
center: [-80.45, 38.75],
zoom: 7,
constraints: { minZoom: 6, maxZoom: 16 }
});
},
createLayers(){
this.layers.selection = new GraphicsLayer({ id:"selection" });
this.map.add(this.layers.selection);
this.layers.feature = new FeatureLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
outFields: ["*"],
popupTemplate: new PopupTemplate({
title: "{NAME} County",
content: "POP: {POP2007}"
})
});
this.map.add(this.layers.feature);
this.layers.mapImage = new MapImageLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer",
sublayers: [{ id: 2, visible: true }]
});
this.map.add(this.layers.mapImage);
},
createWidgets(){
this.ui.home = new Home({ view: this.view });
this.view.ui.add(this.ui.home, "top-left");
this.ui.scaleBar = new ScaleBar({ view: this.view, unit: "dual" });
this.view.ui.add(this.ui.scaleBar, "bottom-left");
this.ui.legend = new Legend({ view: this.view, container: "legendDiv" });
this.ui.basemapGallery = new BasemapGallery({ view: this.view });
this.view.ui.add(this.ui.basemapGallery, "top-right");
},
wireEvents(){
// simple demo: click to log features under cursor
this.view.on("click", async (evt)=>{
const hit = await this.view.hitTest(evt);
console.log("HitTest results:", hit.results);
});
}
};
App.init();
window.App = App;
});
</script>
Hands-On
Ensure the Legend shows both the FeatureLayer and the MapImage sublayer (tip:
Legendreads visible layers).
const legend = new Legend({ view });
view.ui.add(legend, "bottom-right");
// Legend automatically lists visible layers (FeatureLayer + visible MapImage sublayers)
Add a toggle button that sets
this.layers.mapImage.sublayers.items[0].visible = !visible.
const firstSL = censusMIL.sublayers.items[0];
firstSL.visible = !firstSL.visible;
5.8. 7) Popups: best practices & pitfalls (quick checklist)#
Always set
outFieldsor use"*"duringdev; trim to necessary fields for perf.For GraphicsLayer, assign
popupTemplateto each graphic (4.23).Use layer order to control which thing is clicked first (
map.reorder).Use links responsibly:
target="_blank"for external resources.Keep HTML concise—avoid heavy inline CSS for performance/accessibility.
Hands-On
Add a second FeatureLayer with overlapping features; reorder to see hit priority change.
Demonstrate that disabling
popupEnabledon the lower layer removes its popup but keeps the top one working.
5.9. Hands-On Lab (capstone for Class 1)#
5.9.1. Lab A — Build the base app#
Create the AMD page (CDN includes).
Add
Map,MapView, constraints, custom background card (#legendDiv).Add one FeatureLayer (counties) with a
PopupTemplate.
Success criteria: You can click counties and see the popup.
const baseMap = new Map({ basemap: "topo-vector" });
const baseView = new MapView({ container:"viewDiv", map:baseMap, center:[-80.45,38.75], zoom:7 });
const countiesA = new FeatureLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3",
outFields: ["*"],
popupTemplate: { title: "{NAME} County", content: "Population (2007): {POP2007}" }
});
baseMap.add(countiesA);
5.9.2. Lab B — Add a MapImageLayer with one sublayer + popup#
Add a
MapImageLayercensus service with States sublayer.Give the sublayer a
PopupTemplate(title + small content).Verify it appears in the
legendandpopupswork when clicking states outside county coverage (or toggle county visibility).
Success criteria: State popups work; toggling visibility makes behavior intuitive.
const censusB = new MapImageLayer({
url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer",
sublayers: [{
id: 2, visible: true,
popupTemplate: { title: "{STATE_NAME}", content: "FIPS: {STATE_FIPS}" }
}]
});
map.add(censusB);