03 — Core Framework (qcobjects)¶
Purpose¶
Specify the core package: what it exports, how classes/packages work, and the complete essentials reference — so the core README can be summarized without loss.
Source: QCObjects README §§ Essentials, List/Math, Reference (v2.5.142).
Code pins: https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/<File>.ts.
Definitions below are authoritative.
Scope¶
Repo QCObjects/QCObjects, npm qcobjects (v2.5.142).
Build/test detail: 14-build-scripts-blueprint.
Distribution contract (normative)¶
- Entry points MUST be:
main → public/cjs/index.cjs,module → public/esm/index.mjs,browser → public/browser/QCObjects.js,types → public/types/index.d.ts, withexportsmaps for.,./package.json,./tsconfig*,./*.js|cjs|mjs, and wildcard./*. - Core MUST NOT embed: HTTP servers, CLI parsing, PWA shell, or widget CSS.
- Every public symbol MUST ship type declarations under
public/types/. - Specs in
spec/(jasmine:testsSpec,testsConfigSpec,testsClassFactorySpec,testsGlobalFeaturesSpec,testsTypeSpec);npm testMUST run lint + full suite green.
TypeScript posture (normative)¶
- Runtime requires no transpiler: apps MAY be pure
.js— the browser bundle runs as-is;Class()/Package()/Import()work in plain JavaScript with zero build step (see 06-app-structure boot sequence). - Transpilers allowed: apps MAY be authored in TypeScript — templates ship
src/js/*.ts+*.d.tsalongside compiled output and a bindingbuild:tsscript;tscdeclaration builds are part of every repo's pipeline (see 14-build-scripts-blueprint). - Framework authored in TypeScript: core
src/is 78.tsfiles / 0.js(SDK: 26.ts/ 0.js) atv2.5.142/v2.5.105; every repo carriestsconfig.json+tsconfig.d.json+tsconfig.jasmine.jsonand ships first-party declarations underpublic/types/(thetypes+exportscontract above). Type coverage MUST NOT regress: new public API without declarations fails the release. - Deno: the CLI is Deno-compatible (
deno.json+mod.ts, strict compiler options) — types flow to Deno consumers via the same declarations.
Class system (normative)¶
Class(name, definition)/Class(name, Parent, definition)declares;InheritClassis the common base;_new_is the constructor hook.New(ClassRef, props)instantiates; getters inpropsexecute once._super_(SuperName, method).call(this, params)reaches the parent implementation.ClassFactory(name)returns the factory from the class queue or a package; last same-name declaration wins the bare reference — use fully-qualifiedClassFactory('org.pkg.Name')when extending across packages to protect scope.Package(name, [classes])defines and registers; a barePackage(name)call with no classes throws (retrieval is synchronousClassFactory(name), which throws when the name is missing).- Shortcut aliases (npm interop):
Package()RETURNS the registered class array, and accepts any non-empty array — so packages nest:Package("myfeature", Package("com.mydomain.feature", [MyFeatureClass1, MyFeatureClass2]))registers both classes under the canonical namespace AND re-registers each under themyfeatureshortcut. BothClassFactory("com.mydomain.feature.MyFeatureClass1")andClassFactory("myfeature.MyFeatureClass1")resolve (last-wins scope rules apply per name independently). npm packages SHOULD expose one short alias for their canonical namespace this way so consumersImportthe short name while definitions keep their fully-qualified identity. Import('dotted.package'[, ready][, external])loads<package>.jsfromrelativeImportPath(orremoteImportsPathwhen external);.jsextension is mandatory and unchangeable (security).Export(symbol)lifts a local to top-level scope.[el].Cast(TargetClass)merges another type's properties (e.g. adivcast to a QCObjects class gains abody).Tag(selector)returns a mappable/sortable/filterable element list.Ready(fn)runs after QCObjects init +window.onload; dynamic<component>loads do NOT trigger Ready — use controllerdone()there.GLOBAL.set/getreaches the global scope store.waitUntil(func, exp)runsfunconce whenexp()turns true (use sparingly).
Object inheritance (normative)¶
Concept 1 of 3 (see also: Component inheritance; Nested components routing).
Applies to every QCObjects object — components, controllers, services, views,
models, effects, and plain classes alike. Sources: src/Class.ts,
src/InheritClass.ts, src/super.ts, src/is_a.ts,
pinned at https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/Class.ts.
- Two equivalent modes. Factory:
Class('Child', Parent, definition)buildsclass extends Parentwith the parent's__definitionmerged in (LegacyCopy,__instanceIDstripped so IDs stay unique). Native:class Child extends Parent {…}. Both produce a real prototype chain; both register by name and resolve viaClassFactory. Mixed hierarchies (factory parent + native child and vice versa) MUST work. - Construction:
InheritClass's constructor copies__definition, binds function props from the init object to the instance, and assigns a read-only__instanceID. Factory classes run the_new_hook; native classes useconstructor(o)+super(o)— pass the init object up in both modes or inherited fields stay unset. _super_is registry-based, not chain-based:_super_('Parent','m')returnsClassFactory('Parent')['m']. It therefore works across packages but REQUIRES the parent to be registered under exactly that name — renaming or late-loading the parent breaks the call. Prefer nativesuper.m()inside native classes; reserve_super_for factory definitions and cross-package reaches.- Type checks:
is_a(obj, typeName)checkshierarchy()membership, then__getType__/ObjectName, thentypeof. Use it instead ofinstanceofacross package boundaries (duplicate module copies breakinstanceof). - Rules: names MUST NOT be forbidden words (
Class()throws); every class SHOULD extendInheritClass(directly or transitively) so__instanceID,__classType, andhierarchy()exist; overrides MUST call the parent implementation unless intentionally replacing it.
Component inheritance (normative)¶
Concept 2 of 3: how inheritance specializes components specifically — the template/class/pairing rules that don't apply to plain objects.
- Canonical pattern: subclass a framework component to specialize it —
FormField extends Component;ButtonField/InputField/TextField/EmailField extends FormField(each fixing afieldTypeselector: button/input/textarea);GridComponent,SliderComponent, splash variants. Prefer subclassing over configuring the base with flags. namerule: a subclass inherits the parent'snameunless it overrides it — andnamedrivestemplateURI. A subclass that renders different markup MUST set its ownname(else it silently reuses the parent's template); a subclass that only changes behavior SHOULD keep the parent'snameto reuse its template. Unnamed components log a build warning.- Pairing inheritance:
controllerClass/viewClass/effectClasspairings andcached/tplsource/tplextensionsettings inherit with the subclass — override only what changes (e.g.SlideItemComponentfixeseffectClass="Fade";GridItemComponentfixes an inline template). subcomponentClassspecialization: spawning parents fix a default child in their constructors (SlideListComponentandGridComponentdefault toGridItemComponent) — subclass the parent and fix a narrowersubcomponentClassto specialize a list/grid without touching its logic.DataGridControlleronly READS the attribute (logs when absent); it sets no default.- Shadowed inheritance:
shadowedinherits; a shadowed subclass of a non-shadowed parent (or vice versa) MUST be a conscious choice — mixed trees route templates into different roots (shadowRootvs body).
Canonical class example:
Class('MyClassName',InheritClass,{
propertyName1:0,
propertyName2:'',
classMethod1: function (){ return this.propertyName1; }
});
var o = New(MyClassName,{ propertyName1:1, propertyName2:"some value" });
Native class / new interop (normative)¶
Recent framework versions accept native ES class syntax everywhere the
factory syntax works — detection via __is_raw_class__ (public API:
a function whose source starts with class), pinned at
https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/is_raw_class.ts.
- Declare natively:
class Main extends InheritClass {}is a first-class class definition;New(Main, {})instantiates it (covered bytestsSpec:__instanceIDis a number,__classTypeis"Main"). - Package natively:
Package('org.pkg',[class Card extends Component {...}])registers each class with namespace stamping; a single class may also be passed directly —Package('org.pkg', MyClass)sets__definition.__namespace+__namespaceand registers it. - Resolve natively:
ClassFactory('org.pkg.Name')returns the native class from the package (last registered wins the bare reference, same rule as above). - Instantiate natively:
New()is defined asnew __class__(args), so the nativenewoperator works too — e.g.New(Move,{…}),new Fade(…),new i18n_messages({})(the forms actually used across the SDK sources, which are themselves written in native class syntax). - Introspection:
__getType__names raw classes viaconstructor.name;LegacyCopycopies them branch-aware. Native and factory classes MAY be mixed freely in one package. - New code SHOULD prefer native
class/extendssyntax; theClass()factory remains supported for cross-browser legacy paths and dynamic definitions. newvsNew()is PARITY, not preference: nativenewfor standard construction;New()when you want its undefined-safety (New(undefined)→new Object()) and single-arg defaults. Use either consistently per file.
CONFIG & processors (normative)¶
CONFIG.set(key, value)/CONFIG.get(key);useConfigService=trueloadsconfig.jsonfrom the app basePath viaConfigService(ConfigService.configFileName='config.json'by default).- Encrypted
config.json: encrypt at the config tool (domain + content), paste ciphertext back; decoding is transparent. - Processors (
Processor.setProcessor(fn), non-arrow functions sothisis the handler):$ENV(VAR)(Node/CLI/Collab only),$config(key)(all envs), plus custom$NAME(args):
{ "domain": "localhost", "env1": "$ENV(ENV1)",
"customSettings": { "value1": "$config(domain)" } }
let SERVICE_HOST = function (arg){
var h = this;
return (new URL(h.processors.ENV(arg))).host;
};
Processor.setProcessor(SERVICE_HOST); // enables "$SERVICE_HOST(SERVICE_URL)"
Minimal complete recipe (declare in JSON, define, register — non-arrow so
this is the handler):
{ "foo": "$meta_processor(value)", "num": 10 }
function meta_processor(value){ /* works against the passed param */ }
Processor.setProcessor(meta_processor);
Multiple params arrive positionally (spread contract):
{ "api": "$MAILCHIMP_API(MAILCHIMP_API_KEY,MAILCHIMP_API_SERVER,MAILCHIMP_API_LIST)" }
function MAILCHIMP_API(keyVar, serverVar, listVar){ /* one param per arg */ }
Processor.setProcessor(MAILCHIMP_API);
Template meta processors $…(…) (normative)¶
Separate from CONFIG processors: $name(args) placeholders inside component
templates (and any string in processed config objects, via processObject
recursion) are expanded by Processor.process(template, component) — matched by
\$name((.*)) and invoked positionally AFTER the component instance, i.e.
fn(componentInstance, arg1, arg2, …) with the comma-split args SPREAD
(this is the contract every processor body assumes: mapper(componentInstance,
componentName, valueName), layout(componentInstance, layoutname, cssfile),
MAILCHIMP_API joining three env names).
Sources: src/Processor.ts, src/defaultProcessors.ts (setDefaultProcessors),
pinned at https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/Processor.ts.
⚠️ KNOWN REGRESSION at the pinned tag: the TypeScript migration (commit
9b0dbfc) changed execute() from [component, ...args.split(",")] (spread,
correct) to [component, args?.split(",")] (single array) — so multi-arg
processors ($mapper, $layout, $component, $MAILCHIMP_API) receive all
args bundled in one array and misbehave. The contract above is normative;
the code MUST be fixed back to spread (one-line fix in execute()).
Default meta processors (always registered):
$mapper(componentName,valueName)— renders a list value (componentdata/ prop, else global) as<quick-component name="…" data-k="v" …>items, one per element with its keys asdata-*attributes.$layout(portrait|landscape, cssfile)— emits orientation/aspect-ratio@importrules for the CSS file (mobile-first portrait set + landscape set).$component(name=…, componentClass=…, …)— emits a<component name="…" componentClass="…" …>tag declaration.$quick_component(name=…, componentClass=…, …)— same for<quick-component>.$repeat(length, text)— repeatstextoverrange(length)(inclusive:range(3)yields 4 items), substituting only the FIRST{{index}}per copy (non-global replace).$ENV(VAR)/$config(key)resolve in the same pass where applicable (Node/CLI/Collab for$ENV; everywhere for$config).
Rules: custom meta processors register via Processor.setProcessor(fn) with
non-arrow functions (this is the handler); names MUST be alphanumeric;
processors MUST be pure string transforms (no DOM writes — return markup);
templates SHOULD prefer $component/$mapper over hand-concatenated tags.
Multi-arg form is supported — args arrive positionally after the component
instance (production proof: $MAILCHIMP_API(KEY,SERVER,KEY_LIST) joins three
env vars with -, registered inside the mailchimp lib package itself).
Processors travel with packages: an add-on that needs custom placeholders
MUST register them in its own module (lib/handler entry), never ask the app to
register them — the mailchimp lib's api/*.js registering MAILCHIMP_API at
import time is the canonical pattern.
Component model (normative)¶
Class properties: domain, basePath (auto); templateURI (use
ComponentURI({COMPONENTS_BASE_PATH, COMPONENT_NAME, TPLEXTENSION, TPL_SOURCE}));
tplsource (default|none|inline|external); url, name, method (default GET); data
({{prop}} binding; needs rebuild() to refresh); reload (replace vs append);
cached (load template once; static default or per-instance); routingWay
(hash|pathname|search, set globally via CONFIG — framework default is
hash (ConfigSettings), but stamped app templates set pathname, so the
EFFECTIVE default in real apps is pathname; hash works with zero configuration), validRoutingWays,
routingNodes, routings, routingPath, routingSelected; subcomponents;
body (plain property — assigning it does NOT rebuild routings; the routings
builder runs from construction and the route flow).
Methods: set/get, rebuild() (via componentLoader), Cast(), route(),
fullscreen()/closefullscreen(), css(obj), append(child), attachIn(selector).
<component> tag attributes: name; cached="true" (only "true" counts);
data-* one-way mock bindings (NOT bidirectional); controllerClass;
viewClass; componentClass; effectClass; template-source (passed through
as-is — default|none|inline|external);
tplextension (default html; free-form — any extension, handler must support
it; text formats work natively).
<component name="main"></component> <!-- loads ./templates/main[.tplextension] -->
Minimal complete component (native class + inline template + widget shell):
Package("com.qcobjects", [
class Main extends Component {
name = "main"
tplsource = "inline"
template = `hello {{foo}}!`
data = { foo: "world" }
}
])
RegisterWidget("main-widget")
<main-widget componentClass="Main"></main-widget>
componentClass takes the bare class name (resolved via ClassFactory);
data.foo binds {{foo}}; no external template file needed.
Loaders: componentLoader(instance, load_async) → Promise
(successStandardResponse{request, component} / failStandardResponse{component});
instance __buildSubComponents__(true) (or exported buildComponents(element))
rebuilds the subtree — there is NO [element].buildComponents() element method
(usually automatic anyway).
MVC: Controller (base; done() fires per component load — the hook for
dynamic components), View, VO (value object), DDO (dynamic data object).
Loading transport: XHR vs fetch (normative)¶
Components and services load over different transports by purpose. Sources:
src/componentLoader.ts, src/serviceLoader.ts, pinned at
https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/componentLoader.ts.
- Component templates over HTTP(S) → XHR.
componentLoaderopens an asyncXMLHttpRequestwithcomponent.method(defaultGET), sends the stringifieddataas the body, setsContent-Type: text/html(skipped on PhoneGap), and treats status200as success. Thexhrtravels in the standard response asrequest, sodone({request, component})can inspect status/headers. Success storesresponseTextascomponent.template(cached whencached:true), then feeds the component; any other status rejects. file:URLs →fetch. XHR cannot reliably readfile:everywhere, sofile:-scheme template URLs usefetch(url).then(response.text())when"fetch" in top(sync-XHR fallback otherwise). This is the local-preview / hybrid-app path — same feed pipeline after the text arrives.- Services → one
serviceLoader, four legs (dispatch onservice.kind, then runtime — callers never choose; full detail in 02-architecture §serviceLoaderdispatch detail):rest+ browser → XHR (async forced; headers loop skipping functions;withCredentials;200→done, elsefail()when defined — WARNING: with nofail()method the promise NEVER settles, it does not reject);rest+ Node → built-in http/https/http2 leg (useHTTP2flag, chunk accumulation);mockup/local→ no-networkservice.mockup()/service.local()with{request: null, …}; unknown kind → resolved no-op. StandaloneserviceLoaderNodehelpers (e.g. the OpenAI package's native-https one) parallel the built-in Node leg and MUST keep its shape. Test doubles MUST usekind:"mockup"(not stub URLs) so tests never touch the network. - Cache short-circuit: cached GET components skip the network entirely via
ComplexStorageCache(alternatepath); non-GET always hits the network. - Rules: custom loaders MUST preserve the
{request, component|service}standard-response shape; MUST NOT switch template transport tofetchfor HTTP(S) (progress/status semantics live on thexhr); services MUST definefail()whenever non-200 is a reachable outcome; test doubles MUST usekind:"mockup"(not stub URLs) so tests never touch the network.
Smart widgets (normative)¶
Smart widgets let a component be declared as a native custom element instead of
a <component> tag. Source: src/WidgetsFactory.ts, pinned at
https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/WidgetsFactory.ts.
RegisterWidget(name)/RegisterWidgets(...names)define real custom elements viacustomElements.define(name, class extends _ComponentWidget_). Widget names MUST contain a hyphen (custom-elements requirement).- Register widgets in app code (
customWidgets.ts), e.g.RegisterWidget("signup-form"), then declare<signup-form componentClass="..." controllerClass="...">directly in HTML. - On upgrade, the widget's light-DOM children are cloned into the component
body and
data-*attributes are forwarded onto the body asdata-*— so slots (<h1 slot="title">) and bindings flow through untouched. - All tag attributes (
name,cached,controllerClass,componentClass,effectClass,template-source,tplextension,data-*) work identically on widget tags and<component>tags. - Browser-only:
RegisterWidgetthrows"RegisterWidget is not implemented for non browser ecosystems yet."outside browsers. - New components SHOULD ship a widget name (hyphenated component name) alongside
the
<component>form; templates SHOULD demonstrate the widget form. - Layout shell pattern: page layouts are themselves widgets —
RegisterWidget("layout-basic")+<layout-basic shadowed=true></layout-basic>with the markup inlayout-basic.html(external default template). The layout owns the page subtree, so give it the root-leveldone()as the stack-ready signal (see § Component authoring rules).
Nested components routing (normative)¶
Concept 3 of 3 (see also: Object inheritance; Component inheritance).
Every component owns its routing table, and subcomponents own theirs —
routing is recursive down the Nested Components Stack. Sources:
src/Component.ts (_generateRoutingPaths), src/routings.ts, pinned at
https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/routings.ts.
- Declaration: routings are literal
<routing>child elements of the component body, e.g.<routing path="/one" name="page-one">(reference: view-stack widget example). Each node's attributes become the routing object (path,name, optional per-routingtplextension, plus any custom attributes). Paths accumulate intocomponent.routingPathsand the globalroutingPathsregistry. (Correction: earlier text said “attribute marker” — the mechanism isquerySelectorAll("routing"), i.e. real elements.) - Matching:
pathis a regex where{param}segments become named capture groups;__valid_routings__(routings, routingPath)filters matches and reverses — later declarations win.__routing_params__(routing, routingPath)extracts the params object. - Selection:
routingSelectedis read-only (setting it only logs); force a rebuild withroute(). The current path resolves perroutingWay(hash|pathname|search, from CONFIG, validated againstvalidRoutingWays); location changes re-trigger matching. - Name → template switch (
_reroute_): for every selected routing, the component rebuildstemplateURIfromrouting.nameviaComponentURI(base path + name +tplextension— per-routing override or the component's), clears the body andrebuild()s (reload=trueis set by the staticroute()wrapper, not by_reroute_itself). Sonamepicks the template file (page-one→page-one.html) whilepathpicks when. - Consuming the selection:
routingSelectedis an array — read the current view with.pop().name(reference pattern: insideaddComponentHelperafter__promise__resolves, branch notifications/effects on the name). Dynamic{param}values come from__routing_params__; withassignRoutingParamsset they merge into templatedataatparseTemplatetime. - Defaults & navigation: declare catch-alls as empty/last paths (
/,`); withroutingWay:"pathname", plainanchors drive the switch — no router calls needed. Pair routed views witheffectClass(e.g. aTransitionEffect` of Fade+Move) for animated transitions. - Nesting: setting
bodytriggers the routings builder for that component;__buildSubComponents__then builds each subcomponent, which builds its own routings in turn — so a route selects a chain of component + subcomponents, each rendering its matched template. Shadowed components route into theirshadowRoot(<slot>content follows the same rules). - Components that never declare
routingchildren match nothing and render their default template unconditionally. - New routable components MUST declare explicit
paths (no catch-all reliance) and MUST list validroutingWays they support. - Custom routing management (escape hatch): canonical routing above covers
standard cases, but a component class MAY implement its own routing entirely —
reference
example2-routing.html: aRoutingComponentbuildsroutingsfrom<routing>nodes in_new_, overrides_reroute_()(exact-match ondocument.location[routingWay], template switch, body clear +rebuild()), exposesroute()sweepingGLOBAL.componentsStackby__classType, and is driven by apopstatelistener. Custom routers MUST reuse the<routing>declaration shape and theroutingSelected/templateURI/rebuild()protocol above so nested children keep working; custom matching semantics MUST be documented on the class.
Template handlers (normative)¶
Every component renders its template through a handler class — swappable per
component, which is the framework's other-framework-interop seam. Sources:
src/Component.ts (parseTemplate), src/DefaultTemplateHandler.ts, pinned at
https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/DefaultTemplateHandler.ts.
- Default:
Component.templateHandler = "DefaultTemplateHandler".parseTemplateresolves the name viaClassFactory(fully-qualified custom names work), instantiatesNew(HandlerClass, {component, template}), mergesroutingParamsinto the data when the component setsassignRoutingParams, and returnsinstance.assign(data). - Contract for custom handlers: constructor takes
{component, template};assign(data) -> stringreturns the rendered markup. (templateHandleris a class field so the raw-passthroughelsebranch is unreachable on normalComponentinstances — passthrough applies only to non-Componentcallers.) - Default semantics (
DefaultTemplateHandler.assign): for each string/number datum, run it throughprocessObject(meta processors resolve inside values too),{{key}}global-replace across the template, thenprocessObjectover the whole result. Non-objectdataskips binding with a debug line; processor failures throw naming the component. - Interop: a custom handler MAY parse/emit any syntax — set
templateHandlerto a registered handler class name on exactly the components that need it (e.g. a React-rendered subtree, Mustache/Handlebars templates). Handler choice is per-component, so hybrid apps MUST document which components use non-default handlers and their syntax. - Canonical example — Markdown docs site (reference: docs website):
templateHandler = "MarkdownTemplateHandler"(a plain registered class NAME string, resolved viaClassFactory) onMarkdownComponentrenders.mdtemplates; agenerateDoc.jsbuild step splitsREADME.mdby heading levels intotemplates/components/markdown/<lang>/page_*.mdfiles AND emits the matching<routing path="^/<slug>$" name="markdown/…">entries into a section shell — documentation-as-routed-components, fully generated.
Services (normative)¶
Service props: domain, basePath (auto); url (absolute or basePath-
relative; external:true + serviceLoader for off-origin); name (descriptive,
non-unique); method (GET/POST/PUT/…); data ({{prop}} response binding);
cached (false = always reload). Methods: set/get.
serviceLoader(instance) → Promise; typical definition:
Class('MyTestService',Service,{
name:'myservice', external:true, cached:false, method:'GET',
headers:{'Content-Type':'application/json'},
url:'https://api.github.com/orgs/QuickCorp/repos', withCredentials:false,
done:()=>{ /* service loaded */ }
});
JSONService extends with JSONresponse; override done via
_super_('JSONService','done').call(this,result); drop headers.charset when
the endpoint dislikes it. ConfigService loads config.json.
SourceJS/SourceCSS inject non-package dependencies from controllers
(controller.dependencies.push(New(SourceJS,{external, url, done}))).
Component authoring rules (normative)¶
Harvested from the field-verified scaffolding recipe (2.4 line; key APIs
re-confirmed in v2.5.142 source: hostElements/subtags, shadow-root
handling, tag filter quick-component:not([loaded]),component:not([loaded])
in src/tag_filter.ts).
- Widget vs generic: a smart widget (
<greeting-component>) is a shell — its constructor creates a child generic node (<quick-component name="…">) and copies attributes onto it; the generic node is the real renderer. Prefer plain<quick-component name>with an explicitcomponentClassstring over widget tags nested under shadowed layouts (the widget transform can dropcomponentClassthere, misnaming the component and 404ing its template). - Class resolution: the class comes from the element's
componentClassattribute (default: baseComponent);namedrives onlytemplateURIand registration. Inlinetemplate/datapatterns REQUIRE the real class viacomponentClassor those fields are ignored. One namespace per component (Package("com.x.card",[Card]), referenced as…card.Card). data-*merging: the base constructor merges elementdata-*attributes intodata— do NOT also declare adatafield on a class that wants those values (the field initializes aftersuper()and clobbers them).- Interaction: implement widget behavior in
done()(fires after build with a liveshadowRoot); query viathis.hostElements(selector)(shadow-aware:shadowRootwhen shadowed, elsebody;subtagsis the same getter). Page-level delegated listeners see retargetedevent.target(the host) — useevent.composedPath()[0]+getRootNode()there instead. - Shadow CSS: page CSS cannot reach shadow roots — each template carries
<style>@import url("css/components/….css")</style>and the imported file chains further imports (e.g. compiled Tailwind); the browser resolves the chain inside the shadow root. - Blank component triage: 404 on the
.tpl.htmlXHR (template must exist under the served root) is the #1 cause; enablelogger.debugEnabledand look fortemplate source … is default|inline,type for … is Component(base-class fallback),LOADING COMPONENT DATA, andSomething wrong loading the component. - Third-party lib integration (reference: QR scanner app): vendor the lib
under
js/packages/thirdparty/libs/<lib>/(with its LICENSE), then chain-load it from the controller vialoadDependencies(callback):CONFIG.get("<lib>-path", "<vendored default>")locates the base,CONFIG.get("<lib>-external", false)flips vendored vs CDN, and nestedNew(SourceJS,{url, external, done})pushes ordered dependencies (worker before lib), calling back when ready. Query live DOM throughcomponent.shadowRoot.subelements(selector)(subelementsworks onShadowRootdirectly). HeadlessNew(Component,{templateURI:"", body: el, tplsource:"none"})MAY wrap raw elements as throwaway component instances for framework-flavored DOM utilities.
Effects, Timer, codecs (normative)¶
- Custom effects extend
Effectand overrideapply, delegating via_super_('Fade','apply').apply(this,arguments); engine runs onrequestAnimationFrameand mutates CSS smartly.
Transition effects + apply-effect-to (normative)¶
Sources: src/TransitionEffect.ts, src/Component.ts
(createEffectInstance, applyTransitionEffect, applyObserveTransitionEffect),
pinned at https://github.com/QCObjects/QCObjects/blob/v2.5.142/src/TransitionEffect.ts.
- Declaration:
effectClass="<TransitionEffect subclass>"on the component tag/body +apply-effect-to="<mode>"(absent ="load"). Only two modes exist:load(apply immediately at build) andobserve(apply on first visibility). Any other value applies nothing. loadpath (applyTransitionEffect): resolveseffectClassviaClassFactory(unknown name throws), requires aTransitionEffectsubclass (anything else logs and skips), instantiatesNew(Effect,{component}), and calls.apply(defaultParams).observepath (applyObserveTransitionEffect): watchescomponentRoot(shadowRootwhen shadowed, elsebody) with anIntersectionObserver; on first intersect it applies once and unobserves. WithoutIntersectionObserver, it applies immediately (same asload). Browser-only.TransitionEffectmechanics (packagecom.qcobjects.effects.transitions.base):effects[]lists effect class names applied in order, each resolved viaClassFactoryand invoked with the full param set (alphaFrom/To,angleFrom/To,radiusFrom/To,scaleFrom/To) — defaultsalpha 0→1,angle 180→0,radius 0→30,scale 0→1,duration385.fitToHeight/fitToWidthsize the root from itsoffsetParent/bounding rect first; the root (or shadow host) is forceddisplay:blockbefore effects run.- Canonical example (view transitions):
Class("MainTransitionEffect",TransitionEffect,{duration:2500, defaultParams:{alphaFrom:0, alphaTo:1}, effects:["Fade","MoveXInFromRight"], fitToHeight:true})+effectClass="MainTransitionEffect" apply-effect-to="observe"— fade+slide-in the first time each view scrolls into view. - Rules: effect names in
effects[]MUST all resolve (one typo skips nothing — resolution throws);observeSHOULD be preferred for below-fold content,loadfor above-fold entrances; custom transitions MUST extendTransitionEffect(not rawEffect) to participate in this protocol. Timer.thread({duration, timing(fraction,elapsed), intervalInterceptor(progress)})emulates threads (modern browsers only).Timer.aliveis the master kill-switch (static, defaulttrue; the frame loop checks it) — countdowns and loops MUST observe it, and teardown MUST setTimer.alive = false. Canonical controller pattern (reference: puzzle-gameTimerController): register the controller inglobal(global.set("timerGameController",…)),start()setsalive=trueand threads{duration: component.duration, …}with per-frame UI writes intimingand completion gating inintervalInterceptor(progress==100→ game-over flow →stop()); cross-controller coordination goes throughglobal.get("puzzleController"). Durations SHOULD come from config (e.g.puzzleTimeoutSeconds), never literals._Crypt:New(_Crypt,{string,key})._encrypt()/._decrypt(), or static_Crypt.encrypt(text,key)/_Crypt.decrypt(cipher,key).shortCode()(aliasuniqueId) — one-shot unique token: encrypts two random values under time-based keys and joins the differing chars. Batch idiom (10 tokens, one line —rangeis inclusive sorange(9)yields 10):let tokens = range(9).map(() => shortCode()). Tokens are uniqueness-graded, NOT cryptographic secrets — for sessions/keys use_Cryptwith explicit passphrases, nevershortCode()output.ComplexStorageCache({index, load, alternate})+getCached(id)for localStorage object caching.asyncLoad(fn, args)runs once after the async queue, before Ready.ArrayList(New(ArrayList,[...])),ArrayCollection(array passed directly to_new_, not{source}),.unique(),.table()(unguardedconsole.table— works anywhere, not shell-only),.sort(),.sortBy(prop),.matrix(n[,v]),.matrix2d,.matrix3d,range(n|a,b),.sum(),.avg(),.min(),.max().
Verification¶
node -e "require('qcobjects')"andimport 'qcobjects'both resolve.npx tsc --noEmit -p tsconfig.d.jsonpasses; jasmine suite passes.- Every symbol above resolves at the pinned tag; drift opens a spec-update PR.