Building a plugin
A plugin is just a package (or app module) that calls provideNgxViewBuilderExtensions() with a feature pack: an id, optional capabilities, and one or more builder tabs backed by Angular components.
Anatomy: the real Templates plugin
import { EnvironmentProviders } from '@angular/core';
import { provideNgxViewBuilderExtensions } from '@ngxviewbuilder/designer';
import { MyStudio } from './my-studio/my-studio';
export const MY_FEATURE_PACK_ID = 'acme-audit';
export function provideAcmeAuditStudio(): EnvironmentProviders {
return provideNgxViewBuilderExtensions({
featurePacks: [
{
id: MY_FEATURE_PACK_ID,
title: 'Audit studio',
capabilities: ['acme.audit'], // optional gating keys
builderTabs: [
{
id: 'acmeAudit', // tab code
label: 'tab.audit', // UI-dictionary key (or plain text)
component: MyStudio, // any standalone component
order: 900, // position among tabs
},
],
},
],
});
}That's the whole registration. The official Templates plugin (@ngxviewbuilder/plugin-templates) is exactly this thin.
The tab component
Your component renders inside the builder shell and talks to the same services as everything else:
@Component({ standalone: true, template: `...` })
export class MyStudio {
private api = inject(NgxViewBuilderApiService);
structure = computed(() => this.api.getStructure());
addAuditNote(): void {
this.api.updateStructure((s) => {
(s.settings as any).auditNotes = [...(s.settings as any).auditNotes ?? [], { at: Date.now() }];
});
}
}Useful building blocks:
api.getStructure()/api.updateStructure(fn): read/write the view (updates flow to Undo history andstructureChanged).api.onStructureChanged.add(...): refresh when the creator edits elsewhere.- Custom data can live in
structure.settingsunder your own key, and unknown settings keys are preserved by the core. api.translateUi(key, fallback)+uiDictionariesin your extensions config: localise your tab.- Tab labels starting with
tab.resolve through UI dictionaries automatically.
Tab icon, description and the More menu
A tab can bring an icon (a built-in icon name, or SVG markup so it never depends on the host's icon set) and a description, a short line shown under its name in the header's More menu. Plugin tabs show after the built-in ones while they fit and move into the menu when they do not; placement: 'menu' keeps a tab in the menu always, for something people open rarely.
builderTabs: [
{
id: 'acmeAudit',
label: 'tab.audit',
component: MyStudio,
order: 900,
icon: '<svg viewBox="0 -960 960 960">…</svg>',
description: 'tab.audit.description',
},
],A section in the Properties panel
propertyPanels adds a category to the Properties panel of the selected element, after the built-in ones. The component gets two inputs, element (the selected element's model) and elementName (its name, or its path inside a panel):
featurePacks: [
{
id: 'acme-audit',
propertyPanels: [
{ id: 'audit', label: 'propertyCategory.audit', icon: 'history', component: AuditPanel },
],
},
],elementTypes limits it to some element types, order places it among the built-in categories (they use 10 to 90; the default is last). The Documentation plugin's panel is built this way.
Letting AI clients use your plugin
NgxViewBuilderAiPluginRegistryService lets a plugin tell AI clients what it offers. A plugin has an id, a description the model reads, an optional query description, a read(query) function and a list of commands, each with params, an example and a run(args):
inject(NgxViewBuilderAiPluginRegistryService).register({
id: 'audit',
title: 'Audit notes',
description: 'Notes reviewers left on the view. Read them before changing a field someone flagged.',
read: () => ({ notes: auditNotes() }),
commands: [
{
name: 'addNote',
description: 'Adds a review note to an element.',
params: { element: 'Element name or path.', text: 'The note.' },
run: (args) => addNote(String(args['element']), String(args['text'])),
},
],
});Register it from provideEnvironmentInitializer() in your provider. The model finds it through listPlugins() and uses it with readPlugin() and executePlugin(), over MCP nvb_list_plugins, nvb_read_plugin and nvb_plugin_execute (AI plugins). available() and unavailableReason() let the plugin say no, with a reason the model can pass on.
Translating your plugin
Give every text a key and put the English texts in your feature pack's uiDictionaries. Packs merge key by key, so your English never replaces another plugin's English. Hosts translate your keys the same way they translate the builder (UI translations). Text your code builds from words (a status line, a sentence about a rule) needs keys too, with placeholders for the values, so a language with another word order can move them: 'audit.count': '{count} notes'.
Capabilities
capabilities are opaque strings other code can require. Property definitions support requiredFeaturePacks / requiredCapabilities: a property (or other gated UI) shows only when a matching pack is registered. Use this to make core-side features light up when your plugin is installed.
Packaging checklist
- [ ] Ship as an Angular library with a single
provideX()entry point. - [ ] Peer-depend on
@ngxviewbuilder/designer(never bundle it). A plugin adds a builder tab, so the designer is the package it extends; the runtime surface reaches you through the designer's re-exports. - [ ] Version-lock releases to the core version you built against.
- [ ] Keep tab
idstable, since hosts may persist the active tab. - [ ] Style with the design tokens so themes work (Theming).
- [ ] Put every text behind a dictionary key, generated sentences included.
- [ ] Keep your code in your package: everything should leave with
npm uninstall.