# @mikosoft/dodo — AI Reference (v1.3.0) Concise reference for AI assistants. Read this file to understand the Dodo framework without analysing source code. Full docs: http://dodo.mikosoft.info ## What is Dodo @mikosoft/dodo is a lightweight MVC JavaScript framework for building reactive single-page applications (SPAs). It has zero external dependencies, requires no build tools or TypeScript, and uses plain ES6 modules. Supported environments: browsers, browser extensions, Electron, Cordova/PhoneGap, PWAs. Key differences from React/Vue/Angular: - No components — one Controller manages the whole page - No virtual DOM — updates real DOM via dd-* directive processing - No JSX/templates — plain HTML files with dd-* attributes - Reactive model via JavaScript Proxy (not observables or signals) ## Architecture ``` URL change → Router → Controller runs lifecycle hooks → dd-* directives update DOM ``` Two app entry-point classes: | Class | File | Use case | |----------|-----------------------|-------------------------------------------| | App | core/App.js | Multi-page SPA with client-side routing | | AppOne | core/AppOne.js | Single-page, no routing (extensions etc.) | Supporting classes (inheritance chain, bottom → top): Controller → Model → Dd → DdCloners → DdListeners → Auxiliary ## Entry Point Setup ### Multi-page app (App) ```javascript import { App } from '@mikosoft/dodo'; const app = new App('myApp'); app .auth($auth) // Auth instance → this.$auth in controllers .httpClient($httpClient) // HTTP client → this.$httpClient in controllers .debug($debugOpts) // console debug flags .fridge($fridge) // shared object persisting across routes → this.$fridge .i18n($i18n) // translations → used by View.loadI18n(langCode) .preflight([fn1, fn2]) // run before every controller __loader() .postflight([fn1, fn2]) // run after every controller __postrend() .destroyflight(fn) // run when any controller is destroyed (route change) .ssr(); // dispatches 'ssr-ready' window event after first render const $routes = [ ['when', '/', HomeCtrl], ['when', '/users/:id', UserCtrl, { authGuards: ['isLogged'] }], ['when', '/login', LoginCtrl], ['redirect', '/home', '/'], // redirect /home → / ['do', [logFn]], // run on every route change ['notfound', NotfoundCtrl] ]; app.routes($routes).listen(); ``` ### Single-page app (AppOne) ```javascript import { AppOne } from '@mikosoft/dodo'; const app = new AppOne('myApp'); app.controller(MyCtrl); ``` ## Controller Lifecycle Every Controller runs hooks in this strict order: ``` __loader(trx) → __init(trx) → __rend(trx) → __postrend(trx) __destroy() fires when navigating away from the route ``` | Hook | Purpose | |----------------|---------------------------------------------------------------| | `__loader(trx)` | Load HTML views/partials into the DOM | | `__init(trx)` | Set initial values on `$model` and controller properties | | `__rend(trx)` | Call `render()` — processes all dd-* directives | | `__postrend(trx)` | Run code after render (fetch data, init 3rd-party plugins) | | `__destroy()` | Cleanup (remove event listeners, cancel timers) | The `trx` (transitional) object: ```javascript trx.uri // '/users/42' trx.params // { id: '42' } — route params from '/users/:id' trx.query // { sort: 'asc' } — query string params trx.pevent // popstate/pushstate browser event ``` Minimal controller: ```javascript import { Controller } from '@mikosoft/dodo'; class HomeCtrl extends Controller { async __loader(trx) { await this.loadView('#main', '/views/home.html'); } async __init(trx) { this.$model.title = 'Hello'; this.$model.users = []; } async __rend(trx) { await this.render(); } async __postrend(trx) { const resp = await this.$httpClient.get('/api/users'); this.$model.users = resp.data; // triggers reactive render('users') automatically } } export default HomeCtrl; ``` ## Reactive Model ($model) `this.$model` is a JavaScript Proxy defined on the Controller. Assigning any property automatically triggers a debounced, serialised `render(modelName)`. ```javascript this.$model.items = []; // next tick: render('items') fires this.$model.count = 42; // next tick: render('count') fires // Manual full render (called in __rend): await this.render(); // Manual targeted render (called anywhere after __init): await this.render('items'); // only re-renders dd-* elements bound to 'items' ``` Render is debounced per property name (rapid successive assignments = one render) and serialised globally (renders never run in parallel — prevents listener corruption). `$modeler` — helper methods auto-defined for each $model property: ```javascript this.$modeler.items.push(newItem); // push to array, then render('items') this.$modeler.items.unshift(item); this.$modeler.items.set(idx, val); this.$modeler.items.del(idx); ``` ## View System `this.loadView(viewName, viewContent, dest, cssSel, skipHideDirectives)` — injects HTML content into the element with `dd-view="viewName"`. Called in `__loader()`. ```javascript async __loader(trx) { this.loadView('#main', mainHtml); this.loadView('#sidebar', sidebarHtml); // skipHideDirectives: skip hiding + immediately process listed directives. // Use for static views whose dd-* expressions don't depend on $model data // (e.g. $fridge-based dd-class for active-link highlighting, dd-href navigation links). // This prevents the flash where links are hidden until __rend() completes after API calls. this.loadView('#sidebar', sidebarHtml, 'inner', '', ['dd-class', 'dd-href']); } ``` HTML files are plain HTML fragments (no `/` wrapper needed). `skipHideDirectives` parameter (5th arg, default `[]`): array of directive names to opt out of the hide-until-render mechanism. Elements matching those directives are inserted visible and the corresponding processors (`ddClass()`, `ddHref()`, …) are called immediately after inject, before `__init()` runs. Only safe for expressions that do not depend on `$model` data (e.g. `$fridge`, static values). `$model`-dependent `dd-class` / `dd-text` etc. on other views are unaffected and still hide+render on the normal `__rend()` cycle. `this.loadI18n(langCode)` — injects translation strings from the `$i18n` object into the view. Translation keys in HTML: `data-i18n="SOME_KEY"`. ## dd-* Directives Reference Directives are `dd-*` HTML attributes processed during `render()`. ### Value format rules (apply to almost all directives) ```html dd-text="firstName" dd-text="this.firstName" dd-text="$model.product.name" dd-text="printTxt('hello')" dd-text="((a + $model.b) * $fridge.x)" dd-text="(a > $model.b ? 'YES' : 'NO')" ``` Supported operators inside expressions: `! !== != === == > >= < <= && ||` **⚠️ IMPORTANT — No method chaining in dd-* expressions:** JavaScript method calls chained after an expression (e.g. `.toFixed()`, `.toUpperCase()`, `.slice()`) are NOT supported in `dd-*` attribute values. The expression parser's `^\(.+\)$` regex matches to the LAST `)` in the string, not the closing paren of the inner expression, causing the method name to be treated as an undefined variable and throwing a runtime error. ```html ``` **Rule: always precompute formatted/transformed values in the controller and bind the result to a `$model` property (or a computed property on each item). Use dd-* directives only to READ those properties or to evaluate simple arithmetic / boolean expressions.** Common modifier available on most directives: ```html dd-text="company.size --forceRender" ``` --- ### Writers **dd-text** — sets `textContent` (HTML tags are NOT interpreted) ```html

``` Modifier: `--pipe:stringMethod()` — applies any String method (slice, replace, toUpperCase…) **dd-html** — sets `innerHTML` (HTML tags ARE interpreted) ```html
``` --- ### Form value directives **dd-value** — sets form element value (one-way: model → view) ```html ``` **dd-model** — two-way binding: model → view AND view → model (fires on `input` event, triggers `render`) ```html ``` Modifier: `--convertType` — auto-converts string input to Number/Boolean/Object. **dd-set** — one-way: view → controller property on `input` event, does NOT trigger `render` ```html ``` --- ### Text attribute directives **dd-label** — sets `label` attribute ```html ``` **dd-placeholder** — sets `placeholder` attribute ```html ``` **dd-title** — sets `title` attribute ```html ``` **dd-data** — sets `data-*` attributes ```html
``` --- ### Conditionals **dd-if / dd-elseif / dd-else** — removes/inserts element from DOM ```html 2 5 not 2, not 5
...
...
``` `dd-else` takes no value. Siblings must be adjacent elements. **dd-visible** — toggles `visibility: visible|hidden` (element stays in DOM) ```html
...
...
``` --- ### Loops (Cloner Directives) `dd-each` goes on **the element being repeated**, not on a parent container. Declare template variables with `--alias,key`. Inside the loop: - `{{alias}}` / `{{key}}` — mustache syntax for text content and HTML - `$$alias` — double-dollar prefix to reference the current item inside other `dd-*` attribute values - `$model` inside the loop still refers to the **controller's reactive model**, NOT the loop item **dd-each** — iterate array ```html dd-each="myArr --val,key" dd-each="$model.myArr --val,key" dd-each="this.$model.myArr --val,key" dd-each="getItems() --val,key" ``` **dd-each2** — nested loop inside `dd-each` (MUST use `$$` prefix for sub-array) ```html
{{user.name}} {{addr.city}}
``` **dd-entries** — iterate object key-value pairs ```html {{key}} {{val}} ``` **dd-repeat** — clone element N times ```html dd-repeat="$model.myNumber" dd-repeat="multiply(5)" dd-repeat="(this.myNumber + 1)" ``` **dd-each full example:** Controller — `src/controllers/UsersCtrl.js`: ```javascript import { Controller } from '@mikosoft/dodo'; class UsersCtrl extends Controller { async __loader(trx) { await this.loadView('#main', '/src/views/pages/users.html'); } async __init(trx) { this.$model.users = []; } async __rend(trx) { await this.render(); } async __postrend(trx) { const resp = await this.$httpClient.get('/api/users'); this.$model.users = resp.data; // triggers render('users') automatically } deleteUser(key) { this.$model.users = this.$model.users.filter((u, i) => i !== key); } } export default UsersCtrl; ``` View — `src/views/pages/users.html`: ```html ``` **dd-each with `