Skip to main content

Directives

Create custom attributes for <uizy-box> elements with reusable logic.

Overview

Directives are custom attributes that extend <uizy-box> functionality. They receive the element and a context object with utilities for reactive effects and cleanup.

// Register a directive
uizy.directive("highlight", (el, { value }) => {
el.style.backgroundColor = value || "yellow";
});
<!-- Use it -->
<uizy-box u-highlight="red">Highlighted text</uizy-box>

Syntax

Recommended: Use u- prefix

Always use the u- prefix for better compatibility across build tools and frameworks. Some TSX/JSX transformers struggle with : prefixed attributes.

PrefixExampleNotes
u-u-foo:arg="value"Recommended - Works everywhere
::foo:arg="value"May cause issues with some transformers

Modifiers

Split multiple modifiers into separate attributes for clarity:

<!-- ✅ Recommended -->
<uizy-box u-tooltip:top u-tooltip:arrow u-tooltip:delay="500">
Hover me
</uizy-box>

<!-- ⚠️ May cause parsing issues -->
<uizy-box u-tooltip:top:arrow:delay="500">Hover me</uizy-box>

Registration

Via start()

uizy.start({
directives: {
// Simple directive
uppercase: (el) => {
el.style.textTransform = "uppercase";
},

// With value
color: (el, { value }) => {
el.style.color = value || "inherit";
},

// Reactive directive
bind: (el, { value, effect }) => {
effect(() => {
const store = uizy.$(value, { raw: true });
return store?.subscribe((v) => {
el.textContent = String(v);
});
});
},
},
});

Via uizy.directive()

uizy.directive("tooltip", (el, { value, modifiers }) => {
const position = modifiers.includes("top") ? "top" : "bottom";
// Setup tooltip...
});

Context Object

The second argument provides utilities:

uizy.directive("example", (el, ctx) => {
ctx.value; // Attribute value (string)
ctx.modifiers; // Array of modifier args
ctx.bindings; // All bindings for this directive
ctx.effect; // Register reactive effect (auto-cleanup)
ctx.cleanup; // Register cleanup function
});

value

The attribute value as a string:

<uizy-box u-example="hello world"></uizy-box>
uizy.directive("example", (el, { value }) => {
console.log(value); // "hello world"
});

modifiers

Args from multiple bindings:

<uizy-box u-tooltip:top u-tooltip:arrow="Help text"></uizy-box>
uizy.directive("tooltip", (el, { value, modifiers }) => {
console.log(modifiers); // ["top", "arrow"]
console.log(value); // "" (from first binding)
});

bindings

Access individual binding values:

<uizy-box u-config:theme="dark" u-config:size="lg"></uizy-box>
uizy.directive("config", (el, { bindings }) => {
// [{ arg: "theme", value: "dark" }, { arg: "size", value: "lg" }]

const config = Object.fromEntries(
bindings.map((b) => [b.arg, b.value || true]),
);
// { theme: "dark", size: "lg" }
});

effect

Register a reactive effect with automatic cleanup:

uizy.directive("autosize", (el, { effect }) => {
effect(() => {
const resize = () => {
el.style.height = "auto";
el.style.height = el.scrollHeight + "px";
};

el.addEventListener("input", resize);
return () => el.removeEventListener("input", resize);
});
});

cleanup

Register cleanup functions directly:

uizy.directive("interval", (el, { value, cleanup }) => {
const id = setInterval(
() => {
el.textContent = new Date().toLocaleTimeString();
},
parseInt(value) || 1000,
);

cleanup(() => clearInterval(id));
});

Examples

Focus on Mount

uizy.directive("focus", (el) => el.focus());
<uizy-box u-focus>Auto-focused</uizy-box>

Click Outside

uizy.directive("click-outside", (el, { value, cleanup }) => {
const handler = (e) => {
if (!el.contains(e.target)) {
new Function(value).call(el);
}
};
document.addEventListener("click", handler);
cleanup(() => document.removeEventListener("click", handler));
});
<uizy-box u-click-outside="uizy.emit('modal.close')">
Click outside to close
</uizy-box>

Lazy Load Images

uizy.directive("lazy", (el, { value, cleanup }) => {
const img = el.querySelector("img") || el;

const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
img.src = value;
observer.disconnect();
}
});

observer.observe(el);
cleanup(() => observer.disconnect());
});
<uizy-box u-lazy="/images/large-photo.jpg">
<img src="/images/placeholder.jpg" alt="Photo" />
</uizy-box>

Two-Way Binding

uizy.directive("model", (el, { value, effect }) => {
const input = el.querySelector("input") || el;

effect(() => {
const store = uizy.$(value, { raw: true });
if (!store) return;

const unsubscribe = store.subscribe((v) => {
if (input.value !== v) input.value = v;
});

const onInput = () => store.set(input.value);
input.addEventListener("input", onInput);

return () => {
unsubscribe();
input.removeEventListener("input", onInput);
};
});
});
<uizy-box u-model="form.username">
<input type="text" />
</uizy-box>

Conditional Show/Hide

uizy.directive("show", (el, { value, effect }) => {
effect(() => {
const store = uizy.$(value, { raw: true });
if (!store) {
el.style.display = value ? "" : "none";
return;
}

return store.subscribe((v) => {
el.style.display = v ? "" : "none";
});
});
});
<uizy-box u-show="ui.modalOpen">Modal content</uizy-box>

Inline Styles

uizy.directive("style", (el, { bindings }) => {
for (const { arg, value } of bindings) {
if (arg && value) el.style[arg] = value;
}
});
<uizy-box u-style:color="red" u-style:fontSize="20px"> Styled text </uizy-box>

Best Practices

Always Clean Up

Prevent memory leaks with proper cleanup:

// ✅ Using cleanup()
uizy.directive("interval", (el, { value, cleanup }) => {
const id = setInterval(() => {
/* ... */
}, 1000);
cleanup(() => clearInterval(id));
});

// ✅ Using effect return
uizy.directive("resize", (el, { effect }) => {
effect(() => {
const handler = () => {
/* ... */
};
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
});
});

Handle Missing Values

Provide defaults for optional values:

uizy.directive("color", (el, { value }) => {
el.style.color = value || "inherit"; // ✅ Fallback
});

uizy.directive("delay", (el, { value }) => {
const ms = parseInt(value) || 1000; // ✅ Parse + default
});

Keep Directives Focused

Each directive should do one thing:

// ✅ Single-purpose
uizy.directive("focus", (el) => el.focus());
uizy.directive("uppercase", (el) => el.style.textTransform = "uppercase");

// ❌ Too many responsibilities
uizy.directive("setup", (el, { modifiers }) => {
if (modifiers.includes("focus")) el.focus();
if (modifiers.includes("uppercase")) /* ... */
// Split into separate directives instead
});

API Reference

uizy.directive(name, handler)

ParameterTypeDescription
namestringDirective name (used as u-name)
handler(el, ctx) => voidHandler function

uizy.directives

MethodDescription
.add(name, handler)Register a directive
.addAll(directives)Register multiple
.get(name)Get handler
.has(name)Check if exists
.names()List all names
.clear()Remove all

Types

interface DirectiveContext {
value: string;
modifiers: string[];
bindings: { arg: string; value: string }[];
effect: (fn: () => (() => void) | void) => void;
cleanup: (fn: () => void) => void;
}

type DirectiveHandler = (el: HTMLElement, ctx: DirectiveContext) => void;