Skip to content

The CustomWidget lifecycle ​

The AMD module contract ​

script.js must call define() synchronously while it executes, and the factory must return a constructor function. The returned constructor is called with new, and the instance is expected to carry a callbacks object.

js
define(['jquery'], function ($) {
  return function CustomWidget() {
    this.callbacks = {
      render:       function (container) { return true },
      init:         function () { return true },
      bind_actions: function () { return true },
      initMenuPage: function (params) { return true },
      settings:     function ($modal) { return true },
      onSave:       function (settings) { return true },
      destroy:      function () { return true },
    }
  }
})

The loader is deliberately minimal — it is not a general-purpose AMD implementation:

  • The only dependency names that resolve are 'jquery' and '$', both to the jQuery 4 Bito ships. Every other name resolves to undefined — no CDN fetch, no module registry.
  • No named modules, no nested require(), no module cache.
  • If the script loads but never calls define(), or define() returns something that is not a function, the mount fails with an error card.
  • Widget scripts are loaded one at a time. A single define shim services one in-flight load, so two widgets on one screen load in sequence, not in parallel.

Nothing here is a sandbox. The script executes in the admin panel's origin; see the overview.

The callbacks ​

Every callback is optional. A missing callback is simply skipped — it is not an error.

CallbackReceivesWhen it runs
renderThe DOM element your widget should render intoFirst, on mount
init—After render
bind_actions—After init
initMenuPage{ location, item_code, subitem_code }Left-menu pages only: after bind_actions, then again on each subitem switch — see below
settingsThe settings modal body, wrapped in the $ shimWhen the user opens the widget's settings
onSaveThe collected settings objectWhen the user presses Save in that modal
destroy—On unmount, always

render → init → bind_actions run in that order on every mount, and stop early if the widget gets disabled part-way.

INFO

Like amoCRM, Bito hands settings a wrapped modal body — $modal is a collection object, not a raw DOM node, and it is the same $ your module's 'jquery' dependency resolves to. So $modal.find('input[name=api_token]').val() works as written, see jQuery compatibility.

What onSave receives ​

Bito looks for the first <form> inside the settings modal body and hands you its FormData entries as a plain object. If your settings markup has no <form>, you receive {} — build your form as a real <form> with named inputs.

js
onSave: function (settings) {
  if (!settings.api_token) {
    bito.notify({ message: bito.i18n('token_required'), severity: 'error' })
    return false          // blocks the save
  }
  return bito.settings.save(settings).then(function () { return true })
}

initMenuPage ​

Runs only on left-menu pages — the widget_page surface. It receives the open page, the same object as bito.context.menu:

js
initMenuPage: function (params) {
  // { location: "widget_page", item_code: "acme_crm", subitem_code: null }
  // { location: "saleMain", item_code: "acme_crm", subitem_code: "order_sync" }
  root.innerHTML = renderPage(params.subitem_code)   // root: the container render() received
}
  • It is first called once, right after bind_actions.
  • When the user opens another subitem of the same block (the same location), Bito does not remount: your container and state stay, bito.context.menu is updated, and initMenuPage is called again with the new object. Render the new page yourself.
  • If the widget does not define initMenuPage, Bito remounts it on each subitem switch instead — destroy, then render → init → bind_actions — so a simple widget that reads bito.context.menu in render still works.
  • Moving to another block or leaving the widget's pages unmounts the widget and runs destroy.
  • A throw or rejection is caught, logged and counted toward the same two-throw budget as every other callback. The return value is ignored, and no error card is shown for it.

Failure containment ​

Every callback invocation is wrapped. Your widget failing must never white-screen the ERP, so:

  1. A callback that throws is caught and logged, and an error card is rendered in place of the widget. The widget keeps running.
  2. A second throw — of any callback, at any point — disables the widget for the rest of that mount. Nothing further is invoked.
  3. The flag lives on the mounted instance. Navigating away and back re-mounts the widget and resets the counter; it is not persisted.

A rejected promise counts exactly like a synchronous throw: callbacks are awaited.

onSave blocks the save ​

onSave is the one callback whose return value changes what the host does.

Your onSaveResult
Returns falseThe save is blocked with a readable message. This does not count toward the throw budget — returning false is the documented API working, not a bug.
Throws or rejectsThe save is blocked, and it counts as a throw.
Takes longer than 30 secondsThe save is blocked with a timeout message, and it counts as a throw.
Returns anything else, or nothingThe save proceeds.

If the widget is already disabled when the user presses Save, the save is blocked outright.

destroy always runs ​

destroy runs on unmount even after an earlier callback threw, and even once the widget is disabled — it is deliberately not gated on the disabled flag. Its own exceptions are swallowed and logged: teardown must never be the thing that breaks teardown.

Use it to clear timers, abort in-flight requests and detach anything you attached outside your own container. Nothing else cleans up after you — your style.css link and your <script> tag are removed, but global listeners, intervals and DOM you injected elsewhere are yours to undo.

Bito 2.0 Integration API