Agent Skills: ArcGIS Popup Templates

Configure rich popup content with text, fields, media, charts, attachments, and related records. Use for customizing feature popups with dynamic content, expressions, and interactive elements.

UncategorizedID: SaschaBrunnerCH/arcgis-maps-sdk-js-ai-context/arcgis-popup-templates

Install this agent skill to your local

pnpm dlx add-skill https://github.com/SaschaBrunnerCH/arcgis-maps-sdk-js-ai-context/tree/HEAD/contexts/5.0/skills/arcgis-popup-templates

Skill Files

Browse the full folder contents for arcgis-popup-templates.

Download Skill

Loading file tree…

contexts/5.0/skills/arcgis-popup-templates/SKILL.md

Skill Metadata

Name
arcgis-popup-templates
Description
Configure rich popup content with text, fields, media, charts, attachments, and related records.

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}<br>Area: {area} sq mi"
};

With Field Substitution

layer.popupTemplate = {
  title: "{city_name}, {state}",
  content: `
    <h3>Demographics</h3>
    <p>Population: {population:NumberFormat(places: 0)}</p>
    <p>Median Income: {median_income:NumberFormat(digitSeparator: true, places: 0)}</p>
    <p>Founded: {founded_date:DateFormat(selector: 'date', datePattern: 'MMMM d, yyyy')}</p>
  `
};

Content Array (Multiple Content Types)

layer.popupTemplate = {
  title: "{name}",
  content: [
    {
      type: "text",
      text: "<b>Overview</b><br>{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: `
    <div style="padding: 10px;">
      <h2>{name}</h2>
      <p>{description}</p>
      <a href="{website}" target="_blank">Visit Website</a>
    </div>
  `
}

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/2024
  • short-date-short-time - 12/30/2024, 3:30 PM
  • short-date-long-time - 12/30/2024, 3:30:45 PM
  • long-month-day-year - December 30, 2024
  • day-short-month-year - 30 Dec 2024
  • year - 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 < 25) return "New";
        if (age < 50) return "Moderate";
        return "Historic";
      `
    }
  ],
  content: [
    {
      type: "expression",
      expressionInfo: {
        name: "population-density"
      }
    }
  ]
};

CustomContent

import CustomContent from "@arcgis/core/popup/content/CustomContent.js";

const customContent = new CustomContent({
  outFields: ["*"],
  creator: (event) => {
    const div = document.createElement("div");
    const graphic = event.graphic;

    div.innerHTML = `
      <div class="custom-popup">
        <h3>${graphic.attributes.name}</h3>
        <canvas id="chart-${graphic.attributes.OBJECTID}"></canvas>
      </div>
    `;

    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 <arcgis-popup> 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 `
        <h3>Residential Property</h3>
        <p>Bedrooms: ${attributes.bedrooms}</p>
        <p>Bathrooms: ${attributes.bathrooms}</p>
      `;
    } else {
      return `
        <h3>Commercial Property</h3>
        <p>Square Footage: ${attributes.sqft}</p>
      `;
    }
  }
};

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 `
      <h3>${data.title}</h3>
      <p>${data.description}</p>
    `;
  }
};

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

<!DOCTYPE html>
<html>
<head>
  <script src="https://js.arcgis.com/5.0/"></script>
  <script type="module" src="https://js.arcgis.com/5.0/map-components/"></script>
  <style>
    html, body { height: 100%; margin: 0; }
  </style>
</head>
<body>
  <arcgis-map basemap="gray-vector" center="-73.95,40.70" zoom="11">
    <arcgis-zoom slot="top-left"></arcgis-zoom>
    <arcgis-legend slot="bottom-left"></arcgis-legend>
  </arcgis-map>

  <script type="module">
    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);
  </script>
</body>
</html>

Reference Samples

  • intro-popuptemplate - Basic PopupTemplate configuration
  • get-started-popuptemplate - Getting started with PopupTemplate
  • popup-actions - Adding custom actions to popups
  • popup-custom-action - Custom popup actions with geometry operators
  • popup-customcontent - Custom popup content elements
  • popuptemplate-arcade - Using Arcade expressions in popups
  • popuptemplate-arcade-expression-content - Arcade expression content
  • popup-multipleelements - Multiple content elements in popups
  • popuptemplate-function - Function-based popup content
  • popuptemplate-promise - Promise-based popup content
  • popuptemplate-browse-related-records - Related records in popups

Common Pitfalls

  1. Field Names Case Sensitive: Field names must match exactly.

    // If field is "Population" (capital P)
    content: "{Population}"  // Correct
    content: "{population}"  // Wrong - shows literal {population}
    
  2. OutFields Required: Fields used in popup must be in outFields when using function content.

    popupTemplate: {
      title: "{name}",
      outFields: ["name", "description"],  // Required for function content
      content: (feature) => {
        return feature.graphic.attributes.description;
      }
    }
    
  3. Expression Reference: Use expression/ prefix for Arcade expressions in fieldInfos.

    fieldInfos: [
      { fieldName: "expression/my-expression", label: "Calculated" }
    ]
    
  4. Async Content Must Return: Function content must return a value or Promise.

    // Wrong - no return
    content: (feature) => {
      const div = document.createElement("div");
    }
    
    // Correct
    content: (feature) => {
      const div = document.createElement("div");
      return div;
    }
    
  5. GeoJSON Field Path: GeoJSON requires properties/ prefix for field names.

    // GeoJSON
    title: "{properties/name}"
    // Regular FeatureLayer
    title: "{name}"
    

Related Skills

  • See arcgis-interaction for hit testing and event handling.
  • See arcgis-editing for feature editing workflows.
  • See arcgis-arcade for detailed Arcade expression syntax.