From 96e0e5aa0c6aa1dfcbd722212c67810897558cdd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 16 May 2026 10:13:26 -0700 Subject: [PATCH] fix(render): strip undeclared inputs in NgComponentOutlet pass (NG0303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RenderElement passes 5 framework-only inputs to every mounted component (bindings, emit, loading, childKeys, spec), plus any user-defined props from the spec. Angular dev mode raises NG0303 for any key the target component doesn't declare — measured ~100 errors per dashboard turn: ~25 from the brief skeleton phase (DefaultFallbackComponent declares zero inputs and rejects all of them) and ~75 from user view components that don't declare the framework keys. Filter via reflectComponentType() before passing to ngComponentOutlet: - Reflection succeeded → pass only the keys the component declares - Reflection failed (uncompiled / non-component) → pass-through The distinction between "0 declared inputs" (filter all) and "no metadata available" (pass-through) is important — naive `.size === 0` treats DefaultFallbackComponent as no-metadata and keeps the spam. Verified via chrome MCP: 0 NG0303 errors per dashboard turn (was ~100), all 4 stat cards / 2 charts / 1 grid still render correctly. Co-Authored-By: Claude Opus 4.7 --- .../src/lib/render-element.component.ts | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/libs/render/src/lib/render-element.component.ts b/libs/render/src/lib/render-element.component.ts index 0f8faae16..ed95792e8 100644 --- a/libs/render/src/lib/render-element.component.ts +++ b/libs/render/src/lib/render-element.component.ts @@ -9,9 +9,11 @@ import { Injector, input, OnInit, + reflectComponentType, runInInjectionContext, signal, type Signal, + type Type, } from '@angular/core'; import { NgComponentOutlet } from '@angular/common'; import { @@ -34,6 +36,38 @@ import type { AngularComponentRenderer } from './render.types'; * the catalog components have no way to declare for arbitrary paths. */ const A2UI_DATAMODEL_PREFIX = 'a2ui:datamodel:'; +/** Cache of declared input names per component class. NgComponentOutlet + * passes every key in its `inputs` prop to the target; Angular dev mode + * raises NG0303 for any input the component doesn't declare. We strip + * undeclared keys before mounting so simple view components (`StatCard`, + * `Container`, etc.) don't get spammed with framework-only inputs + * (`bindings`, `emit`, `loading`, `childKeys`, `spec`) they ignore. */ +/** `null` means reflection failed (likely uncompiled / non-component) — in + * that case we pass inputs through unmodified rather than swallow them. + * An empty Set means the component genuinely declares zero inputs (e.g. a + * pure presentational fallback) and ALL keys should be dropped. */ +const declaredInputsCache = new WeakMap, Set | null>(); +function getDeclaredInputs(cls: Type): Set | null { + if (declaredInputsCache.has(cls)) return declaredInputsCache.get(cls)!; + const meta = reflectComponentType(cls); + const result = meta ? new Set(meta.inputs.map(i => i.templateName)) : null; + declaredInputsCache.set(cls, result); + return result; +} +function filterInputsForClass( + cls: Type | null, + inputs: Record, +): Record { + if (!cls) return inputs; + const declared = getDeclaredInputs(cls); + if (declared === null) return inputs; + const out: Record = {}; + for (const [k, v] of Object.entries(inputs)) { + if (declared.has(k)) out[k] = v; + } + return out; +} + /** Best-effort string→typed coercion for datamodel writes. Catalog * components emit raw string values; the underlying state may have * been declared as number/boolean/array, and consumers reading the @@ -76,13 +110,13 @@ function coerceValue(raw: string): unknown { @if (!element()?.repeat) { @if (visible()) { } } @else { @for (repeatInjector of repeatInjectors(); track $index) { } } @@ -274,6 +308,14 @@ export class RenderElementComponent implements OnInit { }; }); + /** `resolvedInputs` filtered down to keys the target component actually + * declares — silences NG0303 dev-mode warnings from framework-only + * inputs (bindings/emit/loading/childKeys/spec) passed to simple view + * components that don't declare them. */ + readonly filteredResolvedInputs = computed(() => + filterInputsForClass(this.mountClass() as Type | null, this.resolvedInputs()), + ); + // --- Repeat support --- /** Items from the state array for repeat elements. */ @@ -327,4 +369,10 @@ export class RenderElementComponent implements OnInit { }; }); }); + + /** `repeatInputs` filtered per-item to declared component inputs. */ + readonly filteredRepeatInputs = computed(() => { + const cls = this.mountClass() as Type | null; + return this.repeatInputs().map(inputs => filterInputsForClass(cls, inputs)); + }); }