Scenario#
In an E10 EB form, some static page labels need to change with a browser field selection. For example, when the browser field selects “Construction Permit”, the default label should become “Construction Permit Number”.
The target element carries its own mapping:
<span class="666" data-施工许可证="施工许可证编号" data-营业执照="统一社会信用代码">Default label</span>The code reads the browser field display name, finds data-browserDisplayName on target elements, and replaces the visible text with that attribute value.
Key Points#
BROWSER_FIELD_DATA_KEYis the browser field data key, not a CSS id.TARGET_CLASSis the class of elements whose text should be updated.- For multi-select browser fields, the code first matches the full display name, then each comma-separated display item.
- After saving, E10 may refresh only the form DOM or form instance instead of the whole page.
- Each refresh tries to get the latest
formSdk; do not rely on an old instance forever. - The snippet combines field-change events, save actions,
MutationObserver, and polling to recover when labels are reset after save. - Before starting, it destroys the previous script instance to avoid duplicate timers and listeners.
Reusable Code#
/**
* E10 EB form ecode: dynamically change element text by browser-field display name.
*/
(function () {
var DEBUG = false;
var BROWSER_FIELD_DATA_KEY = "replace_with_browser_field_data_key";
var TARGET_CLASS = "666";
var POLL_INTERVAL = 800;
var APPLY_DELAY = 120;
var CHANGE_DELAY = 500;
var GLOBAL_STATE_KEY = "__dynamic_title_666_state__";
if (window[GLOBAL_STATE_KEY] && window[GLOBAL_STATE_KEY].destroy) {
window[GLOBAL_STATE_KEY].destroy();
}
var state = {
destroyed: false,
formSdk: null,
moduleKey: "",
formId: "",
dataId: "",
timerId: null,
pollId: null,
observer: null,
listener: null,
lastBoundSdk: null,
lastBoundField: "",
lastSaveActionSdk: null
};
window[GLOBAL_STATE_KEY] = state;
function trim(value) {
return String(value || "").replace(/^\s+|\s+$/g, "");
}
function debugLog(message, data) {
if (DEBUG && window.console && console.log) {
console.log("[dynamic-title]", message, data || "");
}
}
function getLatestFormSdk() {
var sdk = null;
try {
if (window.WeFormSDK && window.WeFormSDK.getWeFormInstance) {
if (state.moduleKey && state.formId && state.dataId) {
sdk = window.WeFormSDK.getWeFormInstance(state.moduleKey, state.formId, state.dataId);
}
if (!sdk && state.moduleKey && state.formId) {
sdk = window.WeFormSDK.getWeFormInstance(state.moduleKey, state.formId);
}
if (!sdk && state.moduleKey) {
sdk = window.WeFormSDK.getWeFormInstance(state.moduleKey);
}
if (!sdk) {
sdk = window.WeFormSDK.getWeFormInstance();
}
}
} catch (e) {
debugLog("Failed to get latest formSdk", e.message);
}
if (sdk) {
state.formSdk = sdk;
}
return sdk || state.formSdk;
}
function getFormSdkFromEvent(event) {
var detail = event && event.detail ? event.detail : {};
state.moduleKey = detail.module || detail.moduleKey || state.moduleKey;
state.formId = detail.formId || state.formId;
state.dataId = detail.dataId || state.dataId;
if (detail.formSdk) {
state.formSdk = detail.formSdk;
}
return state.formSdk || getLatestFormSdk();
}
function resolveBrowserField(formSdk) {
try {
return formSdk.convertFieldNameToId(BROWSER_FIELD_DATA_KEY, "main", true);
} catch (e) {
debugLog("Failed to convert browser data key to fieldMark", e.message);
return "";
}
}
function getBrowserShowName(formSdk, browserField) {
var showName = "";
try {
if (formSdk.getBrowserShowName) {
showName = trim(formSdk.getBrowserShowName(browserField, ","));
}
if (!showName && formSdk.getFieldValue) {
showName = trim(formSdk.getFieldValue(browserField));
}
} catch (e) {
debugLog("Failed to read browser display name", e.message);
}
return showName;
}
function addAttrName(list, attrName) {
if (attrName && list.indexOf(attrName) === -1) {
list.push(attrName);
}
}
function buildAttrNames(showName) {
var list = [];
var cleanName = trim(showName);
var parts = cleanName.split(",");
addAttrName(list, "data-" + cleanName);
for (var i = 0; i < parts.length; i++) {
addAttrName(list, "data-" + trim(parts[i]));
}
return list;
}
function findNewTitle(node, attrNames) {
for (var i = 0; i < attrNames.length; i++) {
var value = node.getAttribute(attrNames[i]);
if (value !== null && typeof value !== "undefined") {
return value;
}
}
return null;
}
function setNodeText(node, text) {
if (typeof node.innerText !== "undefined") {
if (node.innerText !== text) {
node.innerText = text;
}
return;
}
if (node.textContent !== text) {
node.textContent = text;
}
}
function applyNames(reason) {
if (state.destroyed) {
return;
}
var formSdk = getLatestFormSdk();
if (!formSdk) {
debugLog("No formSdk, skip", reason);
return;
}
var browserField = resolveBrowserField(formSdk);
if (!browserField) {
return;
}
var showName = getBrowserShowName(formSdk, browserField);
if (!showName) {
debugLog("Browser display name is empty, skip", reason);
return;
}
var attrNames = buildAttrNames(showName);
var nodes = document.getElementsByClassName(TARGET_CLASS);
debugLog("Apply dynamic title", {
reason: reason,
showName: showName,
attrNames: attrNames,
count: nodes.length
});
for (var i = 0; i < nodes.length; i++) {
var newTitle = findNewTitle(nodes[i], attrNames);
if (newTitle !== null) {
setNodeText(nodes[i], newTitle);
}
}
}
function scheduleApply(reason) {
if (state.destroyed) {
return;
}
if (state.timerId) {
clearTimeout(state.timerId);
}
state.timerId = setTimeout(function () {
applyNames(reason);
}, APPLY_DELAY);
}
function bindFieldChange(formSdk, browserField) {
if (!formSdk.bindFieldChangeEvent) {
return;
}
if (state.lastBoundSdk === formSdk && state.lastBoundField === browserField) {
return;
}
state.lastBoundSdk = formSdk;
state.lastBoundField = browserField;
formSdk.bindFieldChangeEvent(browserField, function () {
scheduleApply("field changed");
setTimeout(function () {
applyNames("field changed delayed refresh");
}, CHANGE_DELAY);
});
}
function bindSaveAction(formSdk) {
if (!formSdk.registerAction || !window.WeFormSDK || !window.WeFormSDK.ACTION_FORM_SAVE) {
return;
}
if (state.lastSaveActionSdk === formSdk) {
return;
}
state.lastSaveActionSdk = formSdk;
formSdk.registerAction(window.WeFormSDK.ACTION_FORM_SAVE, function () {
scheduleApply("form saved");
setTimeout(function () { applyNames("form saved 500ms refresh"); }, 500);
setTimeout(function () { applyNames("form saved 1500ms refresh"); }, 1500);
setTimeout(function () { applyNames("form saved 3000ms refresh"); }, 3000);
});
}
function ensureObserver() {
if (state.observer || !window.MutationObserver || !document.body) {
return;
}
state.observer = new MutationObserver(function () {
scheduleApply("DOM changed");
});
state.observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true
});
}
function ensurePoll() {
if (state.pollId) {
clearInterval(state.pollId);
}
state.pollId = setInterval(function () {
applyNames("poll");
}, POLL_INTERVAL);
}
function start(formSdk) {
if (state.destroyed) {
return;
}
state.formSdk = formSdk || getLatestFormSdk();
if (!state.formSdk) {
return;
}
var browserField = resolveBrowserField(state.formSdk);
if (!browserField) {
return;
}
bindFieldChange(state.formSdk, browserField);
bindSaveAction(state.formSdk);
ensureObserver();
ensurePoll();
applyNames("startup immediate refresh");
setTimeout(function () { applyNames("startup 500ms refresh"); }, 500);
setTimeout(function () { applyNames("startup 1500ms refresh"); }, 1500);
}
state.destroy = function () {
state.destroyed = true;
if (state.timerId) clearTimeout(state.timerId);
if (state.pollId) clearInterval(state.pollId);
if (state.observer) state.observer.disconnect();
if (state.listener) window.removeEventListener("onFormReady", state.listener);
};
state.listener = function (event) {
var formSdk = getFormSdkFromEvent(event);
if (formSdk) {
start(formSdk);
}
};
window.addEventListener("onFormReady", state.listener);
setTimeout(function () { start(getLatestFormSdk()); }, 300);
setTimeout(function () { applyNames("loaded 1200ms refresh"); }, 1200);
setTimeout(function () { applyNames("loaded 3000ms refresh"); }, 3000);
})();Usage#
- Set
BROWSER_FIELD_DATA_KEYto the browser field data key. - Set
TARGET_CLASSto the class used by target elements. - Add
data-browserDisplayName="new visible text"to target elements. - For multi-select browser fields, configure either the full display name or individual display-name items.
- Test field change, form save, and DOM refresh scenarios.
Notes#
- Class name
666works, but a semantic name likedynamic-titleis easier to maintain. MutationObservercurrently watchesdocument.body; on heavy pages, narrow it to the form container if possible.- Polling is a fallback for cases where E10 resets DOM after save but does not fire enough useful events.
- Keep
GLOBAL_STATE_KEYunique, otherwise different ecode snippets may destroy each other. - If the snippet should only affect one form instance, keep the precise
moduleKey/formId/dataIdinstance lookup logic.
Follow-up: ESB Mapping-driven Version#
The earlier version matches the browser field display name against data-* attributes on page elements. That works for quick validation, but the mapping is scattered across the page. The more stable follow-up is to maintain mappings in an ESB action flow. The frontend only loads the mapping, reads the certificate type option ID, and updates title text.
This version only handles dynamic renaming. It does not merge hidden rows, remove blank rows, or rearrange layout. Layout cleanup is riskier and should be debugged as a separate ecode snippet.
Mapping API#
POST /api/esb/server/event/triggerActionFlowAdditional note: this endpoint effectively calls an internal E10 API from the page to trigger a specific action flow. The action flow can start with a custom trigger component and end with the default end component; these two components roughly correspond to the request body and response body of the call. Do not guess the rest of the structure blindly; use action-flow trigger debugging to inspect the real input and output shape.
The frontend ecode only needs to pass customParams and esbFlowId. Mapping assembly, query logic, and returned data should live inside the action flow. The internal reference requires login: E10 action-flow trigger reference.
Minimal request body:
{
customParams: {
mainTable: {}
},
moduleSource: "#optional",
esbFlowId: "ESB_FLOW_ID"
}Read path:
response.actionData.responseData.customData.mainTable.detail3Example row:
[
{
"fieldname": ["varcharField1"],
"showname": "New field title",
"certtype": "CERT_TYPE_OPTION_ID"
}
]Field meaning:
| Field | Purpose |
|---|---|
certtype | Certificate-type browser option ID. |
fieldname | Original title text or field dataKey; may be an array. |
showname | New title text for the matched certificate type. |
Key Notes#
- The certificate type field is read from
certType. - Prefer
getBrowserOptionIdfor browser-field option IDs; do not rely on display-name matching for this version. - The real target is the title text under
.weapp-form-widget-internal-title--text span, not an inputplaceholder. - Restore titles changed by the previous run before applying the next certificate type, otherwise stale labels remain after switching.
getFieldInfois useful for locating business input cells, but not for directly locating title text.- E10 may refresh only the DOM or form instance after save, so use
formReady, delayed refreshes, and a lightweight watcher. - In multi-card pages,
getWeFormInstance()may return the active form. For the current single-form flow, the simplified approach is acceptable.
Minimal Stable Code#
(function () {
var FLOW_ID = "ESB_FLOW_ID";
var CERT_TYPE_DATA_KEY = "certType";
var STATE_KEY = "__hz_cert_mapping_rename_only__";
if (window[STATE_KEY] && window[STATE_KEY].destroy) {
window[STATE_KEY].destroy();
}
var state = {
destroyed: false,
formSdk: null,
mappings: [],
mappingLoaded: false,
timer: null,
watcher: null,
lastCertId: ""
};
window[STATE_KEY] = state;
function trim(value) {
return String(value || "").replace(/\u00a0/g, " ").replace(/^\s+|\s+$/g, "");
}
function sdk() {
var formSdk = null;
try {
if (window.WeFormSDK && window.WeFormSDK.getWeFormInstance) {
formSdk = window.WeFormSDK.getWeFormInstance();
}
} catch (e) {}
if (formSdk) {
state.formSdk = formSdk;
}
return formSdk || state.formSdk;
}
function fieldMark(dataKey) {
var formSdk = sdk();
if (!formSdk || !dataKey) {
return "";
}
try {
return formSdk.convertFieldNameToId(dataKey, "main", true);
} catch (e) {
return "";
}
}
function certId() {
var formSdk = sdk();
var mark = fieldMark(CERT_TYPE_DATA_KEY);
if (!formSdk || !mark) {
return "";
}
try {
return trim(formSdk.getBrowserOptionId(mark, ","));
} catch (e1) {
try {
return trim(formSdk.getFieldValue(mark));
} catch (e2) {
return "";
}
}
}
function normalizeMappings(rows) {
var list = [];
var i;
var j;
var row;
var names;
for (i = 0; rows && i < rows.length; i++) {
row = rows[i] || {};
names = row.fieldname;
if (typeof names === "string") {
names = [names];
}
for (j = 0; names && j < names.length; j++) {
if (trim(row.certtype) && trim(names[j])) {
list.push({
certtype: trim(row.certtype),
fieldname: trim(names[j]),
showname: trim(row.showname) || trim(names[j])
});
}
}
}
return list;
}
function loadMappings(callback) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/esb/server/event/triggerActionFlow", true);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onreadystatechange = function () {
var json;
var rows;
if (xhr.readyState !== 4) {
return;
}
try {
json = JSON.parse(xhr.responseText || "{}");
} catch (e) {
json = {};
}
rows =
json &&
json.actionData &&
json.actionData.responseData &&
json.actionData.responseData.customData &&
json.actionData.responseData.customData.mainTable &&
json.actionData.responseData.customData.mainTable.detail3;
state.mappings = normalizeMappings(rows || []);
state.mappingLoaded = true;
if (callback) {
callback();
}
};
xhr.send(JSON.stringify({
customParams: {
mainTable: {}
},
moduleSource: "#optional",
esbFlowId: FLOW_ID
}));
}
function originTitle(node) {
var text;
if (!node) {
return "";
}
text = trim(node.getAttribute("data-hz-origin-title"));
if (!text) {
text = trim(node.innerText || node.textContent);
node.setAttribute("data-hz-origin-title", text);
}
return text;
}
function restoreTitle() {
var nodes = document.querySelectorAll("[data-hz-renamed-title='1']");
var i;
var old;
for (i = 0; i < nodes.length; i++) {
old = nodes[i].getAttribute("data-hz-origin-title");
if (old) {
nodes[i].innerText = old;
}
nodes[i].removeAttribute("data-hz-renamed-title");
}
}
function setTitle(node, text) {
if (!node) {
return;
}
if (!node.getAttribute("data-hz-origin-title")) {
node.setAttribute("data-hz-origin-title", trim(node.innerText || node.textContent));
}
node.innerText = text;
node.setAttribute("data-hz-renamed-title", "1");
}
function applyRename() {
var id = certId();
var nodes = document.querySelectorAll(".weapp-form-widget-internal-title--text span");
var i;
var j;
var key;
var mapping;
restoreTitle();
if (!state.mappingLoaded || !id) {
return;
}
for (i = 0; i < nodes.length; i++) {
key = originTitle(nodes[i]);
for (j = 0; j < state.mappings.length; j++) {
mapping = state.mappings[j];
if (mapping.certtype === id && mapping.fieldname === key) {
setTitle(nodes[i], mapping.showname);
}
}
}
}
function schedule(delay) {
if (state.destroyed) {
return;
}
clearTimeout(state.timer);
state.timer = setTimeout(function () {
applyRename();
}, delay || 100);
}
function startWatcher() {
state.watcher = setInterval(function () {
var id;
if (state.destroyed) {
return;
}
id = certId();
if (id !== state.lastCertId) {
state.lastCertId = id;
schedule(80);
}
}, 300);
}
function boot() {
loadMappings(function () {
state.lastCertId = certId();
schedule(100);
setTimeout(function () {
schedule(0);
}, 600);
});
startWatcher();
}
state.destroy = function () {
state.destroyed = true;
clearTimeout(state.timer);
clearInterval(state.watcher);
restoreTitle();
};
try {
if (window.ebuilderSDK && window.ebuilderSDK.getPageSDK) {
window.ebuilderSDK.getPageSDK().on("formReady", function () {
boot();
});
} else {
boot();
}
} catch (e) {
boot();
}
})();Debug Order#
- Check whether the ESB API returns data.
- Check whether the response path is still
actionData.responseData.customData.mainTable.detail3. - Check whether
certTypereturns a browser option ID. - Check whether mapping
certtypeequals the current option ID. - Check whether the original title text equals mapping
fieldname. - Check whether the code modified the title text node instead of the input element.
Current Conclusion#
- Keep the ESB mapping-driven renaming approach.
- Match by certificate type option ID.
- Match mapping
fieldnameagainst the original title DOM text. - Store original titles in
data-hz-origin-title, then restore before renaming again. - Do not mix row hiding, blank removal, or layout rearrangement into this snippet.
General Debugging Addendum#
E10 ecode usually fails in one of these layers: execution timing, SDK instance, field lookup, API response, or DOM targeting. Debug them in layers instead of building the final feature all at once.
Layered Check Order#
- Check whether the console shows a script-start log.
- Check whether
formReadyoronFormReadyfires. - Check whether
window.WeFormSDK.getWeFormInstance()returns a form SDK. - Check whether
convertFieldNameToId("certType", "main", true)returns a fieldMark. - Check whether the browser field returns
getBrowserOptionId. - Check whether the ESB API returns mapping data and whether the response path is correct.
- Check whether
.weapp-form-widget-internal-title--text spanfinds title nodes. - Check whether the original title text equals mapping
fieldname. - Check whether save-time or partial refreshes require rebinding and delayed refreshes.
Syntax And Entry Points#
E10 JavaScript validation can be conservative. Use ES5-style code by default: var, normal function, and try/catch. Avoid optional chaining, arrow functions, const / let, JSX, classes, and heavy syntax sugar.
Minimal execution test:
(function () {
try {
console.log("[ecode-test] script start", new Date().toLocaleString());
} catch (e) {
console.log("[ecode-test] script error", e);
}
})();Form-ready test:
(function () {
try {
if (window.ebuilderSDK && window.ebuilderSDK.getPageSDK) {
window.ebuilderSDK.getPageSDK().on("formReady", function (args) {
console.log("[ecode-test] pageSdk formReady", args);
});
}
window.addEventListener("onFormReady", function (event) {
console.log("[ecode-test] window onFormReady", event && event.detail);
});
} catch (e) {
console.log("[ecode-test] formReady error", e);
}
})();SDK, Field, And Browser Value#
For a single-form page, start with the simple SDK getter:
var formSdk = window.WeFormSDK.getWeFormInstance();
var mark = formSdk.convertFieldNameToId("certType", "main", true);
console.log("[ecode-test] fieldMark", mark);
console.log("[ecode-test] getFieldValue", formSdk.getFieldValue(mark));
console.log("[ecode-test] getBrowserOptionId", formSdk.getBrowserOptionId(mark, ","));
console.log("[ecode-test] getBrowserShowName", formSdk.getBrowserShowName(mark, ","));Notes:
- A
dataKeyis not the field ID; main-table fields usually needconvertFieldNameToId(dataKey, "main", true). - Use
getBrowserOptionIdfor stable browser-field matching. - When a field-change event fires, the browser value may not be fully updated yet; read it again after a short delay.
- After save, linkage, or partial refresh, event bindings may be lost; make rebinding possible after
formReady.
API And DOM#
Test the API by itself before connecting it to renaming logic. Do not debug API, DOM, and layout hiding at the same time.
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/esb/server/event/triggerActionFlow", true);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return;
}
console.log("[ecode-test] status", xhr.status);
console.log("[ecode-test] responseText", xhr.responseText);
};Title-node check:
var nodes = document.querySelectorAll(".weapp-form-widget-internal-title--text span");
var i;
console.log("[ecode-test] title node count", nodes.length);
for (i = 0; i < nodes.length; i++) {
console.log("[ecode-test] title", i, nodes[i].innerText || nodes[i].textContent, nodes[i]);
}Notes:
- This feature changes title text nodes, not inputs or
placeholdertext. getFieldInfois better for locating business input cells, not direct title text.- If values are correct but the page does not change, check the DOM selector and target node first.
Save-time Reset And Multi-card Pages#
If labels work at first but reset after save, E10 probably repainted the form DOM partially. Run once after formReady, run after field changes, add delayed refreshes after save or partial refreshes, and optionally use a lightweight watcher that only compares the certificate type ID.
var lastId = "";
setInterval(function () {
var id = certId();
if (id !== lastId) {
lastId = id;
applyRename();
}
}, 300);Be careful on multi-card or multi-form pages: getWeFormInstance() may return the active form, and a reused global STATE_KEY may let a new instance destroy the old one. Without clear form isolation parameters, keep dynamic renaming simple and do not mix multi-card locking, row hiding, or layout rearrangement into it.
Keep Layout Hiding Separate#
Layout hiding is high risk. Do not scan all empty td elements and hide them, and do not start with rearrangement. If needed later, build it separately: use mapping to identify retained fields, locate business areas with getFieldInfo and title DOM nodes, operate only on the target table’s td.cell_Sheet1_x_x, and confirm row/column boundaries first.
Log Template#
function log(message, data) {
if (window.console && console.log) {
console.log("[hz-ecode-debug] " + message, data || "");
}
}Useful log points: script start, formSdk, fieldMark, current optionId, API response, mapping count, title-node count, matched field, and target node before mutation.
Pre-release Checklist#
- The code saves without JavaScript format errors.
- Initial page load renames titles correctly.
- Switching certificate type restores old titles and applies new titles.
- Save-time and partial refreshes still recover through delayed refresh.
- ESB API failures do not break the page; renaming simply skips.
- Empty mapping does not break the page.
- Opening another card on the same page does not obviously rename or restore the wrong form.
- Row hiding, blank cleanup, and dynamic renaming are not mixed into the same stable snippet.
