In-Depth Modules
Every Vueform capability is a Module the core resolves by key on the Vueform Instance, and that indirection is a seam you can work with. The same mechanism that serves the built-ins accepts a different engine behind a Module, a wholesale replacement, or a Module of your own.
Reaching a Module
Inside components, just import the Module composables from @vueform/ui — useHttp(), useDate(), useI18n(), useSecurity() — and call them with no arguments.
Each resolves the app's Vueform Instance by injection, so what you get back already works according to the configuration that instance was created with. Each Module's chapter owns its usage. The same composables also work outside a setup context, passed the instance explicitly or bound to it — see Outside a Setup Context.
Adapters and Plugins
Some Modules separate the feature from the engine behind it. The engine is a slot in the Module's $-options, so you can change what powers a feature while the Module, its consumers, and its types stay as they are. The slots come in three kinds:
- Required engines —
$http.adapter,$date.adapter, and$expression.adapter. These Modules ship no default engine. The Full Bundle wiresfetchHttpAdapter,nativeDateAdapter, andpathExpressionAdapter, while a Modular Setup has to wire them itself. Without one, requests reject, dates throw, and expressions stay inert with a dev warning. - Replacement backends —
$i18n.adapterand$router.adapter. These Modules have a built-in implementation, and the adapter — a function from the Module's options to the full Module surface — replaces it wholesale. - Capability engines —
$security.sanitizerand$security.encrypt/decrypt. None is built in. Without a sanitizer, HTML renders raw — silently whileallowHtmlis unset; with it set, with a dev warning in development and rendering empty in production.
A Plugin's config hook runs before the Modules are created, so a Plugin can fill a slot — or register a whole Module. @vueform/expr-eval sets the expression engine and @vueform/dompurify sets $security.sanitizer for instance. The hook itself is covered in Plugins.
When an adapter slot covers your need, fill it instead of replacing the Module — the slot swaps the engine while everything built around it keeps working.
Swapping a Module
A Module definition is a plain object with a create function and optional install and getCSS hooks, typed as VueformModule<T>. create({ vueform }) runs once per createVueform(), and its return value becomes vueform.modules.<key>. Consumers only ever reach the Module through that key, so any object satisfying the key's interface — ConditionsModule, HttpModule, I18nModule, … — is a drop-in replacement:
import { createVueform } from '@vueform/ui/bundle'
import type { I18nModule } from '@vueform/ui'
createVueform({
modules: {
i18n: {
create: ({ vueform }): I18nModule =>
createMyI18n(vueform.config.$i18n),
},
},
})Because modules merges per key, only i18n is replaced here — every other built-in of @vueform/ui/bundle (the Full Bundle) stays registered. Inside create, the resolved config is vueform.config and the Module's own options are vueform.config.$<name>.
To change a single member instead of the whole surface, delegate to the built-in. Import its definition from @vueform/ui/modules, call its create, and override on the result:
import { createVueform } from '@vueform/ui/bundle'
import { i18n } from '@vueform/ui/modules'
createVueform({
modules: {
i18n: {
create: (ctx) => {
const original = i18n.create(ctx)
const t = original.t
original.t = (tag, params) => {
trackTranslation(tag)
return t(tag, params)
}
return original
},
},
},
})Override on the returned object rather than spreading it into a new one — some surfaces expose getters (the i18n Module's tags), and a spread freezes them to their current value.
A Module that depends on another resolves it at call time with get<Name>Module(vueform), never inside create. Modules are created in key order, so anything read in create may not exist yet, while call-time lookups make registration order irrelevant. A definition without a create function throws at createVueform().
Authoring a Module
A brand-new capability needs the parts every built-in has:
Interface — a type describing what
createreturns. This is the surface consumers program against.Typing — three interfaces augmented through
declare module.VueformModulestypes the registration key,VueformInstanceModulestypesvueform.modules.<key>, andVueformConfigtypes the$-options (the user-facing config derives from it):tsimport type { VueformModule } from '@vueform/ui' import type { AnalyticsModule, AnalyticsModuleOptions } from './analytics' declare module '@vueform/core' { interface VueformModules { analytics?: VueformModule<AnalyticsModule> } interface VueformInstanceModules { analytics?: AnalyticsModule } interface VueformConfig { $analytics: AnalyticsModuleOptions } }Options key — name it
$<key>after the Module. Every$-prefixed config key counts as a Module options bag, which is what makes the orphan-options warning work for custom Modules too.Doorway and stand-in — a
getAnalyticsModule/hasAnalyticsModulepair plus an inert stand-in the getter serves when the Module is absent (or a throw, when no fallback makes sense), and optionally a composable.
Two optional hooks run on the app side. install({ app, vueform }) runs at app.use() — the Theme Module injects its styles there, and devtools sets up its inspector. getCSS({ vueform }) contributes CSS to vueform.getCSS() and getStyleTag() — see Styles on the Server.
For a working reference, most folders in packages/core/src/modules follow the same layout — definition.ts, create*.ts, module.ts for the doorway, types.ts, and noop.ts where the Module degrades.