Conventions

Component contract

Every JS component exposes the same shape: an initX(scope) function with a complete meta object, registered in the ng.core registry. Same lifecycle, same cleanup, same events — for all of them.

The contract is what lets the core mount, track and unmount any component uniformly, and what the mutation observer relies on to handle markup injected at runtime.

The contract

ng_x.js
export function initX(scope = document) {
    // find roots NOT yet initialized
    const roots = scope.querySelectorAll('.ng-x:not([data-ng-uid])');
    // ... setup, listeners via u.listen(...), per-instance API ...
    return [...roots]; // array of initialized roots
}

initX.meta = {
    name:         'x',
    version:      '1.0',
    description:  'Short operational description of the component.',
    dependencies: [],            // required components, e.g. ['overlay']
    author:       'NexiGrid',
    experimental: false
};

if (window.ng) {
    window.ng.registerComponent('x', initX);
}

Required meta fields

namestring — lowercase, one word ("popover").
versionstring — "1.0", "2.37.0".
descriptionstring — one sentence, max 200 chars.
dependenciesstring[] — [] or e.g. ["overlay"].
authorstring — always "NexiGrid".
experimentalboolean — true while WIP.

Init rules

ScopeinitX(scope = document) — accepts an optional scope so the observer can init partial DOM.
SelectorOnly roots not yet mounted: .ng-x:not([data-ng-uid]). The core assigns data-ng-uid in initComponent — never set it manually.
ListenersAlways via u.listen(...), tracked in root.__ngListeners ({el, type, handler, options}). Direct addEventListener escapes ng.unmount cleanup.
ReturnArray of initialized roots; [] for delegated components (one global listener).

Teardown — __ngProbe

Unmounting (ng.unmount, cleanRegistry) automatically removes the listeners tracked in __ngListeners — but not other resources. Timers, observers (IntersectionObserver, ResizeObserver, MutationObserver), BroadcastChannel, rAF loops or third-party instances are released through a teardown hook on the root:

ng_x.js
root.__ngProbe = {
    teardown() {
        clearInterval(timer);
        observer.disconnect();
        channel.close();
    }
};

The core invokes it once at unmount, before any DOM removal, then deletes it (no double teardown). It is the only supported teardown hook, and it is only for non-listener resources.

Runtime API

Global APIs attach to window.ng.<name>; anchored components expose their per-instance API on the root as root.__ng<Name>:

ng_x.js
// global runtime API
window.ng.snackbar = { show, dismiss };
window.ng.state    = { get, set, watch /* ... */ };

// per-instance API for anchored components (popover, modal, range...)
root.__ngPopover = { open, close, toggle, update };

SCSS-only components

Components without JS (badge, card, pagination, skeleton, spinner, page-loader…) still declare their version in a docblock at the top of _ng_<name>.scss:

_ng_badge.scss
/* ==========================================================
NexiGrid — <Name>
Version: 1.0
Status:  stable
========================================================== */

Custom events

Event names follow ng:<component>:<action> — lowercase, max two levels after ng:. Dispatch on the component root (not on document, except global events like ng:ready), with the standard detail { <target>, trigger?, ...payload }:

ng_popover.js
root.dispatchEvent(new CustomEvent('ng:popover:open', {
    detail: { popover: root, trigger: activeTrigger }
}));

Examples

ng:ready ng:popover:open ng:popover:close ng:modal:confirm ng:modal:cancel ng:state:change ng:aux:copy ng:range:change

Forms like ng-popover-opened or popover:open are invalid.