Chapter 26 — API Quick Reference

All entries verified against packages/create-nativecore/template/.nativecore/ and packages/nativecorejs/. When in doubt, open the source file — it is the canonical truth.


CoreController (@core/controller.js)

import { CoreController } from '@core/controller.js';
MemberSignatureNotes
constructor(root?: HTMLElement)Defaults to document.querySelector('[data-view]')
onMount(): voidOverride; runs after the controller is bound to the view
destroy(): voidCall from the factory cleanup; removes all this.on listeners
this.state<T>(initial: T): State<T>Creates a reactive State scoped to this controller ({ value })
this.signal<T>(initial: T): [get, set]Returns a [getter, setter] tuple (not an alias of this.state)
this.compute<T>(fn: () => T): State<T>Derived reactive value
this.effect`(fn: () => void \(() => void)): void`Runs immediately; re-runs on signal reads inside fn
this.bind(state, el, classOrAttr?)textContent bind, or class toggle / attribute bind when third arg is provided
this.on(target, type, handler)Adds an event listener; removed automatically on destroy()
this.emit(name, detail?, opts?)Dispatches CustomEvent with bubbles: true, composed: true
this.assertRefs(...names: string[])Throws if any ref="name" is not found in the root
this.rebind(root?: HTMLElement)Re-scans refs after dynamic DOM changes
this[refName]HTMLElementAuto-populated from ref="refName" attributes in the view

Factory pattern (required)

export function myController(_params, _state, _loaderData, rootElement) {
    const ctrl = new MyController(rootElement);
    return () => ctrl.destroy();
}

The factory is what lazyController calls. It must return a cleanup function.


CoreComponent (@core/component.js)

import { CoreComponent, defineComponent } from '@core/component.js';
MemberNotes
static useShadowDOMtrue (default) → shadow root; false → light DOM
static observedAttributesstring[] — attributes the browser reports changes for
template()Returns an HTML string (use html\...\`` tag)
onMount()Runs after connectedCallback + template render
onUnmount()Optional cleanup; this.on cleans up automatically
_handleAttributeUpdate(name, value)Called when an observed attribute changes
this.state, this.compute, this.effect, this.bind, this.on, this.emit, this.assertRefs, this.rebindSame surface as CoreController
defineComponent(tag, Class)Calls customElements.define; idempotent

Shadow DOM access

// Read from shadow inside onMount:
const inner = this.shadowRoot.querySelector('.inner');
// Refs are wired to this.refName directly — no manual querySelector for ref="…"

State (@core/state.js)

import { useState, computed, effect, batch, untrack, peek } from '@core/state.js';
ExportSignatureNotes
useState<T>(initial: T): State<T>Creates a reactive state cell
computed<T>(fn: () => T, options?): ComputedState<T>Derived; { pageCleanup: false } skips router flush
effect(fn, options?): () => voidRuns immediately; { pageCleanup: false } for instance scope
batch(fn: () => void): voidDefers signal notifications until fn completes
untrack<T>(fn: () => T): TRun fn without registering signal reads as dependencies
peek<T>(state): TRead state.value without subscribing

State<T> interface

MemberNotes
.valueRead or write
`.set(value \updater)`Write, or updater (prev) => next
.watch(fn)Subscribe to later writes; returns an unsubscribe function

this.state / this.compute / this.effect on controllers and components use this same engine. Instance compute/effect pass { pageCleanup: false } and dispose on destroy() / disconnect.


Reconcile (@core-utils/reconcile.js)

import { reconcile } from '@core-utils/reconcile.js';

reconcile(container, items, item => item.id, item => {
    const li = document.createElement('li');
    li.textContent = item.label;
    return li;
}, (el, item) => { el.textContent = item.label; });

Keyed list: reuse nodes for the same key, move to match order, remove leftovers. create runs only for new keys. Optional update patches reused nodes. Keys are stored on data-nc-key. After inserting nodes with ref attributes, call this.rebind(container).


Context (@core/context.js)

import { createContext, provide, inject } from '@core/context.js';

const Theme = createContext('theme');
const stop = provide(host, Theme, themeState); // State or static value
const value = inject(child, Theme);
const unsub = inject(child, Theme, next => { /* subscribe */ });

Web Components context-request protocol (composed, bubbles through Shadow DOM). Use for theme, locale, or the current user — not as a global store.


Resource (@core/resource.js)

import { resource } from '@core/resource.js';

const box = resource(async (id, signal) => {
    const res = await fetch(`/api/items/${id}`, { signal });
    return res.json();
}, { source: itemId });

box.data.value;
box.loading.value;
box.error.value;
await box.refetch();

Aborts the in-flight request when source changes, on refetch(), and on page navigation (pageCleanup defaults to true).


Router (@core/router.js)

import { router } from '@core/router.js';   // internal — use via app.js
// Public surface in userland:
window.router.navigate('/tasks');
window.router.replace('/tasks');
window.router.back();
window.router.getCurrentRoute();

Registration (inside registerRoutes(r))

r.register(path, htmlFile, controller?, options?)
// returns r — chain .cache(...) on the route just registered
ArgumentTypeRequiredNotes
pathstringyesURL pattern: /tasks, /tasks/:id, /tasks/:id?, /files/*
htmlFilestringyesProject-relative view path, e.g. src/views/public/tasks.html
controller`ControllerFunction \null`noPrefer lazyController('exportName', '../controllers/….js'); omit/null for HTML-only
optionsPartial<RouteConfig>noSee options table below

ControllerFunction:

(params: Record<string, string>, state?: any, loaderData?: unknown)
  => (() => void) | void | Promise<(() => void) | void>

Always return cleanup from factories: () => ctrl.destroy().

options / RouteConfig fields:

KeyTypeNotes
loader(params, signal: AbortSignal) => Promise<unknown>Runs before controller; result → loaderData
layoutstringPath of another registered route used as a layout. May nest: each layout can set layout too. Every layout file needs #route-outlet or [data-route-outlet]. Shared prefix is reused; layout controllers stay mounted until that frame leaves the chain.
disableTransitionbooleanSkip page transition for this route
cachePolicy{ ttl: number, revalidate?: boolean }Prefer .cache() chain instead
title`string \((match) => string)`Sets document.title after a successful load
meta`Record<string, string> \((match) => Record<string, string>)`Sets or creates <meta name> tags

Failed loads dispatch nativecore:route-error on window (ROUTE_ERROR_EVENT). Listen for a custom error UI; the router still shows its 404 fallback. The event is for the failed page load, not each layout frame.

MethodSignatureNotes
r.register(path, htmlFile, controller?, options?)Register a route; returns this
.cache({ ttl: number, revalidate?: boolean })Chain after r.register; ttl in seconds
r.group({ middleware?: string[]; prefix?: string }, fn)Shared middleware tags and/or path prefix

Path syntax

PatternMatches
/tasksExact path
/tasks/:idDynamic segment → params.id
/tasks/:id?Optional segment
/*Wildcard → params.wildcard

Full router API (internal — use in app.js)

MethodNotes
router.use(fn)Global middleware; `fn(route, state?) → boolean \Promise<boolean>`
router.navigate(path, state?)Push history
router.replace(path, state?)Replace history
router.back()history.back()
router.start()Begin listening; call once after registerRoutes
router.prefetch(path)Pre-fetch route HTML into cache
router.bustCache(path?)Invalidate one or all cache entries
router.getCacheSnapshot()Returns { [path]: { html, expires, etag? } }
router.getRouteDebugInfo()Route table with middleware and controller metadata
router.getCurrentRoute()`{ path, params, middleware, … } \null`
router.getTagsForPath(path)Middleware tags registered for a path
router.getPathsForMiddleware(tag)Paths registered under a given middleware tag

window.router (frozen subset): navigate, replace, back, getCurrentRoute only. Do not rely on any other property of window.router being present — it is a frozen proxy, not the full router. Advanced APIs (prefetch, bustCache, getPathsForMiddleware, …) live on window.__NC_ROUTER__ or import router from '@core/router.js'.


Lazy controller (@core/lazyController.js)

import { createLazyController } from '@core/lazyController.js';

const lazyController = createLazyController(import.meta.url);

// Usage in registerRoutes:
r.register('/tasks', 'src/views/public/tasks.html',
    lazyController('tasksController', '../controllers/tasks.controller.js'));
  • First argument: the exported function name in the controller module
  • Second argument: path relative to routes.js (not to app.js)

Middleware (@core/createMiddleware.js)

import { createMiddleware } from '@core/createMiddleware.js';

const sessionMiddleware = createMiddleware('session', async (route, state) => {
    // Return true to allow navigation; false (or a redirect) to block
    return Boolean(localStorage.getItem('session'));
});

router.use(sessionMiddleware);

fn(route, state?) → boolean | Promise<boolean>


HTTP client (@core/http.js)

import http from '@core/http.js';

const data = await http.get('/api/tasks');
await http.post('/api/tasks', { title: 'New task' });
await http.put('/api/tasks/1', { done: true });
await http.patch('/api/tasks/1', { done: true });
await http.delete('/api/tasks/1');

This is the low-level HTTP primitive. For caching, tag invalidation, and the full service layer used by Deskflow, use src/services/api.service.js instead.


Services (src/services/)

api.service

import api from '@services/api.service.js';

api.setBaseURL('https://api.example.com');
await api.get('/tasks');
await api.post('/tasks', body);
await api.put('/tasks/1', body);
await api.patch('/tasks/1', body);
await api.delete('/tasks/1');
await api.getCached('/tasks', { ttl: 60 });
api.invalidateTags(['tasks']);
api.invalidateQuery(['tasks', 'list']);   // query key array, not a path string
api.clearCache();

storage.service

import storage from '@services/storage.service.js';

storage.setStrategy('local');    // 'memory' | 'session' | 'local'
storage.set('key', value);
storage.get('key');
storage.remove('key');
storage.clear();

logger.service

import logger from '@services/logger.service.js';

logger.debug('msg', data);
logger.info('msg', data);
logger.warn('msg', data);
logger.error('msg', data);

Stores (src/stores/)

Built-in stores

StoreExports
appStore.js / .tsappStore — app-wide reactive state
uiStore.js / .tsuiStoresidebarCollapsed, theme, notifications

Generated stores (make:store)

npm.cmd run make:store -- task
# → src/stores/task.store.js
# Exports: taskItems, taskLoading, taskError, taskCount (computed),
#          loadTasks, addTask, removeTask

Path aliases (all projects)

AliasResolves to
@core/.nativecore/core/
@core-utils/.nativecore/utils/
@core-types/.nativecore/types/
@dev/.nativecore/dev/ (dev only)
@testing/.nativecore/testing/
@components/src/components/
@services/src/services/
@utils/src/utils/
@stores/src/stores/
@middleware/src/middleware/
@routes/src/routes/
@config/src/config/
@types/src/types/
@constants/src/constants/

Always include the .js extension when importing through aliases.


Realtime helpers

WebSocket (@core/ws.js)

import { connectWebSocket } from '@core/ws.js';

const socket = connectWebSocket(url, handlers, options);
socket.send(data);
socket.close();
socket.readyState;   // mirrors WebSocket.readyState
socket.isOpen;       // boolean

Handlers: onOpen, onClose, onError, onMessage (raw), onJsonMessage (when parseJson: true), onReconnect(attempt), onReconnectFailed(lastEvent).

Options: protocols, parseJson, reconnect: { maxRetries, baseDelay, maxDelay } | false, heartbeat: { interval, message? }, signal.

SSE (@core/sse.js)

import { connectSSE } from '@core/sse.js';

const disconnect = connectSSE(url, handlers, options);
disconnect();

Handlers: onOpen, onMessage (default event, raw string), onJsonMessage (when parseJson: true), onError, events: { eventName: fn }, eventsJson: { eventName: fn }, onReconnect(attempt), onReconnectFailed.

Options: parseJson, withCredentials, reconnect: { maxRetries, baseDelay, maxDelay } | false, signal.


i18n (@core/i18n.js)

import { configureI18n, i18n, t } from '@core/i18n.js';
APINotes
configureI18n(options)Merges messages; applies fallbackLocale, persist, and defaultLocale (stored nc:locale wins over defaultLocale)
t(key, params?)Translate; {name} placeholders. When params.count is a number, looks up key.{plural} via Intl.PluralRules, then key.other, then key
i18n.setLocale(code)Switch active locale; constructor wires localStorage persist by default
i18n.localeState<string> — watch inside effect to react to locale changes
i18n.extend({ locale: { key: value } })Merge new keys; does not replace existing
i18n.listLocales()Returns string[] of known locale codes
i18n.has(key)true if the key exists in the active locale
i18n.registerNamespace(name, loaderFn)Register an async loader
i18n.loadNamespace(name, locale?)Load a namespace; idempotent
i18n.isNamespaceLoaded(name, locale?)Boolean
i18n.formatNumber(value, options?)Intl.NumberFormat wrapper
i18n.formatCurrency(value, currency, options?)Intl.NumberFormat currency wrapper
i18n.formatDate(value, options?)Intl.DateTimeFormat wrapper
i18n.formatRelative(value, now?)Intl.RelativeTimeFormat wrapper

Testing (@testing/index.js)

import { mountComponent, mountController, navigateAndWait, waitFor, fireEvent } from '@testing/index.js';
HelperSignatureNotes
mountComponent(tag, attrs?) → { element, cleanup }Appends to document.body; call cleanup() after every test
mountController(html, factory) → { root, cleanup }Mounts a [data-view] root, runs the factory, cleans controller + DOM
navigateAndWait(router, path, timeout?) → PromiseResolves on pageloaded; rejects on nativecore:route-error or timeout
waitFor(predicate, timeout?) → Promise<void>Polls until truthy; default timeout 1000 ms
fireEvent(element, eventName, detail?)Dispatches CustomEvent with bubbles: true, composed: true

CLI scripts

Generators

npm.cmd run make:component -- <name> [--defaults] [--with-tests]
npm.cmd run make:core-component -- <name> [--defaults]
npm.cmd run make:controller -- <name> [--defaults]
npm.cmd run make:store -- <name>
npm.cmd run make:view -- <name> [--defaults]
npm.cmd run make:page -- <name> [--defaults]
npm.cmd run make:middleware -- <name>

Removers

npm.cmd run remove:component -- <name>
npm.cmd run remove:core-component -- <name>
npm.cmd run remove:view -- <name> [--yes]
npm.cmd run remove:store -- <name>
npm.cmd run remove:middleware -- <name>

Build and test

ScriptPurpose
npm run devDev server + HMR
npm run buildProduction compile + dev-strip
npm run build:clientClient bundle without strip
npm run build:ssgPre-render public routes to _deploy/
npm run build:fullbuild then build:ssg
npm testVitest (watch mode)
npm.cmd test -- --runVitest single pass
npm run test:coverageCoverage report
npm run typechecktsc --noEmit (TypeScript projects only)
npm run compileesbuild via .nativecore/scripts/watch-compile.mjs --once (JS + TS scaffolds)

Capacitor

ScriptRequires
npm run cap:initAlways available (even without --capacitor)
npm run cap:sync--capacitor flag at scaffold time
npm run cap:android / cap:ios--capacitor + IDE installed
npm run cap:add:android / cap:add:ios--capacitor
npm run cap:run:android / cap:run:ios--capacitor + device/emulator

Events utility (@core-utils/events.js)

import { on, delegate, trackEvents, onClick, onKeydown, onFocus } from '@core-utils/events.js';

Prefer this.on(target, type, handler) inside controllers. Standalone helpers return a disposer. Shorthands: onClick, onChange, onInput, onSubmit, onKeydown, onKeyup, onFocus, onBlur, onFocusin, onFocusout, onScroll, onMouseenter, onMouseleave, onDblclick.


DOM utilities (@core-utils/dom.js)

import { dom } from '@core-utils/dom.js';
APINotes
dom.create(tag, attrs?, ...children)Flat map = HTML attributes. Options bag { attrs, props, dataset, ns, class } when attrs / props / ns is present
dom.setAttrs(el, map)setAttribute writes
dom.removeAttrs(el, ...names)Remove attributes
dom.setProps(el, map)Assign IDL / custom-element properties
dom.assign(el, { attrs, props, dataset, class })Mutate an existing node
dom.query / dom.$ / dom.queryAll / dom.$$Document queries
dom.within / dom.withinAllScoped queries
dom.view(name)[data-view] / [data-hook] / [data-action] helpers
dom.listen(el, type, handler)Listener + disposer; prefer this.on in controllers

Flat maps are HTML attributes. Objects, arrays, and IDL booleans belong in props. Do not attach listeners in create().

const frame = dom.create('iframe', {
    attrs: { title: 'Preview', allowfullscreen: '' },
    props: { src: url, allowFullscreen: true },
});
dom.setProps(player, { questions, open: true });

Persist and timing

import { persistState } from '@core-utils/persist.js';
import { debounce } from '@core-utils/timing.js';
APINotes
persistState(key, initial, { storage? })useState hydrated from localStorage (or session). JSON + quota-safe
debounce(fn, wait?)Delayed call; .cancel() clears a pending invocation

throttle / rafThrottle remain on @core/gpu-animation.js.


Observe and portal

import { clickOutside, mediaQuery, observe } from '@core-utils/observe.js';
import { portal } from '@core-utils/portal.js';
APINotes
clickOutside(el, fn)pointerdown outside el; returns disposer
mediaQuery(query){ matches: State<boolean>, dispose() }
observe(el, { resize?, intersect?, threshold?, root?, rootMargin? })ResizeObserver and/or IntersectionObserver; returns disposer
portal(node, target)Move node into target (element or selector); disposer restores original position

Forms (@core/form.js)

import { useForm, useFieldArray } from '@core/form.js';
import { required, email } from '@core/validators.js';
APINotes
useForm({ initialValues, rules?, asyncRules?, onSubmit? })Reactive fields, errors, asyncErrors, bindField, handleSubmit
form.bindField(name, el)Syncs value via on() (input / change + blur); returns disposer
form.validateAsync()Runs asyncRules; first message string wins per field
form.handleSubmit(fn?)Returns (event?) => Promise<boolean> — sync errors, then async, then fn
useFieldArray(initial){ items, append, remove, move, replace }

handleSubmit is a factory: await form.handleSubmit(onSave)().


Accessibility (@core-utils/a11y.js)

import { lockBodyScroll } from '@core-utils/a11y.js';

lockBodyScroll() is reference-counted and restores overflow + scrollbar compensation. Also exported from nativecorejs and nativecorejs/a11y.


Package-only APIs (not in scaffold core)

These symbols are exported by the nativecorejs npm package when you import from it directly. They are not available through the .nativecore/core/ vendored copy inside a scaffolded app.

APIImport
registerPlugin / unregisterPluginimport { registerPlugin } from 'nativecorejs'
useForm + validatorsScaffold: @core/form.js / @core/validators.js. Also nativecorejs
onError / handleErrorimport { onError, handleError } from 'nativecorejs' — lazy; does not install window listeners until first call
trapFocus / announce / rovingimport { trapFocus } from 'nativecorejs/a11y'
lockBodyScrollScaffold: @core-utils/a11y.js. Also nativecorejs / nativecorejs/a11y

See Appendix B — Package vs Scaffold for the full breakdown.


nc-animation (path-picking component)

You choose a preset name. The component chooses CSS compositor, Web Animations API (GPU transform / opacity), or a canvas particle overlay — whichever is cheapest for that preset. See Chapter 13.

PathPresetsEngine
cssspin, ping, float, glowCompositor @keyframes, no JS after start
waapifade-in, slide-*, pulse, shake, …gpu-animation.ts / Web Animations API
particleconfetti, sparkles, firework, …canvas2d overlay (CPU). Generic WebGL lives in gpu-animation.ts

Triggers: mount \| visible \| hover \| click \| manual. Methods: play(), pause() (WAAPI only), cancel(). Events: start, finish, cancel.


Appendices