ODIN ADMIN Docs v1.0
Building

JavaScript

The template's JavaScript is plain, modern JavaScript — no jQuery, no framework. Every widget follows one small pattern, and the widgets tell your code what happened through events.

How it fits together

  • js/app.js is the core. It creates the global Odin object and runs on every page, after the Bootstrap bundle and before any other template script.
  • Each widget is a component: a class that attaches to every element matching a CSS selector — select[data-ts], [data-table], #sidebar, …
  • When the page has loaded, app.js finds those elements and creates one component per element. You don't call anything to start them; adding the markup is enough.
  • The other files in js/ each add components for one feature. See the JavaScript files table for what each needs.

The component pattern

A component extends Odin.Component, says which elements it attaches to, and does its setup in init(). Then it's registered under a name:

js/copy-button.js — your own component
// <button data-copy="#apiKey">Copy</button> copies the value of #apiKey
(function (Odin) {
  'use strict';

  class CopyButton extends Odin.Component {
    static selector = '[data-copy]';          // elements it attaches to
    static defaults = { doneText: 'Copied' };   // this.options starts from these

    init() {
      this.target = document.querySelector(this.el.dataset.copy);
      if (!this.target) return;
      this.label = this.el.textContent;
      this.on(this.el, 'click', () => this.copy());
    }

    async copy() {
      await navigator.clipboard.writeText(this.target.value);
      this.el.textContent = this.data('doneText', this.options.doneText);  // data-done-text="…"
      setTimeout(() => { this.el.textContent = this.label; }, 1500);
      this.emit('copied', { value: this.target.value });   // fires "odin:copied"
    }
  }

  Odin.register('copyButton', CopyButton);
})(window.Odin);

Load your file after js/app.js. Every [data-copy] on the page now works — including ones added later, once you call Odin.init() on them (see Content added later).

What a component has

MemberWhat it is
static selectorThe CSS selector of the elements the component attaches to
static defaultsDefault options; this.options starts as a copy of them
this.elThe element this instance belongs to
init()Runs once when the instance is created — do your setup here
this.$(selector), this.$$(selector)Find one / all elements inside this.el ($$ returns an array)
this.on(target, event, handler)addEventListener; with null as target it listens on this.el
this.data(key, fallback)Reads a data-* attribute of this.el ('doneText' reads data-done-text). "true" / "false" become booleans and numbers become numbers; empty or missing gives fallback
this.emit(name, detail)Fires a bubbling odin:name event from this.el, with detail

Components started automatically get no options of their own — this.options is just the defaults. Read per-element settings with this.data(), as the template's components do.

The Odin object

MethodWhat it does
Odin.register(name, Class)Registers a component. If the page has already loaded, it starts on the page right away
Odin.init(root)Starts every registered component inside root (default: the whole page). Elements that already have an instance are skipped, so it's safe to call twice
Odin.init(root, name)The same, for one component only
Odin.get(element, name)The instance of component name on element, or undefined
Odin.create(name, element, options)Creates one instance by hand, with your own options
Odin.delegate(event, selector, handler)One listener on the document for all matching elements, now and later. The handler gets (event, matchedElement)
Odin.dismissible(type, selector, close)Makes [data-dismiss="type"] buttons close their nearest selector (default: remove it)
Odin.$(selector, root), Odin.$$(selector, root)querySelector / querySelectorAll as an array
Odin.componentsA Map of registered names → classes
Examples
// open the mobile menu from your own code
Odin.get(document.getElementById('sidebar'), 'sidebar').open();

// one handler for every "Archive" button, including rows added later
Odin.delegate('click', '[data-archive]', (e, button) => {
  archive(button.dataset.archive);
});

// [data-dismiss="note"] buttons hide their .note-card instead of removing it
Odin.dismissible('note', '.note-card', card => { card.hidden = true; });

Content added later

Components start once, when the page loads. HTML you insert afterwards — rows loaded with fetch, a form in a modal — needs one call to bring its widgets to life:

JavaScript
const html = await (await fetch('/admin/pages/42/edit-form')).text();
const box = document.getElementById('editPanel');
box.innerHTML = html;
Odin.init(box);   // starts selects, date pickers, tooltips, … inside box

Events

Components report what the user did with odin:* events. They bubble, so you can listen on the element or on any parent — even document. The data is in event.detail. These are the places to call your server:

EventFired byevent.detail
odin:sidebar-open, odin:sidebar-close#sidebar on small screens—
odin:form-valid, odin:form-invalidform.needs-validation on submit—
odin:confirmedA [data-confirm] element after "Yes"—
odin:dropA Dragula list ([data-plugin="dragula"]){ item, target, source, order } — order: the data-ids of the target list
odin:chart-readyA chart ([data-plugin="chartjs"], [data-plugin="apexcharts"]){ chart } — the Chart.js or ApexCharts instance
odin:tree-select, -check, -toggle, -move, -create, -rename, -deleteA tree view ([data-tree-view]){ node } + selected, checked, opened, parent, index, from or old
odin:support-periodThe support dashboard, after the period changes{ days }
odin:finance-filterThe finance dashboard, after the period or business unit changes{ period, unit }
odin:sales-countryThe sales dashboard, when a country is chosen (map, table or list){ country }
odin:tree-order-saveThe pages tree table, on "Save order"{ order, previous } — order: [{ id, parent, sort }]
odin:nav-saveThe menu editor, on save{ menu } — the menu as JSON
odin:task-moveThe task board, after a card is dragged{ id, status, sort }
odin:message-open, odin:message-reply, odin:message-status, odin:message-star, odin:message-deleteThe Messages app{ thread } + message (reply), status or starred
odin:ticket-open, odin:ticket-update, odin:ticket-reply, odin:ticket-create, odin:ticket-bulkThe Support Tickets app{ ticket } + field, value, previous (update), message (reply); bulk: { tickets, field, value }
odin:task-changeThe calendar, after a task is moved or resized{ id, start, due }
Saving the task board
document.addEventListener('odin:task-move', e => {
  const { id, status, sort } = e.detail;
  fetch(`/admin/tasks/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ status, sort })
  });
});

The template saves nothing — these events are where your code takes over. The forms, the menu editor and the pages tree only show a "saved" message in the demo.

Ready-made helpers

Some scripts add helpers you can call directly:

HelperFromWhat it does
Odin.loadData(el, name)data-source.jsLoads el's data-source (URL or #id), falls back to data-fallback, shows a notice on fallback or error; returns a Promise
Odin.toast({ type, title, text, timeout, position, … }), Odin.toast.success(text, title), Odin.toast.clear()alerts.jsShows a toast — see Toasts
Odin.Dialog.confirm({ title, text, confirmText, danger })modals.jsAsks a question; resolves to true or false
Odin.Dialog.prompt({ title, label, value, validate })modals.jsAsks for a value; resolves to the text or null
Odin.Dialog.success(title, text), .error(), .warning(), .info()modals.jsA themed message box
Odin.modal('#id')modals.jsThe Bootstrap modal instance — .show(), .hide()
JavaScript
if (await Odin.Dialog.confirm({ title: 'Delete this page?', text: 'It will be moved to the trash.', confirmText: 'Delete' })) {
  await fetch('/admin/pages/42', { method: 'DELETE' });
  Odin.toast({ type: 'success', text: 'Page deleted.' });
}

Components in the template

The names to use with Odin.get(element, name):

NameAttaches toFile
sidebar#sidebarapp.js
submenu[data-toggle="submenu"]
tooltip[data-bs-toggle="tooltip"]
toastTrigger[data-toast]alerts.js
select, datePicker, uploaderselect[data-ts], input[data-fp], [data-dropzone]forms.js
slugField, charCounter, passwordToggle, indeterminate[data-slug-source], [maxlength][data-count], [data-toggle-password], input[data-indeterminate]
formValidatorform.needs-validation
authFormform[data-auth-form]auth.js
confirmTrigger, dialogTrigger, modalFocus[data-confirm], [data-dialog], .modalmodals.js
dataTable[data-table]tables.js
treeTable[data-tree-table]tree-table.js
navEditor[data-nav-editor]navigation.js
supportDashboard[data-support-dashboard]support-dashboard.js
financeDashboard[data-finance-dashboard]finance-dashboard.js
salesDashboard[data-sales-dashboard]sales-dashboard.js
treeView[data-tree-view]tree-view.js
dragBoard, dragCount[data-plugin="dragula"], [data-drag-count]dragula.js
chartjs[data-plugin="chartjs"]chartjs.js
apexcharts[data-plugin="apexcharts"]apexcharts.js
messages[data-messages]messages.js
ticketDesk[data-ticket-desk]support-tickets.js
fileManager[data-file-manager]file-manager.js
taskCalendar[data-task-calendar]calendar.js
taskBoard[data-task-board]kanban.js
ODIN ADMIN v1.0 · Documentation Last updated October 18, 2026