View
@modularityjs/view is a typed component registry and recursive view validator. Apps register Component definitions (name + optional fields + optional slots) into a pool; the framework validates that a given ComponentInstance tree conforms to the registered components at boot time and at the application's request.
It's a building block for headless CMS-style features (page builders, content blocks, email templates) where the backend owns the types of content and the frontend owns the rendering — output is structured JSON, not HTML.
Concepts
Component
A registered building block. May have data fields, may have nested slots, may have both.
interface Component {
readonly name: string;
readonly fields?: readonly ComponentField[];
readonly slots?: readonly ComponentSlot[];
}ComponentField
A piece of typed data carried by a component instance.
interface ComponentField {
readonly name: string;
readonly type: ComponentFieldType;
readonly required?: boolean;
readonly maxLength?: number;
readonly enumValues?: readonly string[];
}
type ComponentFieldType =
| 'string'
| 'number'
| 'boolean'
| 'url' // String with http: or https: protocol
| 'enum' // String constrained to enumValues
| 'json'; // Free-form objectComponentSlot
A position where child components can be nested.
interface ComponentSlot {
readonly name: string;
readonly allowedComponents?: readonly string[]; // Component names permitted here
readonly maxComponents?: number; // Cap on children
}If allowedComponents is omitted, any registered component may appear in the slot. If maxComponents is omitted, the slot is unbounded.
ComponentInstance
A node in the validated tree. The shape carried over the wire and validated against the registry.
interface ComponentInstance {
readonly type: string; // Must resolve to a registered component
readonly data?: Record<string, unknown>;
readonly slots?: Record<string, readonly ComponentInstance[]>;
}Module
import { Module } from '@modularityjs/modularity';
import type { Component } from '@modularityjs/view';
import { ComponentsPool, ViewModule } from '@modularityjs/view';
const hero: Component = {
name: 'hero',
fields: [
{ name: 'title', type: 'string', required: true },
{
name: 'variant',
type: 'enum',
required: true,
enumValues: ['light', 'dark'],
},
{ name: 'link', type: 'url' },
],
};
const richtext: Component = {
name: 'richtext',
fields: [{ name: 'html', type: 'string', required: true, maxLength: 500 }],
};
const columns: Component = {
name: 'columns',
slots: [
{ name: 'left', allowedComponents: ['richtext'] },
{ name: 'right', maxComponents: 2 },
],
};
@Module({
name: 'my-components',
imports: [ViewModule],
pools: [
{ pool: ComponentsPool, key: 'hero', useValue: hero },
{ pool: ComponentsPool, key: 'richtext', useValue: richtext },
{ pool: ComponentsPool, key: 'columns', useValue: columns },
],
})
class MyComponentsModule {}Validation
Inject ViewService and call validate() with the root of a content tree. The validator walks the tree recursively and accumulates ValidationError[] for every issue it finds, then throws a single ValidationException (auto-mapped to HTTP 422 by FrameworkExceptionFilter).
import { Inject, Injectable } from '@modularityjs/di';
import { ViewService } from '@modularityjs/view';
import type { ComponentInstance } from '@modularityjs/view';
@Injectable()
class PreviewController {
constructor(@Inject(ViewService) private readonly view: ViewService) {}
validate(root: ComponentInstance) {
this.view.validate(root); // Throws ValidationException on failure
return { ok: true };
}
}What the validator checks
- Unknown component type — every
instance.typemust resolve to a registered component. - Slot name — every key in
instance.slotsmust match a slot declared on the component. - Slot constraints —
allowedComponentsandmaxComponentsare enforced. - Required fields — fields marked
required: truemust be present (non-undefined, non-null). - Field types —
string/number/boolean/enum/json/url(theurltype also enforceshttp:orhttps:protocol). maxLength— string fields with a configured cap.enumValues— enum fields must be in the declared set.- No slots on slot-less components — a component with no
slotsdeclaration cannot have aninstance.slotsvalue.
Errors carry a structured field path like root.slots.header[0].data.variant, so consumers can show inline messages on the offending node.
Boot-time checks
ViewModule.onInit() fails boot if any of the registered components are inconsistent. This catches misconfiguration before the app starts serving requests:
- Duplicate component names — two components in the pool sharing a
name. - Enum field with no values — a field of
type: 'enum'missing theenumValuesarray. allowedComponentsreferences unknown component — a slot'sallowedComponentslisting a name no other component provides.
All errors are accumulated and reported in a single ValidationException.
listComponents()
Returns the pool's full set of registered components. Useful for admin UIs that need to know what's available, or for emitting a JSON Schema description of the registry.
const components = this.view.listComponents();
// → readonly Component[]See also
apps/view-preview— a two-pane POC: type aComponentInstanceJSON on the left, see the rendered output (or validation errors) on the right. Demonstrates the validator + a renderer-side warning path for components the frontend hasn't implemented.