Tools JS lifecycle
The client-side contract of a Dédalo tool — vanilla constructor-function/prototype JS, no build step. Base module: client/dedalo/core/tools_common/js/tool_common.js, served at /dedalo/core/tools_common/js/tool_common.js (see Roots & static serving in engineering/TOOLS_SPEC.md). Reference implementation: tools/tool_dev_template/js/.
Files
| File | Role |
|---|---|
js/index.js |
module entry; export * from './tool_x.js' |
js/tool_x.js |
tool constructor, wiring, custom logic |
js/render_tool_x.js |
render module: edit() (and/or list()) building the DOM |
css/tool_x.css |
tool stylesheet, loaded on demand (LESS/SASS sources welcome; the compiled file must keep this name) |
Wiring
wire_tool performs the standard prototype assignments every tool needs:
import {tool_common, wire_tool} from '../../../core/tools_common/js/tool_common.js'
import {render_tool_x} from './render_tool_x.js'
export const tool_x = function () { /* declare instance vars */ }
wire_tool(tool_x, render_tool_x)
// equals:
// tool_x.prototype.render = tool_common.prototype.render
// tool_x.prototype.destroy = common.prototype.destroy
// tool_x.prototype.refresh = common.prototype.refresh
// tool_x.prototype.edit = render_tool_x.prototype.edit (when defined)
// tool_x.prototype.list = render_tool_x.prototype.list (when defined)
Add further prototype methods after the call as usual.
Lifecycle: init → build → render
A tool is opened by open_tool({tool_context, caller, open_as}) (bound automatically to the tool buttons that the section/component tool filter — getElementTools, src/core/tools/registry.ts — placed in the element context).
- init(options) — call super first:
await tool_common.prototype.init.call(this, options). It assigns the common vars (model,section_tipo,section_id,lang,mode), resolves thecaller(directly, or reconstructed from the compressed URL when the tool opens in its own window) and thetool_config. When notool_config.ddo_mapis defined, a fallback ddo_map is built from the caller. - build(autoload) — call super: it awaits the tool CSS (
load_style) and resolves everyddo_mapentry into a live component/section instance inself.ar_instances. Pass{load_ddo_map: fn}as the third argument to replace the default loader. Locate your elements by role:const ddo = self.tool_config.ddo_map.find(el => el.role==='main_element') self.main_element = self.ar_instances.find(el => el.tipo===ddo.tipo) - render() — delegates to your
edit()/list()(matchingself.mode). The render must return a wrapper that exposes atool_headernode; build it with the helpers:const content_data = ui.tool.build_content_data(self) // ...append your nodes to content_data... const wrapper = ui.tool.build_wrapper_edit(self, { content_data }) return wrapper
Wrap custom init/build code in try/catch and set self.error = error on failure — tool_common.prototype.render then renders the standard error view instead of your edit(). Build/render exceptions inside the modal are also surfaced visibly to the user (standard content_data_error block).
ddo_map
tool_config.ddo_map declares the elements the tool operates on:
{
"ddo_map": [{
"model": "component_input_text",
"tipo": "rsc36",
"section_tipo": "rsc197",
"section_id": "self",
"lang": "lg-eng",
"role": "main_element",
"autoload": true,
"mode": "edit"
}]
}
"section_id": "self" is substituted with the caller's record id. "autoload": false entries are skipped by the default loader (load them yourself later). role is your own label for locating instances.
Calling the server: tool_request
const response = await self.tool_request({
action : 'my_method', // key of the server module's apiActions map
options : { section_tipo: '...', section_id: '...', my_param: 1 },
background : false, // true = detached run (the action must be in the module's backgroundRunnable)
url : null // optional API URL override
})
// response: { result, msg, errors }
The helper builds the full rqo (source via create_source, prevent_lock: true) and posts it through data_manager. Permission errors arrive as the standard permissions_denied response. For streaming (NDJSON) use data_manager.request_fetch_stream directly (see tool_export).
prevent_lock is vestigial
There is no per-session write-lock to release — request state is scoped per-request via AsyncLocalStorage (see engineering/REWRITE_SPEC.md), so a long tool request never blocks the user's other tabs. The field is still accepted by the RQO schema (src/core/concepts/rqo.ts) for wire compatibility with the unchanged client, but the dispatcher does not read it.
Modal and window modes
open_tool routes by open_as (from the call or the tool properties.open_as, default modal):
- modal — the tool renders inside a Dédalo modal; CSS is awaited before first paint; on close,
on_close_actions('modal')is called if you define it, otherwise the caller is refreshed. - window — the caller state is LZString-compressed into the URL and the tool opens at
/core/page/?tool={name}&raw_data=...; the caller refreshes on window focus. Size/position viaproperties.windowFeatures.
Labels
UI strings declared in register.json labels are retrieved with language fallback (current → default → any):
const text = self.get_tool_label('my_first_label') || 'Fallback text'
Events
Use the global event_manager for cross-component communication (e.g. upload_file_done_{id} from service_upload — see the template). Push subscriptions into self.events_tokens so destroy() unsubscribes them.
Assets and multi-root tools
Tool JS modules, CSS and icons resolve per tool: tools living in a DEDALO_ADDITIONAL_TOOLS root are loaded from their configured URL (DEDALO_TOOLS_URLS map / tool_base_url() util); in-repo tools keep the historical DEDALO_TOOLS_URL paths. This is transparent to tool code — never hardcode tool asset URLs; the framework builds them.