ArcGIS JS 基础教程(46):编辑要素属性 FeatureForm
零、写在前面
📌 本系列教程完整目录 :ArcGIS JS 系列基础教程(100个项目常用热门功能)
💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验
🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达
❤️ 一键三连:点赞 + 评论 + 收藏
一、功能介绍
<arcgis-feature-form> 是 FeatureForm 的 Web Component 形态 ,用于渲染一个要素的属性编辑表单。它根据 FormTemplate 自动生成输入框、下拉选择等控件,配合 applyEdits 即可把修改写回图层(客户端图层写回内存)。
本课要点:
- 用
FormTemplate定义可编辑字段,下拉字段用input: { type: "combo-box" }声明; - 把要素赋给
featureForm.feature,组件自动渲染表单; - 监听
arcgisSubmit事件,用featureForm.getValues()取回修改后的字段,再applyEdits({ updateFeatures })写回; - 点击场景要素 →
hitTest拾取 → 载入表单,实现「点哪改哪」。
参考:官方 FeatureForm | arcgis-feature-form 组件
💡 三维要点 :本例图层为客户端
FeatureLayer,applyEdits直接作用于内存source,无需后端服务即可完成「编辑闭环」。
二、功能实现
2.1 FormTemplate 定义可编辑字段
javascript
const formTemplate = new FormTemplate({
title: "编辑监测点属性",
elements: [
{ type: "field", fieldName: "NAME", label: "名称" },
{
type: "field", fieldName: "CATEGORY", label: "类别",
input: { type: "combo-box", options: [
{ label: "A类", value: "A类" },
{ label: "B类", value: "B类" },
{ label: "C类", value: "C类" }
]}
},
{
type: "field", fieldName: "STATUS", label: "运行状态",
input: { type: "combo-box", options: [
{ label: "正常", value: "正常" },
{ label: "预警", value: "预警" },
{ label: "故障", value: "故障" }
]}
},
{ type: "field", fieldName: "VALUE", label: "实时数值" }
]
});
const featureForm = document.getElementById("featureForm");
featureForm.formTemplate = formTemplate;
2.2 载入要素 / 提交保存
javascript
// 点击要素 → 载入表单
view.on("click", async (event) => {
const hit = await view.hitTest(event);
const graphic = (hit.results || []).find(r => r.type === "graphic" && r.layer === monitorLayer);
if (graphic) featureForm.feature = graphic.graphic;
});
// 提交:arcgisSubmit → 取回值 → applyEdits 写回
featureForm.addEventListener("arcgisSubmit", async () => {
const values = await featureForm.getValues();
const feature = featureForm.feature;
feature.attributes = { ...feature.attributes, ...values };
await monitorLayer.applyEdits({ updateFeatures: [feature] });
});
三、功能应用
| 应用场景 | 说明 |
|---|---|
| 属性编辑 | 表单化修改要素字段 |
| 下拉约束 | input: { type: "combo-box", options } 限定取值 |
| 点选编辑 | 拾取即载入,所见即所改 |
| 客户端闭环 | applyEdits 写回内存 source |
四、核心代码
📦 完整代码 已保存至
sample/lesson46_feature_form.html,可直接在浏览器打开。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>第46课:编辑要素属性 FeatureForm</title>
<link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css">
<script type="module" src="https://js.arcgis.com/5.0/"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", sans-serif; }
#mapContainer { width: 100vw; height: 100vh; }
.page-title {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
background: rgba(255,255,255,0.95); padding: 10px 24px; border-radius: 6px;
font-size: 18px; font-weight: bold; z-index: 100;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
/* 左侧:arcgis-feature-form 组件面板 */
.form-panel {
position: absolute; top: 80px; left: 20px;
width: 300px; max-height: 75vh; overflow: auto;
background: rgba(255,255,255,0.97); padding: 12px; border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
z-index: 100;
}
.form-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #333; }
#featureForm { width: 100%; }
.form-actions { margin-top: 10px; display: flex; gap: 8px; }
.form-actions button {
flex: 1; padding: 8px 0;
border: 1px solid #1890ff; border-radius: 4px;
background: #1890ff; color: #fff; cursor: pointer; font-size: 13px;
}
.form-actions button.ghost {
background: white; color: #1890ff;
}
.form-actions button:hover { opacity: 0.9; }
.control-panel {
position: absolute; top: 80px; right: 20px;
background: rgba(255,255,255,0.95); padding: 16px; border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
z-index: 100; min-width: 240px;
}
.control-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #333; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.btn-row button {
flex: 1; min-width: 100px; padding: 6px 0;
border: 1px solid #d9d9d9; border-radius: 4px;
background: white; cursor: pointer; font-size: 12px;
}
.btn-row button:hover { border-color: #1890ff; color: #1890ff; }
.btn-row button.on { background: #1890ff; color: white; border-color: #1890ff; }
.info-card {
margin-top: 10px; padding: 10px 12px;
background: #f0f5ff; border-radius: 6px;
border-left: 3px solid #1890ff; font-size: 12px; line-height: 1.8;
}
.info-card .val { font-weight: bold; color: #1890ff; }
.status-text {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 8px 20px;
border-radius: 20px; font-size: 13px; z-index: 100; pointer-events: none;
white-space: nowrap;
}
</style>
</head>
<body>
<h1 class="page-title">第46课:编辑要素属性 FeatureForm</h1>
<div class="form-panel">
<h3>✏️ 要素属性编辑(arcgis-feature-form)</h3>
<arcgis-feature-form id="featureForm"></arcgis-feature-form>
<div class="form-actions">
<button id="btnSave">💾 保存修改</button>
<button id="btnClear" class="ghost">清空</button>
</div>
</div>
<div class="control-panel">
<h3>🖱️ 操作</h3>
<div class="btn-row">
<button id="btnLoad">📥 载入首个要素</button>
<button id="btnPick" class="on">🎯 点击拾取编辑</button>
</div>
<div class="info-card">
<div>当前编辑:<span class="val" id="editName">未载入</span></div>
<div>保存次数:<span class="val" id="saveCount">0</span></div>
</div>
</div>
<div class="status-text" id="statusText">FeatureForm | 编辑要素属性并写回图层</div>
<div id="mapContainer"></div>
<script type="module">
const [Map, SceneView, FeatureLayer, FormTemplate, Graphic] = await $arcgis.import([
"@arcgis/core/Map.js",
"@arcgis/core/views/SceneView.js",
"@arcgis/core/layers/FeatureLayer.js",
"@arcgis/core/form/FormTemplate.js",
"@arcgis/core/Graphic.js"
]);
const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js");
const vecLayers = getTianditu.default({ type: "vec_w" });
const map = new Map({ basemap: { baseLayers: [vecLayers.base, vecLayers.anno] } });
const view = new SceneView({
container: "mapContainer", map: map,
camera: {
position: { longitude: 121.498, latitude: 31.205, z: 4000 },
heading: 20, tilt: 60
}
});
// ===== 监测点图层(可编辑客户端 FeatureLayer) =====
const monitorLayer = (() => {
const cats = ["A类", "B类", "C类"];
const colors = {
"A类": [227, 74, 51],
"B类": [56, 135, 220],
"C类": [60, 180, 110]
};
const statuses = ["正常", "预警", "故障"];
const graphics = [];
for (let i = 0; i < 90; i++) {
graphics.push(new Graphic({
geometry: {
type: "point",
x: 121.46 + Math.random() * 0.08,
y: 31.18 + Math.random() * 0.06,
z: 80 + Math.random() * 400,
spatialReference: { wkid: 4326 }
},
attributes: {
OBJECTID: i + 1,
NAME: "监测点" + (i + 1),
CATEGORY: cats[i % 3],
STATUS: statuses[Math.floor(Math.random() * 3)],
VALUE: (Math.random() * 100).toFixed(1)
}
}));
}
return new FeatureLayer({
source: graphics,
objectIdField: "OBJECTID",
fields: [
{ name: "OBJECTID", type: "oid" },
{ name: "NAME", type: "string" },
{ name: "CATEGORY", type: "string" },
{ name: "STATUS", type: "string" },
{ name: "VALUE", type: "double" }
],
geometryType: "point",
spatialReference: { wkid: 4326 },
title: "实时监测点",
elevationInfo: { mode: "relative-to-ground" },
renderer: {
type: "unique-value",
field: "CATEGORY",
uniqueValueInfos: cats.map(cat => ({
value: cat,
symbol: {
type: "point-3d",
symbolLayers: [{
type: "object",
resource: { primitive: "sphere" },
width: 220, height: 220, depth: 220,
material: { color: colors[cat] }
}]
}
}))
},
popupTemplate: {
title: "{NAME}",
content: [
{ type: "fields", fieldInfos: [
{ fieldName: "CATEGORY", label: "类别" },
{ fieldName: "STATUS", label: "运行状态" },
{ fieldName: "VALUE", label: "实时数值" }
]}
]
}
});
})();
map.add(monitorLayer);
// ===== FormTemplate:定义可编辑字段 =====
const formTemplate = new FormTemplate({
title: "编辑监测点属性",
elements: [
{ type: "field", fieldName: "NAME", label: "名称" },
{
type: "field", fieldName: "CATEGORY", label: "类别",
input: { type: "combo-box", options: [
{ label: "A类", value: "A类" },
{ label: "B类", value: "B类" },
{ label: "C类", value: "C类" }
]}
},
{
type: "field", fieldName: "STATUS", label: "运行状态",
input: { type: "combo-box", options: [
{ label: "正常", value: "正常" },
{ label: "预警", value: "预警" },
{ label: "故障", value: "故障" }
]}
},
{ type: "field", fieldName: "VALUE", label: "实时数值" }
]
});
// ===== arcgis-feature-form 组件 =====
const featureForm = document.getElementById("featureForm");
featureForm.formTemplate = formTemplate;
let pickMode = true;
let saveCount = 0;
function loadFeature(feature) {
featureForm.feature = feature; // 组件渲染表单
document.getElementById("editName").textContent =
feature.attributes.NAME || "要素";
setStatus("已载入:" + (feature.attributes.NAME || "要素"));
}
// 点击拾取要素载入表单
view.on("click", async (event) => {
if (!pickMode) return;
const hit = await view.hitTest(event);
const graphic = (hit.results || []).find(r => r.type === "graphic" && r.layer === monitorLayer);
if (graphic) loadFeature(graphic.graphic);
else setStatus("请点击监测点要素");
});
// ===== 提交保存(arcgisSubmit → applyEdits) =====
featureForm.addEventListener("arcgisSubmit", async () => {
try {
const values = await featureForm.getValues();
const feature = featureForm.feature;
if (!feature) return;
feature.attributes = { ...feature.attributes, ...values };
await monitorLayer.applyEdits({ updateFeatures: [feature] });
saveCount += 1;
document.getElementById("saveCount").textContent = saveCount;
setStatus("已保存:" + feature.attributes.NAME +
"(类别=" + feature.attributes.CATEGORY + ")");
} catch (e) {
console.warn("保存失败:", e.message);
setStatus("保存失败:" + e.message);
}
});
await view.when();
await monitorLayer.when();
view.goTo(monitorLayer.fullExtent, { duration: 1500 }).catch(() => {});
setStatus("FeatureForm 就绪 | 点击要素或载入首个要素进行编辑");
function setStatus(msg) { document.getElementById("statusText").textContent = msg; }
// 载入首个要素
document.getElementById("btnLoad").addEventListener("click", async () => {
const res = await monitorLayer.queryFeatures({
where: "1=1", outFields: ["*"], returnGeometry: false, num: 1
});
if (res.features[0]) loadFeature(res.features[0]);
});
// 保存(触发组件 submit,进而触发 arcgisSubmit)
document.getElementById("btnSave").addEventListener("click", () => {
if (!featureForm.feature) { setStatus("请先载入要素"); return; }
featureForm.submit();
});
// 清空
document.getElementById("btnClear").addEventListener("click", () => {
featureForm.feature = null;
document.getElementById("editName").textContent = "未载入";
setStatus("已清空表单");
});
// 拾取模式开关
document.getElementById("btnPick").addEventListener("click", function () {
pickMode = !pickMode;
this.classList.toggle("on", pickMode);
setStatus(pickMode ? "点击拾取已开启" : "点击拾取已关闭");
});
</script>
</body>
</html>
五、在线示例
🔗 在线体验 :https://southjor.github.io/arcgis-examples/lessons/lesson46.html

操作说明:
- 左侧为
<arcgis-feature-form>组件,按FormTemplate渲染名称、类别(下拉)、状态(下拉)、数值字段。- 「载入首个要素」或「点击场景中的监测点」把要素载入表单。
- 修改后点「保存修改」→
arcgisSubmit→getValues()→applyEdits({updateFeatures})写回图层。
六、关键 API 说明
| API | 说明 |
|---|---|
FormTemplate |
定义表单字段与编辑器类型 |
input: { type: "combo-box" } |
下拉选择编辑器(options 限定取值) |
arcgis-feature-form.feature |
赋要素即渲染表单 |
arcgisSubmit 事件 |
表单提交触发 |
featureForm.getValues() |
取回修改后的字段值 |
layer.applyEdits({updateFeatures}) |
写回图层(客户端写内存) |
参考链接: FeatureForm API
七、系列导航
⬅️ 上一篇 :ArcGIS JS 基础教程(45):鼠标悬停显示信息
➡️ 下一篇 :ArcGIS JS 基础教程(47):内置组件集成
💡 小贴士 :
<arcgis-feature-form>把「属性编辑」变成了声明式表单,你只需定义FormTemplate,下拉、校验、提交全部由组件接管。配合客户端FeatureLayer的applyEdits,无需任何后端即可跑通完整的编辑闭环。