Extending Dédalo
How to add new capabilities to Dédalo v7 — and how to recognise that most "new" work is ontology authoring, not code.
See also: Ontology authoring · Component base classes · Sections · Areas · Creating new tools
This is the overview for extending Dédalo. It explains the one principle that governs every extension — ontology-first — gives you a decision guide for when you need code at all, and links to the per-typology cookbooks. Each cookbook is a focused step-by-step procedure; this page tells you which one you need and why.
The ontology-first principle
In Dédalo there is no schema file and no model registry to edit. The ontology is the schema, and the schema is data you author in the back office. A new record type, a new field on an existing section, a new menu area that reuses existing field models, a portal wired to a target section — all of these are created by adding or editing ontology nodes, never by writing code or SQL.
This works because the framework is model-driven. Component behavior lives in
horizontal engines (src/core/resolve/, src/core/relations/,
src/core/section/read.ts) that dispatch on the model string — not in a
class-per-model tree. Extension is therefore dispatch over a descriptor registry:
- Server resolution is the descriptor registry. A component
modelmaps to a small declarativedescriptor.tscollected insrc/core/components/registry.ts(getComponentModel(model)). The engines read the descriptor (which column it stores in, whether it is class-translatable, which relation resolver emits its rows) and do the work. Adding a model = addcomponent_<model>/descriptor.ts+ one line in the registry; the load-time integrity check inregistry.tsfails at boot on a malformed or dangling entry. - The node carries the
model. Each ontology node stores amodelvalue (section,component_input_text,area_admin,tool_export, …). The runtime reads it viagetModelByTipo(tipo)(src/core/ontology/resolver.ts) and surfaces it to the client asoptions.model/self.modelin the emitted context. - Client resolution is by prefix.
client/dedalo/core/common/js/instances.jsdynamically imports the ES module for a model by prefix:service_*→core/services/<model>/js/<model>.js,tool_*→ the tools root, default →core/<model>/js/<model>.js. The named export inside must equal the model exactly (export const component_email = function(){…}).
So the default for most extension tasks is: author a node, regenerate, done. Code only enters the picture when you need a new kind of behaviour that no existing model provides.
When do you need this? (ontology-only vs code)
Decide with one question: does an existing model already do what I need?
| You want to… | Existing model exists? | What you do |
|---|---|---|
| Add a record type (a "table") | Yes — section |
Ontology only. Author a section node + child component nodes. |
| Add a field to a section (text, number, date, picker, image, …) | Yes — component_* |
Ontology only. Author a component_<type> node under the section. |
| Group/lay out fields (tabs, groups) | Yes — section_tab, section_group, … |
Ontology only. Author grouper nodes (they store no data). |
| Add a back-office area that reuses existing fields | Yes — the generic area model plus menu nodes |
Ontology only, unless the area needs its own client JS/CSS. |
| Add a new field type with its own value shape, render and validation | No | Code: new component_<X> (a descriptor.ts + registry entry + engine wiring + client JS/CSS) and an ontology node. |
| Add a reusable client interaction (uploader, picker UI) hosted by a component | No | Code: new service_<X> (client-only JS), consumed by a host component. |
| Add a computed read-only display embedded in a host | No | Code: new widget — a client module under client/dedalo/core/widgets/<X>/ plus its server-side compute descriptor under src/core/components/component_info/widgets/<tld>/, hosted by a component_info. |
| Add an isolated block of UI/logic attached to a section/component/area | No | A tool — see its own guide: Creating new tools. |
The rule of thumb: new sections, fields, groupers and most areas are ontology-only. You write code only for a genuinely new component model, service, or widget — and tools have their own dedicated path.
Reuse before you build
Before authoring a new component model, check the components index: Dédalo ships text, number, date, email, IRI, JSON, geolocation, the media family (image, av, 3d, pdf, svg), and the relation family (select, check_box, radio_button, portal, dataframe, parent/children, …). A new model is rarely the answer.
File-layout conventions
The two sides of the seam have different shapes:
- Server (Bun/TS). A new component model is a per-model home under
src/core/components/component_<model>/— a declarativedescriptor.ts(and, for most, asamples/reference set) registered insrc/core/components/registry.ts. Its behavior is not in that folder; it is in the horizontal engines (resolve/,relations/,section/read.ts). New areas and sections need no server module at all — they are served by the generic engines (src/core/area/,src/core/section/read.ts). A new tool is a package undertools/<tool>/with aregister.jsonand aserver/index.tsexporting aToolServerModule. - Client (vanilla JS). Every model with a UI keeps its directory in the
client tree, resolved by directory name / prefix by
instances.js:
client/dedalo/core/<model>/ # or core/services/<model>/, core/widgets/<model>/, tools/<tool>/
├── js/
│ ├── <model>.js # client class; named export === <model>
│ ├── render_edit_<model>.js # per-mode render dispatchers
│ └── view_*.js # the actual DOM builders
├── css/<model>.less # bundled into page.css
└── img/icon.svg # where an icon is needed
The one server-side registry a new component must touch is its
descriptor.ts (column: 'string'|'relation'|'media'|'number'|'date'|'geo'|'iri'|'section_id'|'misc')
and its one line in registry.ts. That column is what
getColumnNameByModel() (src/core/ontology/resolver.ts) returns; omit it and
the engines have no matrix column to read/write. The model→column map is
decentralised — one column per descriptor, no central table to keep in sync.
A real scaffolder + template exists only for tools
(tools/tool_dev_template/ + scripts/create_tool.ts). For components,
sections, areas, services and widgets there is no generator: you copy an
existing sibling — a component descriptor, or a client model directory — and
add the ontology node (plus the descriptor + registry entry for a component).
The ontology JSON templates at core/ontology/templates/
(main_section_data.json, area_grouper_data.json, virtual_section_data.json)
are node-shape references, not generators.
Step-by-step (the universal checklist)
Whatever you extend, the procedure is the same skeleton — the cookbooks fill in the specifics:
- Confirm you need code. Run the table above. If an existing model fits, stop here and author the ontology node (steps 5–6 only).
- Decide what the value IS (code paths only). For a component this is the
most important decision — which matrix column it stores in and which engine
path emits it (literal / relation / info-widget). See
base classes and its decision guide.
Areas and sections need no per-model class; a tool exports a
ToolServerModule. - Create the home by copying a sibling. For a component: copy an existing
component_<model>/descriptor.tsand itssamples/. For a tool: runbun run scripts/create_tool.ts. For a client-facing model, copy the sibling directory in the client tree and rename the directory, the JS named export, andregister.json(tools). The engines find a component by its registry entry — no include edit. - Implement the halves. Server side: the
descriptor.ts+ registry line (component), or the tool'sserver/index.tsapiActions; areas/sections fall through to the generic engines. Then the clientjs/+css/. The horizontal engines emit the component's{context, data}— you never write a per-model server module for it. - Author the ontology node with the right
modelvalue,parent,term/lg-*label,order_number, and anyproperties/relations. See Ontology authoring. - Wire it into the tree. Sections need a
matrix_tableterm (or fall back to the sharedmatrix); areas need amenuentry; components are children of their section; services/widgets are hosted (rendered inline) by a component rather than placed as a data node. - Regenerate so the edit goes live. An ontology edit is not live until you
regenerate the compiled
dd_ontology(tools/tool_ontology_parser). See How changes apply live. - Add samples and a test (optional but recommended): a
samples/set beside the descriptor and abun:testundertest/(the descriptor equivalence is pinned bytest/unit/component_registry.test.ts).
Worked example — add a field, two ways
You want a "Wikidata QID" field on the Objects section. Two scenarios show the ontology-only / code split.
Scenario A — reuse an existing model (ontology only, no code)
A QID is just a short string. Reuse component_input_text:
- In the Ontology area, open the Objects
section node (e.g.
tch1) and create a child record. - Set
model = component_input_text,parent = tch1, the term{ "lg-eng": "Wikidata QID" }, anorder_number, andtranslatable = no. - Regenerate the TLD and clear caches.
The field now reads, writes, searches and exports — zero lines of code,
because component_input_text already provides all of it and its descriptor
already maps its column (src/core/components/component_input_text/descriptor.ts:
column: 'string').
Scenario B — a genuinely new model (code)
Suppose instead you need a validated QID field (must match ^Q[0-9]+$, links
out to wikidata.org). No existing model validates that, so you create
component_qid, mirroring component_email
(a literal-direct string stored in the string column):
src/core/components/component_qid/descriptor.ts— export oneComponentModel:{ model: 'component_qid', column: 'string' }(addclassSupportsTranslation: trueonly if its items are lang-filtered; a QID is language-neutral, so omit it). It stores its own literal string, so thestringcolumn is right; see the decision guide.- Register it: add the import + array entry in
src/core/components/registry.ts. The boot-time integrity check validates it. - Engine wiring for the bespoke behavior. A plain literal needs nothing
more — the generic literal branch of
emitDdoData(src/core/section/read.ts) reads thestringcolumn by descriptor. The validation (^Q[0-9]+$) lives on the write path (src/core/section/record/save_component.ts); add the check there if the value must be rejected server-side. - Client: copy
client/dedalo/core/component_email/toclient/dedalo/core/component_qid/, rename the directory, the JS named export (component_qid), the render/view files and the.lessbasename. - Author the ontology node with
model = component_qidunder the Objects section, then regenerate.
Then every future "QID field" anywhere is Scenario A again — author a node, no code.
Common pitfalls
- Forgetting the descriptor
column/ registry line. A new component with nocolumn(or not added tosrc/core/components/registry.ts) has no matrix column:getColumnNameByModel()returnsnulland the engines silently fail to read/write. This is the single non-convention edit — do not skip it. - Mismatched client export name.
instances.jsimportscore/<model>/js/<model>.jsand expectsexport const <model> = …. If the export name ≠ the model, the client import fails. - Hunting for a per-model server class. There is none. A component's
{context, data}is emitted by the horizontal engines; a component is declared by itsdescriptor.ts, not implemented as a class. - Skipping regenerate. Editing the ontology record updates the editable
layer only; the runtime reads the compiled
dd_ontologytable. Regenerate the TLD to make the edit live — see How changes apply live. - Picking the wrong storage column. Re-implementing datum load/save,
permissions or search means you mismodelled the value. Walk the
decision guide
top-down and stop at the first match; a relation stores in
relationand is emitted by a resolver, media inmedia, a literal in its typed column. - Treating a widget like a top-level model. A
core/widgets/widget has no ontology node of its own and no top-level model; its client module is hosted by acomponent_infofield and its server-side compute lives in thecomponent_infowidget framework (src/core/components/component_info/widgets/,computeInfoWidgets), not a class of its own.
Related
- Per-typology cookbooks (the step-by-step procedures):
- Add a component — a new field model (descriptor + registry + client JS/CSS)
- Add a section — a new record type (mostly ontology)
- Add an area — a new back-office area (ontology node + menu)
- Add a service — a reusable client interaction hosted by a component
- Add a widget — a computed display embedded in a host
- Creating new tools — tools have their own scaffolder and registration flow.
- Reference docs:
- Component base classes — the inheritance chain and the base-class decision guide.
- Sections — what a section is and how it is defined.
- Areas — the area inheritance model and every
area_*class. - Ontology authoring — the node shape,
propertiesgrammar, and the regenerate/cache-clear lifecycle.