Plugins
Extend Uizy with reusable, namespaced modules.
Overview
Plugins allow you to bundle components, actions, stores, and directives into reusable packages. Each plugin uses a namespace to avoid conflicts.
// Define a plugin
function MyPlugin(app, options) {
app.plugin("mylib", {
components: { ... },
actions: { ... },
stores: { ... },
directives: { ... },
});
}
// Install it
uizy.start({
plugins: [MyPlugin],
});
Creating Plugins
A plugin is a function that receives the Uizy app instance and optional configuration:
function TooltipPlugin(app, options = {}) {
const { defaultPosition = "bottom" } = options;
app.plugin("tooltip", {
// Directives become :tooltip-show, :tooltip-hide, etc.
directives: {
show: (el, { value, modifiers }) => {
const position = modifiers[0] || defaultPosition;
// Setup tooltip...
},
},
// Components become tooltip.dark, tooltip.light, etc.
components: {
dark: () => "bg-gray-900 text-white px-2 py-1 rounded text-sm",
light: () => "bg-white text-gray-900 px-2 py-1 rounded text-sm shadow",
},
});
}
Plugin as Object
Plugins can also be objects with an install method:
const AnimationPlugin = {
install(app, options) {
app.plugin("animate", {
directives: {
fade: (el) => {
el.style.transition = "opacity 0.3s ease";
},
slide: (el, { modifiers }) => {
const direction = modifiers[0] || "up";
// Setup slide animation...
},
},
});
},
};
Installing Plugins
Via start()
uizy.start({
plugins: [
// Plugin without options
MyPlugin,
// Plugin with options
[TooltipPlugin, { defaultPosition: "top" }],
],
});
Duplicate Prevention
Plugins are only installed once. If you try to install the same plugin twice, it will be skipped:
uizy.start({
plugins: [
MyPlugin,
MyPlugin, // Skipped with warning
],
});
Namespace Convention
All plugin exports are prefixed with the namespace:
| Export Type | Registration | Usage |
|---|---|---|
| Components | mylib.button | uizy.use("mylib.button") or use="mylib.button" |
| Actions | mylib.submit | uizy.emit("mylib.submit") or $emit("mylib.submit") |
| Stores | mylib.count | uizy.$("mylib.count") |
| Directives | mylib-hover | :mylib-hover="value" |
Note: Directives use a hyphen (-) instead of a dot (.) to follow HTML attribute naming conventions.
Plugin Exports
components
Register component functions under the namespace:
app.plugin("ui", {
components: {
button: {
primary: () => "bg-blue-500 text-white px-4 py-2 rounded",
secondary: () => "bg-gray-200 text-gray-800 px-4 py-2 rounded",
},
card: () => "bg-white shadow rounded p-4",
},
});
// Usage
// uizy.use("ui.button.primary")
// <uizy-box use="ui.button.primary">
actions
Register action handlers under the namespace:
app.plugin("auth", {
actions: {
login: async (credentials) => {
// Handle login...
},
logout: () => {
// Handle logout...
},
},
});
// Usage
// uizy.emit("auth.login", { email, password })
stores
Register stores under the namespace:
app.plugin("cart", {
stores: {
items: app.store.atom([]),
total: app.store.atom(0),
},
});
// Usage
// uizy.$("cart.items")
// uizy.$set("cart.total", 100)
directives
Register directives with the namespace prefix:
app.plugin("fx", {
directives: {
ripple: (el, { cleanup }) => {
const handler = (e) => {
// Create ripple effect...
};
el.addEventListener("click", handler);
cleanup(() => el.removeEventListener("click", handler));
},
shake: (el) => {
el.style.animation = "shake 0.5s ease";
},
},
});
// Usage
// <uizy-box :fx-ripple>Click me</uizy-box>
// <uizy-box :fx-shake>Error!</uizy-box>
Examples
UI Kit Plugin
function UIKitPlugin(app) {
app.plugin("kit", {
components: {
button: {
base: () => "px-4 py-2 rounded font-medium transition",
primary: () => "px-4 py-2 rounded bg-primary text-white",
ghost: () => "px-4 py-2 rounded hover:bg-gray-100",
},
input: {
text: () => "px-3 py-2 border rounded focus:ring-2",
checkbox: () => "w-4 h-4 rounded",
},
badge: {
success: () =>
"px-2 py-1 text-xs rounded-full bg-green-100 text-green-800",
error: () => "px-2 py-1 text-xs rounded-full bg-red-100 text-red-800",
},
},
});
}
<uizy-box use="kit.button.primary">Submit</uizy-box>
<uizy-box use="kit.badge.success">Active</uizy-box>
Effects Plugin
function EffectsPlugin(app) {
app.plugin("fx", {
directives: {
// :fx-highlight="yellow"
highlight: (el, { value }) => {
el.style.backgroundColor = value || "yellow";
el.style.padding = "2px 4px";
el.style.borderRadius = "2px";
},
// :fx-tooltip:top="Help text"
tooltip: (el, { value, modifiers, cleanup }) => {
const position = modifiers[0] || "bottom";
const tooltip = document.createElement("div");
tooltip.textContent = value;
tooltip.className = "tooltip tooltip-" + position;
document.body.appendChild(tooltip);
const show = () => {
// Position and show tooltip...
};
const hide = () => {
tooltip.style.opacity = "0";
};
el.addEventListener("mouseenter", show);
el.addEventListener("mouseleave", hide);
cleanup(() => {
el.removeEventListener("mouseenter", show);
el.removeEventListener("mouseleave", hide);
tooltip.remove();
});
},
// :fx-show="store.path"
show: (el, { value, effect }) => {
effect(() => {
const store = app.$(value, { raw: true });
if (!store) {
el.style.display = value ? "" : "none";
return;
}
return store.subscribe((v) => {
el.style.display = v ? "" : "none";
});
});
},
// :fx-fade
fade: (el) => {
el.style.transition = "opacity 0.3s ease";
},
},
});
}
<uizy-box :fx-highlight="lightblue">Highlighted</uizy-box>
<uizy-box :fx-tooltip:top="Click to submit" use="kit.button.primary"
>Submit</uizy-box
>
<uizy-box :fx-show="modal.visible" :fx-fade>Modal content</uizy-box>
Form Plugin
function FormPlugin(app) {
app.plugin("form", {
stores: {
data: app.store.map({}),
errors: app.store.map({}),
submitting: app.store.atom(false),
},
actions: {
setField: ({ field, value }) => {
app.$key("form.data", field, value);
},
setError: ({ field, message }) => {
app.$key("form.errors", field, message);
},
clearErrors: () => {
app.$set("form.errors", {});
},
reset: () => {
app.$set("form.data", {});
app.$set("form.errors", {});
},
},
directives: {
// :form-model="fieldName"
model: (el, { value, effect }) => {
const input = el.querySelector("input, textarea, select") || el;
effect(() => {
// Initialize field in form.data
const current = app.$("form.data");
if (!(value in current)) {
app.$key("form.data", value, input.value || "");
}
// Two-way binding
const store = app.$("form.data", { raw: true });
const unsubscribe = store.subscribe((data) => {
if (input.value !== data[value]) {
input.value = data[value] || "";
}
});
const onInput = () => app.$key("form.data", value, input.value);
input.addEventListener("input", onInput);
return () => {
unsubscribe();
input.removeEventListener("input", onInput);
};
});
},
},
});
}
<uizy-box :form-model="username">
<input type="text" placeholder="Username" />
</uizy-box>
<uizy-box :form-model="email">
<input type="email" placeholder="Email" />
</uizy-box>
<uizy-box use="kit.button.primary" :click="$emit('form.submit')"
>Submit</uizy-box
>
API Reference
Plugin Function
type PluginFn<T = unknown> = (app: typeof uizy, options?: T) => void;
Plugin Object
type Plugin<T = unknown> = PluginFn<T> | { install: PluginFn<T> };
Plugin Exports
interface PluginExports {
components?: ComponentTree;
actions?: ComponentTree;
stores?: ComponentTree;
directives?: Record<string, DirectiveHandler>;
}
uizy.plugin(namespace, exports)
Register a namespaced plugin.
| Parameter | Type | Description |
|---|---|---|
namespace | string | Namespace prefix for all exports |
exports | PluginExports | Components, actions, stores, and directives to register |
Best Practices
- Use descriptive namespaces - Choose clear, short names like
ui,fx,auth - Keep plugins focused - One plugin should solve one problem
- Document your exports - List all components, actions, stores, and directives
- Handle options gracefully - Provide sensible defaults for all options
- Clean up directives - Always use
cleanup()to prevent memory leaks