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)); + }); }