Lifecycle

Every Plugster instance moves through the same fixed sequence: construction, initialization, registration, and — optionally — teardown. The framework guarantees an exact contract at each step; respecting it is what keeps outlet wiring deterministic and prevents the ghost-handler bugs that come from skipping a phase.

The sequence #

        
            1. new MyPlugster(outlets, ...deps)
               → constructor: stores dependencies, sets locales, declares event signatures.
                 The DOM has NOT been touched yet.

            2. await instance.init()
               → framework binds outlets, fetches and compiles any declared child templates,
                 then invokes afterInit() once everything is ready.

            3. Plugster.plug(instance)
               → adds the instance to the global registry, wires HTML-declared subscriptions
                 (the data-on-* attributes), and flushes any events that were dispatched before
                 the instance existed.

               ... the Plugster is now live and handling events ...

            4. Plugster.unplug(instance, options?)         (optional, see below)
               → reverses step 3: removes handlers, drops queued events targeting the instance,
                 deletes registry entries. If options.destroy is true, also calls instance.destroy().
        
    

The boot file from Core concepts is exactly this sequence, condensed into an anonymous async IIFE:

        
            import {Plugster} from "https://cdn.jsdelivr.net/gh/paranoid-software/plugster@1.0.14/dist/plugster.min.js";
            import {ExchangeRatesPlugster} from "./exchange-rates-plugster.js";

            (async function () {
                let exchangeRatesPlugster = await new ExchangeRatesPlugster({
                    selectedCurrencyLabel: {},
                    ratesList: {}
                }).init();

                Plugster.plug(exchangeRatesPlugster);
            }());
        
    

Phase 1 — the constructor #

The constructor receives the outlets descriptor as its first argument; any external dependencies your Plugster needs come after that. The contract for this phase is narrow: store references, declare locales, declare event signatures, and forward outlets to super. Nothing else.

        
            constructor(outlets, exchangeRatesSvcs) {
                super(outlets);
                this.exchangeRatesSvcs = exchangeRatesSvcs;
                this.setLocales({ 'es': { 'Loading...': 'Cargando...' } });
            }
        
    

At this point this._ exists but its outlets are empty stubs — the framework has not yet walked the DOM. Touching outlets here works in the sense that JavaScript will not throw, but the handlers you attach will be attached to nothing useful. The same applies to reading this._.root.data('lang') or any other view attribute: it is too early.

Phase 2 — init() and afterInit() #

init() is the framework method you call from the boot block. It returns a promise because it may need to fetch HTML files (child templates declared via data-child-templates). Internally it performs three steps:

  1. Walks the view's DOM and binds each declared outlet to this._.<outletName>.
  2. Loads and compiles every child template, populating the list-outlet API on the relevant outlets.
  3. Calls afterInit() on the instance.

afterInit() is the ONLY safe place for outlet wiring. By the time the framework calls it, outlets exist and are bound; before that, they do not. The vast majority of subtle Plugster bugs trace back to violating this rule.

        
            afterInit() {
                let self = this;
                self._.emailInput.on('keyup', () => self._validate());
                self._.submitButton.on('click', () => self._handleSubmit());
                self._.statusLabel.text(self.translateTo(self.lang, 'Loading...'));
            }
        
    

Note that afterInit() is plain — no async, no await. If your Plugster needs to do async work as part of initialization (a first data fetch, for example), kick it off inside afterInit() and let the rest of the lifecycle continue.

Phase 3 — Plugster.plug() #

Plugster.plug() is the framework's "register this instance" call. It does three things:

  • Adds the instance to Plugster.registry under its lowercased name, and mirrors it on window.plugsters.
  • Scans every other registered Plugster's view for data-on-{thisinstance}-* attributes and wires the matching method on the publisher as a jQuery custom event listener. This is the engine behind HTML-declared subscriptions (see Events).
  • Flushes any queued events. If another Plugster dispatched an event before this one was plugged in, the event was parked in Plugster.eventQueue; plug() delivers it now.

Order matters: await init() must complete before plug() is called. Registering an uninitialized instance is one of the framework's documented anti-patterns — the framework will register an object whose outlets are not yet bound, and any HTML subscriptions that depend on it will fire against missing handles.

Phase 4 — Plugster.unplug() (optional) #

Most Plugsters live for as long as the page does, so you never call unplug explicitly. The phase exists for two specific situations: long-lived single-page applications where a Plugster gets swapped out as the user navigates, and dynamically created Plugsters that mount and unmount in response to events from elsewhere on the page.

The signature accepts either the instance itself or its name (case-insensitive):

        
            Plugster.unplug(myEditor, { reason: 'route-change' });
            Plugster.unplug('MyEditor');
        
    

What unplug does in order:

  • Removes every jQuery handler the framework attached on the instance via registerEventSignature.
  • Drops every queued event that targeted the instance before it was plugged in.
  • Removes every explicit subscription (set up via listenTo) where the instance is publisher or listener.
  • Removes every HTML-declared subscription where the instance is publisher or listener.
  • Deletes the instance from Plugster.registry and from the window.plugsters mirror.
  • If options.destroy === true, calls instance.destroy() last (see next section).

unplug returns true on success and false if the argument cannot be resolved to a known Plugster. The reason option is purely informational — it ends up in the console log entry that unplug writes, which makes audit trails in devtools easier to read.

Phase 5 — destroy() (opt-in cleanup) #

unplug only knows how to tear down the things the framework itself created — outlet handlers, subscriptions, registry entries. If your Plugster wraps resources of its own (a setInterval timer, a MutationObserver, a MobX autorun disposer, an open WebSocket), you need a hook to release them too. That is what destroy() is for.

        
            class MyEditor extends Plugster {
                afterInit() {
                    this.timer = setInterval(() => this.tick(), 1000);
                    this.observer = new MutationObserver(() => this._reflow());
                    this.observer.observe(this._.root[0], { childList: true });
                }
                destroy() {
                    if (this.timer) clearInterval(this.timer);
                    if (this.observer) this.observer.disconnect();
                }
            }

            Plugster.unplug(myEditor, { destroy: true, reason: 'navigation' });
        
    

The framework never calls destroy() automatically. You must pass { destroy: true } to unplug for it to fire. The asymmetry is deliberate: defining a destroy method should not silently change tear-down behavior for callers that do not know it exists. If you ship a Plugster with a destroy method, document that consumers should pass { destroy: true } when they unplug it.

Common mistakes #

  • Wiring events in the constructor. Outlets are empty stubs at that point; the wiring attaches to nothing. Move it to afterInit().
  • Calling Plugster.plug() before await init() resolves. The framework registers a half-initialized instance. Always await the init promise first.
  • Forgetting Plugster.plug() entirely. The Plugster's outlets work in isolation, but HTML-declared subscriptions never wire — the framework never sees the instance.
  • Removing a Plugster from the DOM without calling unplug. Subscriptions and queued events outlive the Plugster, causing handlers from the dead instance to fire on subsequent interactions.
  • Calling unplug(instance) without { destroy: true } when the class defines a destroy(). Subscriptions detach cleanly, but timers, observers and other wrapped resources keep running.

Each of these has a corresponding entry in Anti-patterns with a longer treatment.

Content