A powerful component system plugin for Zog.js that enables you to create reusable, encapsulated UI components with props, events, slots, and scoped reactivity.
npm install zogjs @zogjs/componentimport { createApp } from 'zogjs';
import { ComponentPlugin } from '@zogjs/component';
const app = createApp(() => ({
message: 'Hello from parent'
}));
// Install the plugin and get access to registerComponent
const { registerComponent } = app.use(ComponentPlugin);
// Register a simple component
registerComponent('greeting', {
template: `<div class="greeting">{{ message }}</div>`,
setup(props) {
return {
message: props.message || 'Hello!'
};
}
});
app.mount('#app');<div id="app">
<z-greeting :message="message"></z-greeting>
</div>- ✅ Reactive Props: Pass data from parent to child with
:prop-namesyntax - ✅ Custom Events: Emit events from child to parent with
@event-namehandlers - ✅ Slots: Project content from parent into child components
- ✅ Scoped Reactivity: Each component has its own isolated reactive scope
- ✅ Setup Function: Define component logic with access to props and emit
- ✅ Kebab-case Support: Automatically converts kebab-case props to camelCase
- ✅ Class & Style Merging: Parent classes and styles are merged with component root
registerComponent('component-name', {
template: '<div>{{ content }}</div>',
setup(props, { emit }) {
// props: object containing all passed props
// emit: function to emit events to parent
// Return reactive data for the template
return {
content: props.initialValue || 'default'
};
}
});- Must have a single root element
- Can use all Zog.js directives (
z-if,z-for,z-model, etc.) - Has access to data returned from
setup()and props
Props allow you to pass data from parent to child components.
Use the : prefix for reactive props:
<!-- Reactive prop (updates when parent data changes) -->
<z-counter :initial-value="startCount"></z-counter>
<!-- Static prop -->
<z-button label="Click me"></z-button>
<!-- Multiple props -->
<z-card :title="cardTitle" :description="cardDesc" theme="dark"></z-card>Props are automatically converted from kebab-case to camelCase:
registerComponent('counter', {
template: `<div>Count: {{ count }}</div>`,
setup(props) {
// :initial-value becomes props.initialValue
const count = ref(props.initialValue || 0);
return { count };
}
});- Reactive props (
:prop-name): Update automatically when parent data changes - Static props (
prop-name): String values that don't change
Components can emit events to communicate with their parent.
registerComponent('custom-button', {
template: `
<button @click="handleClick">
{{ label }}
</button>
`,
setup(props, { emit }) {
const handleClick = () => {
emit('button-clicked', { timestamp: Date.now() });
};
return {
label: props.label || 'Click',
handleClick
};
}
});<z-custom-button
label="Save"
@button-clicked="onSave">
</z-custom-button>const app = createApp(() => ({
onSave(event) {
console.log('Button clicked at:', event.timestamp);
}
}));Slots allow you to pass content from parent to child.
In your component template, use <slot></slot>:
registerComponent('card', {
template: `
<div class="card">
<h2>{{ title }}</h2>
<div class="card-body">
<slot></slot>
</div>
</div>
`,
setup(props) {
return { title: props.title };
}
});Pass content to the slot:
<z-card title="User Profile">
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
</z-card>Note: Currently, only default (unnamed) slots are supported.
A basic counter component:
import { createApp, ref } from 'zogjs';
import { ComponentPlugin } from '@zogjs/component';
const app = createApp(() => ({
initialCount: 0
}));
const { registerComponent } = app.use(ComponentPlugin);
registerComponent('simple-counter', {
template: `
<div class="counter">
<button @click="decrement">-</button>
<span class="count">{{ count }}</span>
<button @click="increment">+</button>
</div>
`,
setup(props, { emit }) {
const count = ref(props.initialValue || 0);
const increment = () => {
count.value++;
emit('change', count.value);
};
const decrement = () => {
count.value--;
emit('change', count.value);
};
return { count, increment, decrement };
}
});
app.mount('#app');<div id="app">
<simple-counter
:initial-value="initialCount"
@change="initialCount = $event">
</simple-counter>
<p>Current value: {{ initialCount }}</p>
</div>A more complex todo list component with multiple features:
import { createApp, ref, reactive, computed } from 'zogjs';
import { ComponentPlugin } from '@zogjs/component';
const app = createApp(() => ({
todos: [
{ id: 1, text: 'Learn Zog.js', done: false },
{ id: 2, text: 'Build a component', done: true }
],
handleTodoChange(todos) {
console.log('Todos updated:', todos);
}
}));
const { registerComponent } = app.use(ComponentPlugin);
// Todo Item Component
registerComponent('todo-item', {
template: `
<li :class="{ completed: todo.done, editing: isEditing }">
<div class="view" z-show="!isEditing">
<input
type="checkbox"
:checked="todo.done"
@change="toggleDone">
<label @dblclick="startEdit">{{ todo.text }}</label>
<button @click="remove" class="destroy">×</button>
</div>
<input
class="edit"
z-show="isEditing"
:value="editText"
@input="editText = $event.target.value"
@blur="finishEdit"
@keyup.enter="finishEdit"
@keyup.escape="cancelEdit">
</li>
`,
setup(props, { emit }) {
const isEditing = ref(false);
const editText = ref('');
const toggleDone = () => {
emit('toggle', props.todo.id);
};
const remove = () => {
emit('remove', props.todo.id);
};
const startEdit = () => {
isEditing.value = true;
editText.value = props.todo.text;
};
const finishEdit = () => {
if (editText.value.trim()) {
emit('edit', { id: props.todo.id, text: editText.value.trim() });
}
isEditing.value = false;
};
const cancelEdit = () => {
isEditing.value = false;
editText.value = props.todo.text;
};
return {
isEditing,
editText,
toggleDone,
remove,
startEdit,
finishEdit,
cancelEdit
};
}
});
// Todo List Component
registerComponent('todo-list', {
template: `
<div class="todo-app">
<header>
<h1>{{ title }}</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
:value="newTodoText"
@input="newTodoText = $event.target.value"
@keyup.enter="addTodo">
</header>
<section class="main" z-show="todos.length > 0">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
:checked="allDone"
@change="toggleAll">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<z-todo-item
z-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@remove="removeTodo"
@edit="editTodo">
</z-todo-item>
</ul>
</section>
<footer class="footer" z-show="todos.length > 0">
<span class="todo-count">
{{ remaining }} {{ remaining === 1 ? 'item' : 'items' }} left
</span>
<ul class="filters">
<li><a :class="{ selected: filter === 'all' }" @click="filter = 'all'">All</a></li>
<li><a :class="{ selected: filter === 'active' }" @click="filter = 'active'">Active</a></li>
<li><a :class="{ selected: filter === 'completed' }" @click="filter = 'completed'">Completed</a></li>
</ul>
<button
class="clear-completed"
@click="clearCompleted"
z-show="completedCount > 0">
Clear completed
</button>
</footer>
</div>
`,
setup(props, { emit }) {
const todos = ref(props.initialTodos || []);
const newTodoText = ref('');
const filter = ref('all');
const remaining = computed(() =>
todos.value.filter(t => !t.done).length
);
const completedCount = computed(() =>
todos.value.filter(t => t.done).length
);
const allDone = computed(() =>
todos.value.length > 0 && remaining.value === 0
);
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.value.filter(t => !t.done);
}
if (filter.value === 'completed') {
return todos.value.filter(t => t.done);
}
return todos.value;
});
const addTodo = () => {
const text = newTodoText.value.trim();
if (text) {
todos.value.push({
id: Date.now(),
text,
done: false
});
newTodoText.value = '';
emit('change', todos.value);
}
};
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id);
if (todo) {
todo.done = !todo.done;
emit('change', todos.value);
}
};
const removeTodo = (id) => {
const index = todos.value.findIndex(t => t.id === id);
if (index > -1) {
todos.value.splice(index, 1);
emit('change', todos.value);
}
};
const editTodo = ({ id, text }) => {
const todo = todos.value.find(t => t.id === id);
if (todo) {
todo.text = text;
emit('change', todos.value);
}
};
const toggleAll = () => {
const done = !allDone.value;
todos.value.forEach(todo => todo.done = done);
emit('change', todos.value);
};
const clearCompleted = () => {
todos.value = todos.value.filter(t => !t.done);
emit('change', todos.value);
};
return {
todos,
newTodoText,
filter,
remaining,
completedCount,
allDone,
filteredTodos,
addTodo,
toggleTodo,
removeTodo,
editTodo,
toggleAll,
clearCompleted,
title: props.title || 'todos'
};
}
});
app.mount('#app');<div id="app">
<todo-list
:initial-todos="todos"
title="My Tasks"
@change="handleTodoChange">
</todo-list>
</div>-
Single Root Element: Always ensure your template has exactly one root element.
-
Prop Naming: Use kebab-case in HTML and camelCase in JavaScript:
<z-component :initial-value="data"></z-component>
setup(props) { console.log(props.initialValue); // camelCase }
-
Event Naming: Use descriptive event names and pass relevant data:
emit('item-selected', { id: item.id, name: item.name });
-
Reactivity: Use
ref()orreactive()for data that needs to be reactive:setup(props) { const count = ref(0); // reactive const user = reactive({ name: 'John' }); // reactive object return { count, user }; }
-
Component Names: Use descriptive, hyphenated names:
registerComponent('user-profile', { ... }); registerComponent('data-table', { ... });
Registers a new component.
Parameters:
name(string): Component name (used as<z-name>in templates)definition(object):template(string, required): Component HTML templatesetup(function, optional): Setup function that receives props and context
Returns: void
setup(props, context)Parameters:
props(object): All props passed to the componentcontext(object):emit(eventName, ...args): Function to emit events to parent
Returns: Object with data and methods for the template
- Ensure the component is registered before mounting the app
- Check that the template has a single root element
- Verify the component name matches (case-insensitive)
- Use
:prop-namesyntax for reactive props - Check that the parent data is reactive (using
ref()orreactive())
- Verify event handler exists in parent scope
- Check console for errors in event handler execution
- Ensure correct event name in both
emit()and@event-name
MIT
Made with ❤️ for nothing