I am working with a non-hosted feature layer with a related table. The service is sitting on our REST endpoint, not on ArcGIS Online, because it contains data we consider sensitive. Because it is not hosted on AGOL, I am unable to use relationshipContent. Also, while I've been using the JavaScript API off and on for a while, I am still very much a beginner.
I ran across this sample earlier - PopupTemplate with promise | Sample Code | ArcGIS Maps SDK for JavaScript 4.26 | ArcGIS Developers. It meets a lot of my needs I believe, but I am unsure how to rework the code to pull from a related table. I believe I need to change the queryURL, but that may not even be doable.
Any guidance on reworking this code or suggestions on what I should consider instead?
Have you looked at the "Browse related records in a popup" sample?
I have, yes. We can't use that sample as a guide as the layer sits on our ArcGIS Server and not on ArcGIS Online.
I believe the closest I can get is actually using Query Related Records (Feature Service)—ArcGIS REST APIs | ArcGIS Developers. I have no experience with writing queries, so that's acting as another wall at the moment.
Do you have relationships set up in your service the way they are in the service used in that sample? If so, why wouldn't you be able to use your own services in a similar way?
I do.
But, per Esri, there is a limitation in ArcGIS Enterprise that doesn't allow it to work. It only works on hosted feature layers on AGOL. See the note here: RelationshipContent | API Reference | ArcGIS Maps SDK for JavaScript 4.26 | ArcGIS Developers
Ah, thanks for that clarification.
A while back, I wrote a script to return the related records for a feature using an older methodology. Maybe this will help
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="initial-scale=1, maximum-scale=1,user-scalable=no" />
<title>
Query Related Features | Sample | ArcGIS API for JavaScript 4.16
</title>
<style>
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
#gridDiv {
padding: 10px;
max-width: 500px;
}
#viewDiv {
height: 100%;
width: 100%;
}
#clearButton {
margin-top: 5px;
display: none;
}
.dgrid {
height: auto !important;
}
.dgrid .dgrid-scroller {
position: relative;
max-height: 200px;
overflow: auto;
}
.action {
color: blue;
cursor: pointer;
text-decoration: underline;
}
</style>
<link rel="stylesheet"
href="https://js.arcgis.com/4.16/dgrid/css/dgrid.css" />
<link rel="stylesheet"
href="https://js.arcgis.com/4.16/esri/themes/light/main.css" />
<script src="https://js.arcgis.com/4.16/"></script>
<script>
require([
"esri/Map",
"esri/Basemap",
"esri/views/MapView",
"esri/layers/FeatureLayer",
"esri/widgets/Legend",
"esri/widgets/Expand",
"dgrid/Grid",
"dojo/dom-style",
"dojo/dom-attr",
"dojo/on",
"dojo/dom"
], function (Map, Basemap, MapView, FeatureLayer, Legend, Expand, Grid, Portal, OAuthInfo, identityManager, domStyle, domAttr, on, dom) {
// Create the layer
let map, view;
let highlight, grid, clearbutton;
let layer = new FeatureLayer({
portalItem: {
id: "8be2866c3b1441b6beb5d79650198b05"
},
outFields: ["*"]
});
map = new Map({
basemap: 'oceans',
layers: [layer]
});
view = new MapView({
container: "viewDiv",
map: map,
center: [-153, 63],
zoom: 4,
popup: {
autoOpenEnabled: false
}
});
const legend = new Legend({ view: view });
// Expand widget to expand and contract the legend widget
const legendExpand = new Expand({
expandTooltip: "Show Legend",
expanded: false,
view: view,
content: legend
});
// Add widgets to the view
view.ui.add(document.getElementById("gridDiv"), "bottom-left");
view.ui.add(legendExpand, "top-right");
// Initialize variables
// call clearMap method when clear is clicked
clearbutton = document.getElementById("clearButton");
clearbutton.addEventListener("click", clearMap);
layer.load().then(function () {
return (g = new Grid());
});
view.on("click", function (event) {
clearMap();
queryFeatures(event);
});
function queryFeatures(screenPoint) {
const point = view.toMap(screenPoint);
// Query the for the feature ids where the user clicked
layer
.queryObjectIds({
geometry: point,
spatialRelationship: "intersects",
returnGeometry: false,
outFields: ["*"]
})
.then(function (objectIds) {
if (!objectIds.length) {
return;
}
// Highlight the area returned from the first query
view.whenLayerView(layer).then(function (layerView) {
if (highlight) {
highlight.remove();
}
highlight = layerView.highlight(objectIds);
});
// Query the for the related features for the features ids found
return layer.queryRelatedFeatures({
outFields: ["Source"],
relationshipId: layer.relationships[0].id,
objectIds: objectIds
});
})
.then(function (relatedFeatureSetByObjectId) {
if (!relatedFeatureSetByObjectId) {
return;
}
// Create a grid with the data
Object.keys(relatedFeatureSetByObjectId).forEach(function (
objectId
) {
// get the attributes of the FeatureSet
const relatedFeatureSet = relatedFeatureSetByObjectId[objectId];
const rows = relatedFeatureSet.features.map(function (feature) {
return feature.attributes;
});
if (!rows.length) {
return;
}
// create a new div for the grid of related features
// append to queryResults div inside of the gridDiv
const gridDiv = document.createElement("div");
const results = document.getElementById("queryResults");
results.appendChild(gridDiv);
// destroy current grid if exists
if (grid) {
grid.destroy();
}
// create new grid to hold the results of the query
grid = new Grid(
{
columns: Object.keys(rows[0]).map(function (fieldName) {
return {
label: fieldName,
field: fieldName,
sortable: true
};
})
},
gridDiv
);
// add the data to the grid
grid.renderArray(rows);
});
clearbutton.style.display = "inline";
})
.catch(function (error) {
console.error(error);
});
}
function clearMap() {
if (highlight) {
highlight.remove();
}
if (grid) {
grid.destroy();
}
clearbutton.style.display = "none";
}
});</script>
</head>
<body>
<div id="gridDiv" class="esri-widget">
<h2>Alaska Spatial Bibliography</h2>
<p>
Click on a region in the map to view the documents found in that region.
</p>
<div id="queryResults"></div>
<button id="clearButton" class="esri-widget">Clear Query</button>
</div>
<div id="viewDiv"></div>
</body>
</html>
Actually, we did run across that script and have been playing around with it. It works for us, but the map has three layers we need to toggle between, and we haven't been able to get it to work/focus on just the layer that is turned on. We also tried to rework it so it would open in a popup, but have not had success with that either.
Both of those issues are likely due to lack of experience with queries on our end.
If you gave us some code, it would be easier to help. Here is my take on your problem
const layer = map.getLayer("some ID") as FeatureLayer;
layer.queryRelatedFeatures(query).then(function (result) {
if (layer.id === "layer1")
// do something with layer1
else if (layer.id === "layer2")
// do something with layer2
else if (layer.id === "layer3")
// do something with layer3
}).catch(function (error) {
console.log("error from queryRelatedFeatures", error);
});
Adding a popup should be fairly easy https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Popup.html#open
Build the content from the result of the relatedQuery, send it to the view's popup as content, and open the popup.
Viktor,
We've taken the code that Ken provided and have reworked it so it works with our data. Here it is:
// Initialize variables
// This is the construct for telling the query which layer to pull from
const visibleLayer = counties;
let highlight, grid;
// call clearMap method when clear is clicked
const clearbutton = document.getElementById("clearButton");
clearbutton.addEventListener("click", clearMap);
view.on("click", function (event) {
clearMap();
queryFeatures(event);
});
function queryFeatures(screenPoint) {
const point = view.toMap(screenPoint);
// Query the for the feature ids where the user clicked
visibleLayer
.queryObjectIds({
geometry: point,
spatialRelationship: "intersects",
returnGeometry: false,
outFields: ["*"]
})
.then(function (objectIds) {
if (!objectIds.length) {
return;
}
// Highlight the area returned from the first query
view.whenLayerView(visibleLayer).then(function (layerView) {
if (highlight) {
highlight.remove();
}
highlight = layerView.highlight(objectIds);
});
// Query the for the related features for the features ids found
return visibleLayer.queryRelatedFeatures({
outFields: ["SpeciesGroup", "CommonName", "ProtectionLevels"],
relationshipId: visibleLayer.relationships[2].id,
objectIds: objectIds
});
})
.then(function (relatedFeatureSetByObjectId) {
if (!relatedFeatureSetByObjectId) {
return;
}
// Create a grid with the data
Object.keys(relatedFeatureSetByObjectId).forEach(function (
objectId
) {
// get the attributes of the FeatureSet
const relatedFeatureSet = relatedFeatureSetByObjectId[objectId];
const rows = relatedFeatureSet.features.map(function (feature) {
return feature.attributes;
});
if (!rows.length) {
return;
}
// create a new div for the grid of related features
// append to queryResults div inside of the gridDiv
const gridDiv = document.createElement("div");
const results = document.getElementById("queryResults");
results.appendChild(gridDiv);
// destroy current grid if exists
if (grid) {
grid.destroy();
}
// create new grid to hold the results of the query
grid = new Grid(
{
// create field aliases for query window
columns: Object.keys(rows[0]).map(function (fieldName) {
var fieldAliases = {
SpeciesGroup: "Species Group",
CommonName: "Common Name",
ProtectionLevels: "Protection Level"
};
return {
label: fieldAliases[fieldName] || fieldName,
field: fieldName,
sortable: true
};
})
},
gridDiv
);
// add the data to the grid
grid.renderArray(rows);
});
clearbutton.style.display = "inline";
})
.catch(function (error) {
console.error(error);
});
}
function clearMap() {
if (highlight) {
highlight.remove();
}
if (grid) {
grid.destroy();
}
clearbutton.style.display = "none";
}
I've never tried to write queries before within the API, so I have had a few meetings with our IT group to get their assistance with this issue. We gave them the same code block above, with the idea of working from it to move towards a popup. So far, this is the closest we've come up with, and it isn't behaving properly.
const visibleLayer3 = hex;
let highlight3, grid3;
// call clearMap method when clear is clicked
const clearbutton3 = document.getElementById("clearButton");
clearbutton3.addEventListener("click", clearMap);
view.on("click", function (event) {
clearMap();
queryFeatures(event);
});
function queryFeatures(screenPoint) {
var strResult = '';
//const strResult = document.getElementById("prpResults");
const point = view.toMap(screenPoint);
// Query the for the feature ids where the user clicked
visibleLayer3
.queryObjectIds({
geometry: point,
spatialRelationship: "intersects",
returnGeometry: false,
outFields: ["*"]
})
.then(function (objectIds) {
if (!objectIds.length) {
return;
}
// Highlight the area returned from the first query
view.whenLayerView(visibleLayer3).then(function (layerView) {
if (highlight3) {
highlight3.remove();
}
highlight3 = layerView.highlight3(objectIds);
});
// Query the for the related features for the features ids found
return visibleLayer3.queryRelatedFeatures({
outFields: ["SpeciesGroup", "CommonName", "ProtectionLevels"],
relationshipId: visibleLayer3.relationships[0].id,
objectIds: objectIds
});
})
.then(function (relatedFeatureSetByObjectId) {
if (!relatedFeatureSetByObjectId) {
return;
}
// Create a grid with the data
Object.keys(relatedFeatureSetByObjectId).forEach(function (
objectId
) {
// get the attributes of the FeatureSet
const relatedFeatureSet = relatedFeatureSetByObjectId[objectId];
const rows = relatedFeatureSet.features.map(function (feature) {
return feature.attributes;
});
if (!rows.length) {
return;
}
//**** Shane Code Beg ****
strResult = '<ul>';
//document.getElementById("prpResults").innerHTML = '<ul>';
//strResult.value = '<ul>';
var aobjRows = rows;
for (let z = 0; z < aobjRows.length; z++) {
let strCommonName = aobjRows[z].CommonName;
let strSpeciesGroup = aobjRows[z].SpeciesGroup;
let strProtectionLevel = aobjRows[z].ProtectionLevels;
//alert(aobjRows[z].CommonName);
strResult += '<li>' + strCommonName + ',' + strSpeciesGroup + ',' + strProtectionLevel + '<li>';
//strResult.value += '<li>' + strCommonName + ',' + strSpeciesGroup + ',' + strProtectionLevel + '<li>';
//document.getElementById("prpResults").innerHTML += '<li>' + strCommonName + ',' + strSpeciesGroup + ',' + strProtectionLevel + '<li>';
}
strResult += '</ul>';
//strResult.value += '</ul>';
//document.getElementById("prpResults").innerHTML += '</ul>';
//**** Shane Code End ****
//*** Begin Comment ***
// create a new div for the grid of related features
// append to queryResults div inside of the gridDiv
const gridDiv = document.createElement("div");
const results = document.getElementById("queryResults");
results.appendChild(gridDiv);
// destroy current grid if exists
if (grid3) {
grid3.destroy();
}
// create new grid to hold the results of the query
grid3 = new Grid(
{
// create field aliases for query window
columns: Object.keys(rows[0]).map(function (fieldName) {
var fieldAliases = {
SpeciesGroup: "Species Group",
CommonName: "Common Name",
ProtectionLevels: "Protection Level"
};
return {
label: fieldAliases[fieldName] || fieldName,
field: fieldName,
sortable: true
};
})
},
gridDiv
);
// add the data to the grid
grid3.renderArray(rows);
//**** End comment ****
});
alert(strResult);
//alert(document.getElementById("prpResults").value);
clearbutton3.style.display = "inline";
})
.catch(function (error) {
console.error(error);
});
alert(strResult);
//alert(document.getElementById("prpResults").innerHTML);
//alert(document.getElementById("prpResults"));
}
function clearMap() {
if (highlight3) {
highlight3.remove();
}
if (grid3) {
grid3.destroy();
}
clearbutton.style.display = "none";
}
This code isn't clean, but will give an idea the direction we are heading.