Install
$ agentstack add skill-saschabrunnerch-arcgis-maps-sdk-js-ai-context-arcgis-popup-templates ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
ArcGIS Popup Templates
Use this skill for creating and customizing popup templates with various content types.
Import Patterns
Direct ESM Imports
import PopupTemplate from "@arcgis/core/PopupTemplate.js";
import CustomContent from "@arcgis/core/popup/content/CustomContent.js";
Dynamic Imports (CDN)
const PopupTemplate = await $arcgis.import("@arcgis/core/PopupTemplate.js");
const CustomContent = await $arcgis.import(
"@arcgis/core/popup/content/CustomContent.js",
);
> Note: The examples in this skill use Direct ESM imports. For CDN usage, replace import X from "path" with const X = await $arcgis.import("path").
PopupTemplate Overview
| Content Type | Purpose | | ------------------- | ------------------------- | | TextContent | HTML or plain text | | FieldsContent | Attribute table | | MediaContent | Charts and images | | AttachmentsContent | File attachments | | ExpressionContent | Arcade expression results | | CustomContent | Custom HTML/JavaScript | | RelationshipContent | Related records |
PopupTemplate Properties
| Property | Type | Description | | ------------------ | -------------------------------------- | --------------------------------------------- | | title | string \| Function \| object | Title with field substitution ({fieldName}) | | content | string \| Array \| Function \| Promise | Content definition | | fieldInfos | FieldInfo[] | Default field formatting | | expressionInfos | ExpressionInfo[] | Arcade expression definitions | | outFields | string[] | Fields to retrieve for popup | | actions | ActionButton[] \| ActionToggle[] | Custom action buttons | | overwriteActions | boolean | Replace default popup actions | | returnGeometry | boolean | Include geometry in popup results |
Basic PopupTemplate
layer.popupTemplate = {
title: "{name}",
content: "Population: {population}Area: {area} sq mi",
};
With Field Substitution
layer.popupTemplate = {
title: "{city_name}, {state}",
content: `
Demographics
Population: {population:NumberFormat(places: 0)}
Median Income: {median_income:NumberFormat(digitSeparator: true, places: 0)}
Founded: {founded_date:DateFormat(selector: 'date', datePattern: 'MMMM d, yyyy')}
`,
};
Content Array (Multiple Content Types)
layer.popupTemplate = {
title: "{name}",
content: [
{
type: "text",
text: "Overview{description}",
},
{
type: "fields",
fieldInfos: [
{ fieldName: "population", label: "Population" },
{ fieldName: "area", label: "Area (sq mi)" },
],
},
{
type: "media",
mediaInfos: [
{
type: "pie-chart",
title: "Demographics",
value: {
fields: ["white", "black", "asian", "other"],
},
},
],
},
],
};
Content Types
TextContent
{
type: "text",
text: `
{name}
{description}
Visit Website
`
}
FieldsContent
{
type: "fields",
fieldInfos: [
{
fieldName: "name",
label: "Name"
},
{
fieldName: "population",
label: "Population",
format: {
digitSeparator: true,
places: 0
}
},
{
fieldName: "date_created",
label: "Created",
format: {
dateFormat: "short-date"
}
}
]
}
Date Formats
short-date- 12/30/2024short-date-short-time- 12/30/2024, 3:30 PMshort-date-long-time- 12/30/2024, 3:30:45 PMlong-month-day-year- December 30, 2024day-short-month-year- 30 Dec 2024year- 2024
MediaContent
{
type: "media",
mediaInfos: [
{
title: "Sales by Quarter",
type: "column-chart", // bar-chart, pie-chart, line-chart, column-chart, image
value: {
fields: ["q1_sales", "q2_sales", "q3_sales", "q4_sales"],
normalizeField: "total_sales" // Optional
}
}
]
}
Chart Types
| Type | Use Case | | -------------- | ------------------------------------------ | | bar-chart | Horizontal bars for categorical comparison | | pie-chart | Proportional distribution | | line-chart | Trends over series | | column-chart | Vertical bars for comparison | | image | Display images from URL fields |
Image MediaInfo:
{
type: "image",
title: "Property Photo",
value: {
sourceURL: "{image_url}",
linkURL: "{detail_page_url}"
}
}
AttachmentsContent
{
type: "attachments",
displayType: "preview", // preview, list, auto
title: "Photos"
}
ExpressionContent
layer.popupTemplate = {
expressionInfos: [
{
name: "population-density",
title: "Population Density",
expression: "Round($feature.population / $feature.area, 2)",
},
{
name: "age-category",
title: "Age Category",
expression: `
var age = $feature.building_age;
if (age {
const div = document.createElement("div");
const graphic = event.graphic;
div.innerHTML = `
${graphic.attributes.name}
`;
return div;
},
});
layer.popupTemplate = {
title: "{name}",
content: [customContent],
};
RelationshipContent
{
type: "relationship",
relationshipId: 0,
title: "Related Inspections",
displayCount: 5,
orderByFields: [
{
field: "inspection_date",
order: "desc"
}
]
}
Popup Component
The `` component provides popup display control.
Key Properties:
| Property | Type | Description | | ---------------------------------- | ------------------------ | ------------------------------ | | actions | Collection | Custom action buttons | | content | string \| Node \| Widget | Popup content | | dock-options | object | Docking behavior configuration | | features | Graphic[] | Features to display | | heading | string | Popup heading text | | heading-level | number | Heading level (1-6) | | include-default-actions-disabled | boolean | Disable default zoom-to action | | initial-display-mode | string | Initial display mode | | location | Point | Popup anchor location | | open | boolean | Whether popup is open | | selected-feature | Graphic | Currently selected feature | | selected-feature-index | number | Index of selected feature |
Key Events:
| Event | Description | | --------------------- | ------------------------------------- | | arcgisTriggerAction | Fires when a custom action is clicked |
Actions
Add custom buttons to popups.
layer.popupTemplate = {
title: "{name}",
content: "...",
actions: [
{
id: "zoom-to",
title: "Zoom To",
className: "esri-icon-zoom-in-magnifying-glass",
},
{
id: "edit",
title: "Edit",
className: "esri-icon-edit",
},
],
};
// Handle action clicks using reactiveUtils
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
reactiveUtils.on(
() => view.popup,
"trigger-action",
(event) => {
if (event.action.id === "zoom-to") {
view.goTo(view.popup.selectedFeature);
} else if (event.action.id === "edit") {
startEditing(view.popup.selectedFeature);
}
},
);
Action Button Types
// Icon button
{ id: "info", title: "More Info", className: "esri-icon-description" }
// Toggle button
{ id: "highlight", title: "Highlight", type: "toggle", value: false }
Dynamic Content with Functions
Content as Function
layer.popupTemplate = {
title: "{name}",
outFields: ["*"],
content: (feature) => {
const attributes = feature.graphic.attributes;
if (attributes.type === "residential") {
return `
Residential Property
Bedrooms: ${attributes.bedrooms}
Bathrooms: ${attributes.bathrooms}
`;
} else {
return `
Commercial Property
Square Footage: ${attributes.sqft}
`;
}
},
};
Async Content Function
layer.popupTemplate = {
title: "{name}",
outFields: ["*"],
content: async (feature) => {
const id = feature.graphic.attributes.OBJECTID;
const response = await fetch(`/api/details/${id}`);
const data = await response.json();
return `
${data.title}
${data.description}
`;
},
};
Arcade Expressions
In Title
layer.popupTemplate = {
title: {
expression: `
var name = $feature.name;
var status = $feature.status;
return name + " (" + status + ")";
`,
},
content: "...",
};
Expression Infos in Fields
layer.popupTemplate = {
expressionInfos: [
{
name: "formatted-date",
title: "Formatted Date",
expression: 'Text($feature.created_date, "MMMM D, YYYY")',
},
{
name: "calculated-field",
title: "Density",
expression:
"Round($feature.population / AreaGeodetic($feature, 'square-miles'), 1)",
},
],
content: [
{
type: "fields",
fieldInfos: [
{ fieldName: "expression/formatted-date", label: "Created" },
{
fieldName: "expression/calculated-field",
label: "Population Density",
},
],
},
],
};
OutFields
layer.popupTemplate = {
title: "{name}",
content: "...",
outFields: ["name", "population", "area", "created_date"],
};
// All fields
layer.popupTemplate = {
title: "{name}",
content: "...",
outFields: ["*"],
};
Clustering Popups
layer.featureReduction = {
type: "cluster",
clusterRadius: 80,
popupTemplate: {
title: "Cluster of {cluster_count} features",
content: [
{
type: "fields",
fieldInfos: [
{
fieldName: "cluster_count",
label: "Features in cluster",
},
{
fieldName: "cluster_avg_population",
label: "Average Population",
format: { digitSeparator: true, places: 0 },
},
],
},
],
},
fields: [
{
name: "cluster_avg_population",
alias: "Average Population",
onStatisticField: "population",
statisticType: "avg",
},
],
};
Complete Example: Map Components
html,
body {
height: 100%;
margin: 0;
}
const FeatureLayer = await $arcgis.import(
"@arcgis/core/layers/FeatureLayer.js",
);
const mapElement = document.querySelector("arcgis-map");
const view = await mapElement.view;
await view.when();
const template = {
title: "Marriage in {NAME} Census Tract {TRACT}",
content: [
{
type: "fields",
fieldInfos: [
{
fieldName: "B12001_calc_pctMarriedE",
label: "Married %",
format: { digitSeparator: true, places: 1 },
},
{
fieldName: "B12001_calc_pctNeverE",
label: "Never Married %",
format: { digitSeparator: true, places: 1 },
},
],
},
],
};
const featureLayer = new FeatureLayer({
url: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/ACS_Marital_Status_Boundaries/FeatureServer/2",
popupTemplate: template,
});
mapElement.map.add(featureLayer);
Reference Samples
intro-popuptemplate- Basic PopupTemplate configurationget-started-popuptemplate- Getting started with PopupTemplatepopup-actions- Adding custom actions to popupspopup-custom-action- Custom popup actions with geometry operatorspopup-customcontent- Custom popup content elementspopuptemplate-arcade- Using Arcade expressions in popupspopuptemplate-arcade-expression-content- Arcade expression contentpopup-multipleelements- Multiple content elements in popupspopuptemplate-function- Function-based popup contentpopuptemplate-promise- Promise-based popup contentpopuptemplate-browse-related-records- Related records in popups
Common Pitfalls
- Field Names Case Sensitive: Field names must match exactly.
``javascript // If field is "Population" (capital P) content: "{Population}"; // Correct content: "{population}"; // Wrong - shows literal {population} ``
- OutFields Required: Fields used in popup must be in outFields when using function content.
``javascript popupTemplate: { title: "{name}", outFields: ["name", "description"], // Required for function content content: (feature) => { return feature.graphic.attributes.description; } } ``
- Expression Reference: Use
expression/prefix for Arcade expressions in fieldInfos.
``javascript fieldInfos: [{ fieldName: "expression/my-expression", label: "Calculated" }]; ``
- Async Content Must Return: Function content must return a value or Promise.
```javascript // Wrong - no return content: (feature) => { const div = document.createElement("div"); };
// Correct content: (feature) => { const div = document.createElement("div"); return div; }; ```
- GeoJSON Field Path: GeoJSON requires
properties/prefix for field names.
``javascript // GeoJSON title: "{properties/name}"; // Regular FeatureLayer title: "{name}"; ``
Related Skills
- See
arcgis-interactionfor hit testing and event handling. - See
arcgis-editingfor feature editing workflows. - See
arcgis-arcadefor detailed Arcade expression syntax.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: SaschaBrunnerCH
- Source: SaschaBrunnerCH/arcgis-maps-sdk-js-ai-context
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.